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.
@@ -13,7 +13,7 @@ roles:
13
13
  hints:
14
14
  grep_patterns: ["class ", "export ", "import ", "schema", "CREATE TABLE"]
15
15
  outputs:
16
- - path: ".sillyspec/docs/<project>/scan/ARCHITECTURE.md"
16
+ - path: "{SPEC_ROOT}/docs/<project>/scan/ARCHITECTURE.md"
17
17
  required: true
18
18
  checks:
19
19
  - type: file_exists
@@ -33,7 +33,7 @@ roles:
33
33
  hints:
34
34
  grep_patterns: ["function ", "const ", "async ", "try ", "catch "]
35
35
  outputs:
36
- - path: ".sillyspec/docs/<project>/scan/CONVENTIONS.md"
36
+ - path: "{SPEC_ROOT}/docs/<project>/scan/CONVENTIONS.md"
37
37
  required: true
38
38
  checks:
39
39
  - type: file_exists
@@ -53,13 +53,13 @@ roles:
53
53
  hints:
54
54
  grep_patterns: ["fetch", "http", "WebSocket", "ws", "chokidar"]
55
55
  outputs:
56
- - path: ".sillyspec/docs/<project>/scan/STRUCTURE.md"
56
+ - path: "{SPEC_ROOT}/docs/<project>/scan/STRUCTURE.md"
57
57
  required: true
58
58
  checks:
59
59
  - type: file_exists
60
60
  - type: min_lines
61
61
  min: 15
62
- - path: ".sillyspec/docs/<project>/scan/INTEGRATIONS.md"
62
+ - path: "{SPEC_ROOT}/docs/<project>/scan/INTEGRATIONS.md"
63
63
  required: true
64
64
  checks:
65
65
  - type: file_exists
@@ -77,14 +77,14 @@ roles:
77
77
  hints:
78
78
  grep_patterns: ["TODO", "FIXME", "deprecated", "test", "describe"]
79
79
  outputs:
80
- - path: ".sillyspec/docs/<project>/scan/TESTING.md"
80
+ - path: "{SPEC_ROOT}/docs/<project>/scan/TESTING.md"
81
81
  required: true
82
82
  checks:
83
83
  - type: file_exists
84
84
  - type: min_lines
85
85
  min: 10
86
86
  - type: no_placeholder
87
- - path: ".sillyspec/docs/<project>/scan/CONCERNS.md"
87
+ - path: "{SPEC_ROOT}/docs/<project>/scan/CONCERNS.md"
88
88
  required: true
89
89
  checks:
90
90
  - type: file_exists
@@ -92,7 +92,7 @@ roles:
92
92
  min: 10
93
93
  - type: contains_sections
94
94
  sections: ["代码质量", "依赖风险"]
95
- - path: ".sillyspec/docs/<project>/scan/PROJECT.md"
95
+ - path: "{SPEC_ROOT}/docs/<project>/scan/PROJECT.md"
96
96
  required: true
97
97
  checks:
98
98
  - type: file_exists
@@ -126,7 +126,7 @@ on_check_failure: prompt_retry
126
126
  permissions:
127
127
  write_mode: direct
128
128
  write_scope:
129
- - ".sillyspec/docs/<project>/scan/"
129
+ - "{SPEC_ROOT}/docs/<project>/scan/"
130
130
  allow_shell: true
131
131
  allow_network: false
132
132
  allow_git: false
@@ -0,0 +1,174 @@
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)
@@ -0,0 +1,51 @@
1
+ /**
2
+ * task-09: sanitizeProjectName 字母校验 + 长度≥2 + 编号正则收紧
3
+ *
4
+ * 覆盖:
5
+ * - 纯数字 "0"/"7" → null(核心目标,scan-projects.json 脏数据来源)
6
+ * - "0/7" 清洗 "07" 无字母 → null
7
+ * - "a" 长度<2 → null
8
+ * - "fe" 含字母长度≥2 → 通过
9
+ * - "frontend" / "order-service" → 通过
10
+ * - "前端项目" 全中文清洗后 "" → null
11
+ * - "" → null
12
+ */
13
+ import { sanitizeProjectName } from '../src/run.js'
14
+
15
+ let passed = 0
16
+ let failed = 0
17
+ function assertEqual (actual, expected, msg) {
18
+ const ok = actual === expected
19
+ if (ok) { console.log(`✅ PASS: ${msg}`); passed++ }
20
+ else { console.error(`❌ FAIL: ${msg}\n expected: ${JSON.stringify(expected)}\n actual: ${JSON.stringify(actual)}`); failed++ }
21
+ }
22
+
23
+ // 通过用例
24
+ assertEqual(sanitizeProjectName('frontend'), 'frontend', '"frontend" 含字母长度≥2 通过')
25
+ assertEqual(sanitizeProjectName('order-service'), 'order-service', '"order-service" 含字母+横线 通过')
26
+ assertEqual(sanitizeProjectName('fe'), 'fe', '"fe" 最小长度 2 通过')
27
+ assertEqual(sanitizeProjectName('user_service'), 'user_service', '"user_service" 含下划线 通过')
28
+ assertEqual(sanitizeProjectName('app.v2'), 'app.v2', '"app.v2" 含点+数字 通过')
29
+
30
+ // 拒绝用例(核心目标)
31
+ assertEqual(sanitizeProjectName('0'), null, '"0" 纯数字拒绝')
32
+ assertEqual(sanitizeProjectName('7'), null, '"7" 纯数字拒绝')
33
+ assertEqual(sanitizeProjectName('07'), null, '"07" 纯数字拒绝(无字母)')
34
+ assertEqual(sanitizeProjectName('0/7'), null, '"0/7" 清洗后 "07" 无字母拒绝')
35
+ assertEqual(sanitizeProjectName('123'), null, '"123" 纯数字拒绝')
36
+
37
+ // 长度<2
38
+ assertEqual(sanitizeProjectName('a'), null, '"a" 长度<2 拒绝(即使含字母)')
39
+ assertEqual(sanitizeProjectName('z'), null, '"z" 长度<2 拒绝')
40
+
41
+ // 中文 / 空
42
+ assertEqual(sanitizeProjectName('前端项目'), null, '"前端项目" 全中文清洗后 "" 拒绝')
43
+ assertEqual(sanitizeProjectName(''), null, '空字符串拒绝')
44
+ assertEqual(sanitizeProjectName(' '), null, '纯空白拒绝')
45
+ assertEqual(sanitizeProjectName(null), null, 'null 拒绝')
46
+ assertEqual(sanitizeProjectName(undefined), null, 'undefined 拒绝')
47
+
48
+ console.log(`\n${'='.repeat(50)}`)
49
+ console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
50
+ console.log(`${'='.repeat(50)}`)
51
+ if (failed > 0) process.exit(1)
@@ -0,0 +1,64 @@
1
+ /**
2
+ * task-06: scan post-check 失败补 return + completed 标记推迟 + 平台模式 exit(1)
3
+ *
4
+ * 验证点:
5
+ * - AC-1: run.js:2433-2438 失败分支末尾补 return { stageCompleted:false, currentIdx, nextPendingIdx: currentIdx }
6
+ * - AC-2: 平台模式 (platformOpts.specRoot || platformOpts.runtimeRoot) 时 process.exit(1)
7
+ * - AC-3: 返回结构与 plan contract (run.js:2551 附近) 完全一致
8
+ * - AC-4: 非平台模式不 exit(只有 if platformOpts 守卫内的 exit)
9
+ */
10
+ import { readFileSync } from 'fs'
11
+ import { join, dirname } from 'path'
12
+ import { fileURLToPath } from 'url'
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url))
15
+ const runPath = join(__dirname, '..', 'src', 'run.js')
16
+ const src = readFileSync(runPath, 'utf8')
17
+
18
+ let passed = 0
19
+ let failed = 0
20
+ function check (cond, msg) {
21
+ if (cond) { console.log(`✅ PASS: ${msg}`); passed++ }
22
+ else { console.error(`❌ FAIL: ${msg}`); failed++ }
23
+ }
24
+
25
+ // 定位 scan post-check failed_post_check 分支
26
+ // 已知关键锚:stageData.status = SCAN_STATUS.FAILED_POST_CHECK
27
+ const failedAnchor = src.indexOf("postResult.status === 'failed_post_check'")
28
+ check(failedAnchor > 0, "源码含 'failed_post_check' 失败分支锚点")
29
+
30
+ // 从锚点向后 800 字符内应该有 return { stageCompleted: false ... }
31
+ const tail = src.slice(failedAnchor, failedAnchor + 1200)
32
+ const hasReturnFalse = /return\s*\{\s*stageCompleted:\s*false/.test(tail)
33
+ check(hasReturnFalse, '失败分支末尾补 return { stageCompleted: false, ... }')
34
+
35
+ // nextPendingIdx: currentIdx 字面出现(与 plan contract 一致)
36
+ const hasNextPendingCurrent = /nextPendingIdx:\s*currentIdx/.test(tail)
37
+ check(hasNextPendingCurrent, '失败分支返回 nextPendingIdx: currentIdx(plan contract 对齐)')
38
+
39
+ // 平台模式 process.exit(1)
40
+ const hasExitGuard = /platformOpts\.specRoot\s*\|\|\s*platformOpts\.runtimeRoot/.test(tail)
41
+ const hasExitOne = /process\.exit\(1\)/.test(tail)
42
+ check(hasExitGuard && hasExitOne, '失败分支含平台模式守卫 + process.exit(1)')
43
+
44
+ // 验证 exit(1) 在 if 守卫内(不是裸调)—— 取 exit(1) 位置向前回溯到最近的 } 或 {
45
+ const exitPos = tail.indexOf('process.exit(1)')
46
+ const segmentBeforeExit = tail.slice(0, exitPos)
47
+ const lastOpenBrace = segmentBeforeExit.lastIndexOf('{')
48
+ const lastCloseBrace = segmentBeforeExit.lastIndexOf('}')
49
+ check(lastOpenBrace > lastCloseBrace, 'process.exit(1) 在某个 if 块的 { ... } 内(受守卫保护)')
50
+
51
+ // 验证 exit 守卫表达式在 exit 之前的同一块内
52
+ const guardSegment = tail.slice(Math.max(0, exitPos - 300), exitPos)
53
+ check(/platformOpts\.(specRoot|runtimeRoot)/.test(guardSegment),
54
+ 'process.exit(1) 前的代码块内含平台模式守卫条件')
55
+
56
+ // 对照 plan contract(run.js 内 plan 失败分支 return 结构)
57
+ // 定位 plan 阶段失败分支的 stageCompleted:false return
58
+ const planFailMatch = src.match(/plan[\s\S]{0,2000}?return\s*\{\s*stageCompleted:\s*false[\s\S]{0,200}?\}/)
59
+ check(!!planFailMatch, 'plan contract 存在 stageCompleted:false return(对照基准)')
60
+
61
+ console.log(`\n${'='.repeat(50)}`)
62
+ console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
63
+ console.log(`${'='.repeat(50)}`)
64
+ if (failed > 0) process.exit(1)
@@ -0,0 +1,77 @@
1
+ /**
2
+ * task-09 集成:编号正则收紧 + sanitize 联动
3
+ *
4
+ * 模拟 run.js:2176 处的解析逻辑(正则 + raw 处理 + sanitizeProjectName + filter(Boolean))
5
+ * 验证:
6
+ * - "1. frontend\n2. 0\n3. 7\n4. order-service" → ['frontend', 'order-service']("0"/"7" 被拒)
7
+ * - 步骤说明"1. 执行 init"不进 projectNames(中文不匹配 [a-zA-Z] 开头)
8
+ * - 兜底分支不被污染(outputText 为空 → 走兜底)
9
+ */
10
+ import { sanitizeProjectName } from '../src/run.js'
11
+
12
+ let passed = 0
13
+ let failed = 0
14
+ function assertDeepEqual (actual, expected, msg) {
15
+ const a = JSON.stringify(actual)
16
+ const e = JSON.stringify(expected)
17
+ if (a === e) { console.log(`✅ PASS: ${msg}`); passed++ }
18
+ else { console.error(`❌ FAIL: ${msg}\n expected: ${e}\n actual: ${a}`); failed++ }
19
+ }
20
+
21
+ // 复刻 run.js:2175-2179 的解析链路
22
+ // task-05 B2 延伸修正:编号解析链 .replace 只针对中文长破折号 `—`,
23
+ // 不再误伤 ASCII `-`(否则 order-service 会被切成 order)。
24
+ function parseNumberedList(outputText) {
25
+ if (!outputText) return []
26
+ const numbered = outputText.match(/^\s*\d+\.\s+([a-zA-Z][\w\-.]*)/gm)
27
+ if (!numbered) return []
28
+ const raw = numbered.map(m => m.replace(/^\s*\d+\.\s+/, '').replace(/—.*$/, '').trim())
29
+ return raw.map(sanitizeProjectName).filter(Boolean)
30
+ }
31
+
32
+ // 用例 1:脏数据列表(task-09 核心场景)+ ASCII 连字符保留(task-05 B2 延伸修正)
33
+ assertDeepEqual(
34
+ parseNumberedList('扫描项目列表:\n1. frontend\n2. 0\n3. 7\n4. order-service\n'),
35
+ ['frontend', 'order-service'],
36
+ '脏数据列表:纯数字被拒(task-09);order-service 完整保留(ASCII - 不再被误切)'
37
+ )
38
+
39
+ // 用例 2:步骤说明干扰(方案 A 边界)
40
+ assertDeepEqual(
41
+ parseNumberedList('以下是步骤:\n1. 执行 init\n2. 启动 scan\n3. frontend\n'),
42
+ ['frontend'],
43
+ '步骤说明中"1. 执行 init"中文不匹配,仅"3. frontend"入选'
44
+ )
45
+
46
+ // 用例 3:英文步骤说明干扰(方案 A 已知边界,task-09 §TDD 第 5 步)
47
+ // 注:英文"1. Run scan"会匹配到 "Run",经 sanitize 通过。这是方案 A 的已知边界,
48
+ // task-09 §实现要求 2 选 A 的前提是"sanitizeProjectName 字母校验双保险"——
49
+ // 纯数字场景已解决(本任务目标),英文步骤误匹配留待 execute 发现再切方案 B。
50
+ const r3 = parseNumberedList('Steps:\n1. Run scan first\n2. backend\n')
51
+ assertDeepEqual(
52
+ r3,
53
+ ['Run', 'backend'],
54
+ '英文步骤会误匹配(方案 A 已知边界,纯数字目标已达成)'
55
+ )
56
+
57
+ // 用例 4:空 outputText
58
+ assertDeepEqual(parseNumberedList(''), [], '空 outputText 返回空列表')
59
+
60
+ // 用例 5:含下划线/点的项目名(避开既有 -replace bug,专测 sanitize)
61
+ assertDeepEqual(
62
+ parseNumberedList('1. app.v2\n2. web_api\n'),
63
+ ['app.v2', 'web_api'],
64
+ '合法项目名(点/下划线)保留'
65
+ )
66
+
67
+ // 用例 6:0/7 这种斜杠分隔的会被单条编号捕获再 sanitize 拒绝
68
+ assertDeepEqual(
69
+ parseNumberedList('1. 0/7\n2. frontend\n'),
70
+ ['frontend'],
71
+ '"0/7" 单条:正则匹配失败(首字符 0 非字母)→ 整条丢弃'
72
+ )
73
+
74
+ console.log(`\n${'='.repeat(50)}`)
75
+ console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
76
+ console.log(`${'='.repeat(50)}`)
77
+ if (failed > 0) process.exit(1)
@@ -0,0 +1,84 @@
1
+ /**
2
+ * task-05: scan-docs.yaml 占位符 {SPEC_ROOT} + 项目名优先级
3
+ *
4
+ * 覆盖:
5
+ * - AC-01: 8 处 outputs.path 均为 {SPEC_ROOT}/docs/<project>/scan/*.md
6
+ * - AC-02: write_scope 含 {SPEC_ROOT}/docs/<project>/scan/
7
+ * - AC-07: 旧 yaml(无 {SPEC_ROOT})兼容,replace 不命中也不报错
8
+ */
9
+ import { readFileSync } from 'fs'
10
+ import { join, dirname } from 'path'
11
+ import { fileURLToPath } from 'url'
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url))
14
+ const yamlPath = join(__dirname, '..', 'templates', 'workflows', 'scan-docs.yaml')
15
+ const yaml = readFileSync(yamlPath, 'utf8')
16
+
17
+ let passed = 0
18
+ let failed = 0
19
+ function assert (cond, msg) {
20
+ if (cond) { console.log(`✅ PASS: ${msg}`); passed++ }
21
+ else { console.error(`❌ FAIL: ${msg}`); failed++ }
22
+ }
23
+
24
+ // AC-01: 8 处 outputs.path 改占位符
25
+ const expectedDocs = [
26
+ 'ARCHITECTURE.md',
27
+ 'CONVENTIONS.md',
28
+ 'STRUCTURE.md',
29
+ 'INTEGRATIONS.md',
30
+ 'TESTING.md',
31
+ 'CONCERNS.md',
32
+ 'PROJECT.md',
33
+ ]
34
+ console.log('=== AC-01: outputs.path 改占位符 {SPEC_ROOT} ===')
35
+ for (const doc of expectedDocs) {
36
+ const expectedLine = `path: "{SPEC_ROOT}/docs/<project>/scan/${doc}"`
37
+ assert(yaml.includes(expectedLine), `outputs.path 含 {SPEC_ROOT} 占位符 → ${doc}`)
38
+ }
39
+
40
+ // AC-01 反向:不应再有硬编码 .sillyspec/docs/<project>/scan/X.md
41
+ console.log('\n=== AC-01 反向:不再有硬编码 .sillyspec/docs/<project>/scan/ ===')
42
+ const hardcodedMatches = yaml.match(/\.sillyspec\/docs\/<project>\/scan\//g) || []
43
+ assert(hardcodedMatches.length === 0,
44
+ `outputs.path/write_scope 不再含 ".sillyspec/docs/<project>/scan/"(找到 ${hardcodedMatches.length} 处)`)
45
+
46
+ // AC-02: write_scope 含 {SPEC_ROOT}
47
+ console.log('\n=== AC-02: write_scope 含 {SPEC_ROOT} ===')
48
+ assert(yaml.includes('- "{SPEC_ROOT}/docs/<project>/scan/"'),
49
+ 'write_scope 含 {SPEC_ROOT}/docs/<project>/scan/')
50
+
51
+ // AC-07 兼容:旧 yaml(无 {SPEC_ROOT})replace 不命中(用代码模拟)
52
+ console.log('\n=== AC-07: 旧 yaml 占位符兼容性 ===')
53
+ {
54
+ const legacyPrompt = '写文件到 .sillyspec/docs/<project>/scan/X.md'
55
+ const replaced = legacyPrompt.replace(/\{SPEC_ROOT\}/g, '/tmp/spec')
56
+ assert(!replaced.includes('{SPEC_ROOT}'), '旧 yaml 无 {SPEC_ROOT} 字面:replace 后无残留')
57
+ assert(replaced.includes('.sillyspec/docs/'), '旧 yaml 行为:保留原 .sillyspec/docs/ 路径(向后兼容)')
58
+ }
59
+
60
+ // AC-03: 新 yaml 替换后路径正确
61
+ console.log('\n=== AC-03: 新 yaml 占位符替换 ===')
62
+ {
63
+ const newPrompt = '写文件到 {SPEC_ROOT}/docs/<project>/scan/X.md'
64
+ const step1 = newPrompt.replace(/\{SPEC_ROOT\}/g, '/tmp/spec')
65
+ const step2 = step1.replace(/<project>/g, 'myaaa')
66
+ assert(step2 === '写文件到 /tmp/spec/docs/myaaa/scan/X.md',
67
+ `占位符替换正确:${step2}`)
68
+ assert(!step2.includes('{SPEC_ROOT}'), '替换后无 {SPEC_ROOT} 残留')
69
+ assert(!step2.includes('<project>'), '替换后无 <project> 残留')
70
+ }
71
+
72
+ // 结构完整性:其他字段未动
73
+ console.log('\n=== 结构完整性:其他字段未变 ===')
74
+ assert(yaml.includes('name: scan-docs'), 'yaml name 字段保留')
75
+ assert(yaml.includes('checks:'), 'checks 段保留')
76
+ assert(yaml.includes('path: "scan/"'), 'workflow_level.path "scan/" 相对子路径保留(不动)')
77
+ assert(yaml.includes('retry:'), 'retry 段保留')
78
+ assert(yaml.includes('on_check_failure: prompt_retry'), 'on_check_failure 保留')
79
+ assert(yaml.includes('allow_shell: true'), 'permissions.allow_shell 保留')
80
+
81
+ console.log(`\n${'='.repeat(50)}`)
82
+ console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
83
+ console.log(`${'='.repeat(50)}`)
84
+ if (failed > 0) process.exit(1)
@@ -0,0 +1,85 @@
1
+ /**
2
+ * task-05: run.js post-check 项目名优先级链
3
+ *
4
+ * 覆盖 AC-04/05/06/10: currentProjectName 优先级 =
5
+ * progress.project (dbProjectName) > change.project > steps[idx].project > name 正则 > null
6
+ *
7
+ * 由于 runStage 是大函数,这里用源码字符串校验关键优先级链顺序,
8
+ * 并通过控制台 fixture 模拟实际行为。
9
+ */
10
+ import { readFileSync } from 'fs'
11
+ import { join, dirname } from 'path'
12
+ import { fileURLToPath } from 'url'
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url))
15
+ const runPath = join(__dirname, '..', 'src', 'run.js')
16
+ const src = readFileSync(runPath, 'utf8')
17
+
18
+ let passed = 0
19
+ let failed = 0
20
+ function assert (cond, msg) {
21
+ if (cond) { console.log(`✅ PASS: ${msg}`); passed++ }
22
+ else { console.error(`❌ FAIL: ${msg}`); failed++ }
23
+ }
24
+
25
+ // AC-10: 定位 workflow post_check 段的 currentProjectName 赋值
26
+ console.log('=== AC-10: currentProjectName 优先级链 ===')
27
+
28
+ // 锚点:workflow post_check 段特征字符串
29
+ const anchor = src.indexOf("Workflow post_check:scan 深度扫描完成后自动检查产物")
30
+ assert(anchor > 0, '找到 workflow post_check scan 锚点')
31
+
32
+ // 取该段后 2000 字符内的 currentProjectName 赋值块
33
+ const tail = src.slice(anchor, anchor + 3000)
34
+ const assignStart = tail.indexOf('const currentProjectName')
35
+ assert(assignStart > 0, 'post_check 段含 currentProjectName 赋值')
36
+
37
+ // 赋值块(到下一行 const 或 let 前)
38
+ const assignBlock = tail.slice(assignStart, assignStart + 600)
39
+
40
+ // 验证优先级链:progress.project 必须在 steps[idx].project 之前出现
41
+ const ppPos = assignBlock.indexOf('progress.project')
42
+ const stepsPos = assignBlock.indexOf('steps[currentIdx].project')
43
+ assert(ppPos > 0 && stepsPos > 0, 'currentProjectName 链含 progress.project + steps[currentIdx].project')
44
+ assert(ppPos < stepsPos,
45
+ `优先级正确:progress.project (pos=${ppPos}) 在 steps[idx].project (pos=${stepsPos}) 之前`)
46
+
47
+ // 验证保留兜底:name 正则提取
48
+ assert(/\[([^\]]+)\]\s*\$\//.test(assignBlock) || /\\\[/i.test(assignBlock),
49
+ '保留兜底:steps[idx].name 正则提取仍存在')
50
+
51
+ // AC-05/06: 行为模拟 — 优先级链实际执行结果
52
+ console.log('\n=== AC-05/06: 优先级链行为模拟 ===')
53
+ // 复刻 run.js:2650 修正后的优先级链
54
+ function pickProjectName (progressProject, changeProject, stepProject, stepName) {
55
+ return progressProject
56
+ || changeProject
57
+ || stepProject
58
+ || (stepName && (stepName.match(/\[([^\]]+)\]\s*$/) || [])[1])
59
+ || null
60
+ }
61
+
62
+ // AC-05: progress.project='myaaa' 优先于 steps[idx].project='frontend'
63
+ const r1 = pickProjectName('myaaa', undefined, 'frontend', '深度扫描 [frontend]')
64
+ assert(r1 === 'myaaa', `AC-05: progress.project='myaaa' 优先 → ${r1}`)
65
+
66
+ // AC-06: progress.project 缺失 → 回退 steps[idx].project
67
+ const r2 = pickProjectName(undefined, undefined, 'frontend', '深度扫描 [frontend]')
68
+ assert(r2 === 'frontend', `AC-06: 兜底 steps[idx].project='frontend' → ${r2}`)
69
+
70
+ // 兜底 2: progress/change/step.project 均缺 → name 正则
71
+ const r3 = pickProjectName(undefined, undefined, undefined, '深度扫描 [backend]')
72
+ assert(r3 === 'backend', `兜底 name 正则提取 → ${r3}`)
73
+
74
+ // 全 null
75
+ const r4 = pickProjectName(undefined, undefined, undefined, '深度扫描')
76
+ assert(r4 === null, `全缺 → null(检查所有项目分支)`)
77
+
78
+ // change.project 优先于 steps[idx].project(progress.project 缺失时)
79
+ const r5 = pickProjectName(undefined, 'myaaa', 'frontend', '深度扫描 [frontend]')
80
+ assert(r5 === 'myaaa', `change.project='myaaa' 优先于 steps[idx].project='frontend' → ${r5}`)
81
+
82
+ console.log(`\n${'='.repeat(50)}`)
83
+ console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
84
+ console.log(`${'='.repeat(50)}`)
85
+ if (failed > 0) process.exit(1)
@@ -0,0 +1,52 @@
1
+ /**
2
+ * task-07: run.js workflow post_check anyFailed 阻断
3
+ *
4
+ * 覆盖:
5
+ * - AC: workflow post_check anyFailed 时返回 { stageCompleted:false, currentIdx, nextPendingIdx: currentIdx }
6
+ * (与 task-06 平台模式 scan-postcheck 失败分支 return 结构对齐)
7
+ *
8
+ * 用源码字符串匹配 + 行为模拟。
9
+ */
10
+ import { readFileSync } from 'fs'
11
+ import { join, dirname } from 'path'
12
+ import { fileURLToPath } from 'url'
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url))
15
+ const runPath = join(__dirname, '..', 'src', 'run.js')
16
+ const src = readFileSync(runPath, 'utf8')
17
+
18
+ let passed = 0
19
+ let failed = 0
20
+ function assert (cond, msg) {
21
+ if (cond) { console.log(`✅ PASS: ${msg}`); passed++ }
22
+ else { console.error(`❌ FAIL: ${msg}`); failed++ }
23
+ }
24
+
25
+ console.log('=== workflow post_check anyFailed 阻断 ===')
26
+
27
+ // 锚点:scan 深度扫描 workflow post_check 段
28
+ const anchor = src.indexOf('if (anyFailed)')
29
+ assert(anchor > 0, '找到 if (anyFailed) 锚点')
30
+
31
+ // 取该 if 块前后 500 字符(向前找变量声明,向后找 return)
32
+ const tail = src.slice(anchor, anchor + 500)
33
+
34
+ // 必须有 return { stageCompleted: false ... }
35
+ assert(/return\s*\{\s*stageCompleted:\s*false/.test(tail),
36
+ 'anyFailed 分支含 return { stageCompleted: false }')
37
+
38
+ // 必须有 nextPendingIdx: currentIdx
39
+ assert(/nextPendingIdx:\s*currentIdx/.test(tail),
40
+ 'anyFailed 分支返回 nextPendingIdx: currentIdx')
41
+
42
+ // 必须有 currentIdx(保持当前 step 不推进)
43
+ assert(/currentIdx/.test(tail), 'anyFailed 分支保留 currentIdx 字段')
44
+
45
+ // 必须有 console.log 警告(保留用户可见提示)
46
+ assert(/console\.log/.test(tail) && /存在检查失败项|重试提示/.test(src.slice(anchor - 200, anchor + 300)),
47
+ 'anyFailed 分支保留 console.log 用户提示')
48
+
49
+ console.log(`\n${'='.repeat(50)}`)
50
+ console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
51
+ console.log(`${'='.repeat(50)}`)
52
+ if (failed > 0) process.exit(1)