sillyspec 3.20.1 → 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 (209) 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 +2 -8
  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 -685
  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 -2920
  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 -468
  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/.npmrc-tmp +0 -1
  131. package/CLAUDE.md +0 -18
  132. package/docs/brainstorm-plan-contract.md +0 -64
  133. package/docs/plan-execute-contract.md +0 -123
  134. package/docs/platform-scan-protocol.md +0 -298
  135. package/docs/revision-mode.md +0 -115
  136. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +0 -99
  137. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +0 -218
  138. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +0 -167
  139. package/docs/sillyspec/file-lifecycle/storage-and-state.md +0 -148
  140. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +0 -193
  141. package/docs/sillyspec/file-lifecycle.md +0 -125
  142. package/docs/workflow-contract-regression.md +0 -106
  143. package/docs/worktree-isolation.md +0 -252
  144. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +0 -1
  145. package/packages/dashboard/dist/prototype-dashboard.html +0 -836
  146. package/packages/dashboard/dist/prototype-overview.html +0 -256
  147. package/packages/dashboard/public/prototype-dashboard.html +0 -836
  148. package/packages/dashboard/public/prototype-overview.html +0 -256
  149. package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
  150. package/packages/dashboard/src/components/ProjectCard.vue +0 -187
  151. package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
  152. package/packages/dashboard/src/composables/useLayout.js +0 -131
  153. package/src/brainstorm-postcheck.js +0 -158
  154. package/src/change-list.js +0 -52
  155. package/src/change-risk-profile.js +0 -352
  156. package/src/classify-change.js +0 -73
  157. package/src/constants.js +0 -70
  158. package/src/contract-matrix.js +0 -278
  159. package/src/db.js +0 -201
  160. package/src/endpoint-extractor.js +0 -315
  161. package/src/hooks/claude-pre-tool-use.cjs +0 -125
  162. package/src/hooks/worktree-guard.js +0 -761
  163. package/src/knowledge-match.js +0 -130
  164. package/src/modules.js +0 -482
  165. package/src/scan-postcheck.js +0 -380
  166. package/src/stage-contract.js +0 -700
  167. package/src/stages/brainstorm-auto.js +0 -229
  168. package/src/stages/explore.js +0 -34
  169. package/src/stages/knowledge.js +0 -498
  170. package/src/stages/plan-postcheck.js +0 -513
  171. package/src/sync.js +0 -497
  172. package/src/task-review.js +0 -346
  173. package/src/workflow.js +0 -785
  174. package/src/worktree-apply.js +0 -549
  175. package/src/worktree.js +0 -932
  176. package/templates/workflows/archive-impact.yaml +0 -79
  177. package/templates/workflows/scan-docs.yaml +0 -132
  178. package/test/brainstorm-plan-contract.test.mjs +0 -273
  179. package/test/check-syntax.mjs +0 -26
  180. package/test/cli-top-level-aliases.test.mjs +0 -174
  181. package/test/contract-artifacts.test.mjs +0 -323
  182. package/test/decision-supersede.test.mjs +0 -277
  183. package/test/knowledge-match.test.mjs +0 -231
  184. package/test/plan-execute-contract.test.mjs +0 -330
  185. package/test/plan-optimization.test.mjs +0 -572
  186. package/test/platform-artifacts.test.mjs +0 -190
  187. package/test/platform-failure-samples.test.mjs +0 -199
  188. package/test/platform-recovery-chain.test.mjs +0 -179
  189. package/test/platform-recovery.test.mjs +0 -167
  190. package/test/platform-scan-p0.test.mjs +0 -175
  191. package/test/revision-v1.test.mjs +0 -1145
  192. package/test/run-sanitize-project-name.test.mjs +0 -51
  193. package/test/run-scan-postcheck-fail.test.mjs +0 -64
  194. package/test/run-scan-project-parse.test.mjs +0 -77
  195. package/test/run-tests.mjs +0 -48
  196. package/test/scan-docs-yaml-placeholders.test.mjs +0 -84
  197. package/test/scan-knowledge.test.mjs +0 -175
  198. package/test/scan-paths.test.mjs +0 -68
  199. package/test/scan-postcheck-project-priority.test.mjs +0 -85
  200. package/test/scan-postcheck.test.mjs +0 -183
  201. package/test/scan-workflow-anyfailed-block.test.mjs +0 -52
  202. package/test/spec-dir.test.mjs +0 -206
  203. package/test/stage-contract-failed-post-check.test.mjs +0 -102
  204. package/test/stage-contract.test.mjs +0 -299
  205. package/test/stage-definitions.test.mjs +0 -39
  206. package/test/wait-gates.test.mjs +0 -501
  207. package/test/workflow-spec-base.test.mjs +0 -142
  208. package/test/worktree-guard.test.mjs +0 -136
  209. package/test/worktree-native-overlay.test.mjs +0 -188
@@ -1,51 +0,0 @@
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)
@@ -1,64 +0,0 @@
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)
@@ -1,77 +0,0 @@
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)
@@ -1,48 +0,0 @@
1
- import { readdirSync } from 'node:fs'
2
- import { dirname, join } from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
- import { execFileSync } from 'node:child_process'
5
-
6
- const testDir = dirname(fileURLToPath(import.meta.url))
7
- const files = readdirSync(testDir)
8
- .filter(file => file.endsWith('.test.mjs'))
9
- .sort()
10
-
11
- if (files.length === 0) {
12
- console.log('No test files found')
13
- process.exit(0)
14
- }
15
-
16
- let passed = 0
17
- let failed = 0
18
- const failures = []
19
-
20
- for (const file of files) {
21
- const fullPath = join(testDir, file)
22
- console.log(`\nRunning ${file}`)
23
- try {
24
- const output = execFileSync(process.execPath, [fullPath], {
25
- cwd: testDir,
26
- stdio: ['pipe', 'pipe', 'pipe'],
27
- encoding: 'utf8',
28
- timeout: 120_000
29
- })
30
- if (output) process.stdout.write(output)
31
- passed++
32
- } catch (err) {
33
- if (err.stdout) process.stdout.write(err.stdout)
34
- if (err.stderr) process.stderr.write(err.stderr)
35
- failed++
36
- failures.push(file)
37
- console.log(` ❌ ${file} exited with code ${err.status || 1}`)
38
- }
39
- }
40
-
41
- console.log(`\n${'='.repeat(50)}`)
42
- console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
43
- if (failures.length > 0) {
44
- console.log(`失败文件: ${failures.join(', ')}`)
45
- }
46
- console.log(`${'='.repeat(50)}`)
47
-
48
- process.exit(failed > 0 ? 1 : 0)
@@ -1,84 +0,0 @@
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)
@@ -1,175 +0,0 @@
1
- /**
2
- * scan-knowledge.test.mjs — knowledge 产物校验测试
3
- */
4
-
5
- import { join, resolve, dirname, basename } from 'path'
6
- import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs'
7
- import { fileURLToPath, pathToFileURL } from 'url'
8
- import { tmpdir } from 'os'
9
-
10
- const __filename = fileURLToPath(import.meta.url)
11
- const __dirname = dirname(__filename)
12
- const root = resolve(__dirname, '..')
13
-
14
- const { runScanPostCheck } = await import(pathToFileURL(join(root, 'src', 'scan-postcheck.js')).href)
15
-
16
- let passed = 0, failed = 0
17
-
18
- function assert(cond, msg) {
19
- if (cond) { console.log(` ✅ PASS: ${msg}`); passed++ }
20
- else { console.log(` ❌ FAIL: ${msg}`); failed++ }
21
- }
22
-
23
- function setup(name) {
24
- const cwd = join(tmpdir(), `kn-${name}`)
25
- mkdirSync(cwd, { recursive: true })
26
- return cwd
27
- }
28
- function specSetup(name) {
29
- const d = join(tmpdir(), `kn-${name}-spec`)
30
- mkdirSync(d, { recursive: true })
31
- return d
32
- }
33
- function clean(...dirs) { for (const d of dirs) try { rmSync(d, { recursive: true, force: true }) } catch {} }
34
-
35
- const DOCS = ['ARCHITECTURE.md','CONVENTIONS.md','STRUCTURE.md','INTEGRATIONS.md','TESTING.md','CONCERNS.md','PROJECT.md']
36
-
37
- function writeFull(cwd, specDir) {
38
- const proj = basename(cwd)
39
- for (const d of DOCS) {
40
- const p = join(specDir, 'docs', proj, 'scan', d)
41
- mkdirSync(dirname(p), { recursive: true })
42
- writeFileSync(p, 'author: bot\ncreated_at: 2026-06-19 10:00:00\n# doc\n')
43
- }
44
- }
45
-
46
- // ── 1: knowledge INDEX.md 存在,引用真实文件 → 通过 ──
47
- console.log('\n=== Test 1: knowledge INDEX.md 存在且引用有效 → no knowledge warning ===')
48
- {
49
- const cwd = setup('t1'), spec = specSetup('t1')
50
- try {
51
- writeFull(cwd, spec)
52
- const knowledgeDir = join(spec, 'knowledge')
53
- mkdirSync(knowledgeDir, { recursive: true })
54
- writeFileSync(join(knowledgeDir, 'INDEX.md'),
55
- '# Knowledge Index\n\n## Conventions\n- [kebab-case naming](conventions.md#kebab-case-naming)\n')
56
- writeFileSync(join(knowledgeDir, 'conventions.md'),
57
- '# Conventions\n\n## kebab-case-naming\nFiles use kebab-case.\n')
58
-
59
- const result = runScanPostCheck({ cwd, specDir: spec })
60
- const knowledgeChecks = result.checks.filter(c => c.name.startsWith('knowledge_'))
61
- assert(knowledgeChecks.length === 0, 'no knowledge warnings when INDEX.md + referenced files exist')
62
- } finally { clean(cwd, spec) }
63
- }
64
-
65
- // ── 2: knowledge 目录不存在 → dir_missing 警告 ──
66
- console.log('\n=== Test 2: knowledge 目录不存在 → knowledge_dir_missing ===')
67
- {
68
- const cwd = setup('t2'), spec = specSetup('t2')
69
- try {
70
- writeFull(cwd, spec)
71
-
72
- const result = runScanPostCheck({ cwd, specDir: spec })
73
- const missing = result.checks.find(c => c.name === 'knowledge_dir_missing')
74
- assert(!!missing, 'knowledge_dir_missing check exists')
75
- assert(missing.detail.includes('knowledge/ 目录不存在'), 'detail text correct')
76
- } finally { clean(cwd, spec) }
77
- }
78
-
79
- // ── 3: INDEX.md 不存在 → index_missing 警告 ──
80
- console.log('\n=== Test 3: INDEX.md 不存在 → knowledge_index_missing ===')
81
- {
82
- const cwd = setup('t3'), spec = specSetup('t3')
83
- try {
84
- writeFull(cwd, spec)
85
- const knowledgeDir = join(spec, 'knowledge')
86
- mkdirSync(knowledgeDir, { recursive: true })
87
- // INDEX.md not created
88
-
89
- const result = runScanPostCheck({ cwd, specDir: spec })
90
- const missing = result.checks.find(c => c.name === 'knowledge_index_missing')
91
- assert(!!missing, 'knowledge_index_missing check exists')
92
- assert(missing.detail.includes('INDEX.md 不存在'), 'detail text correct')
93
- } finally { clean(cwd, spec) }
94
- }
95
-
96
- // ── 4: INDEX.md 引用不存在文件 → broken_refs 警告 ──
97
- console.log('\n=== Test 4: INDEX.md 引用不存在的文件 → knowledge_broken_refs ===')
98
- {
99
- const cwd = setup('t4'), spec = specSetup('t4')
100
- try {
101
- writeFull(cwd, spec)
102
- const knowledgeDir = join(spec, 'knowledge')
103
- mkdirSync(knowledgeDir, { recursive: true })
104
- writeFileSync(join(knowledgeDir, 'INDEX.md'),
105
- '# Knowledge Index\n\n## Conventions\n- [naming](conventions.md#naming)\n\n## Patterns\n- [auth pattern](patterns.md#auth-pattern)\n')
106
- // conventions.md exists, patterns.md does not
107
- writeFileSync(join(knowledgeDir, 'conventions.md'), '# Conventions\n')
108
-
109
- const result = runScanPostCheck({ cwd, specDir: spec })
110
- const broken = result.checks.find(c => c.name === 'knowledge_broken_refs')
111
- assert(!!broken, 'knowledge_broken_refs check exists')
112
- assert(broken.detail.includes('patterns.md'), 'reports missing patterns.md')
113
- } finally { clean(cwd, spec) }
114
- }
115
-
116
- // ── 5: uncategorized.md 为空但存在 → 通过 ──
117
- console.log('\n=== Test 5: uncategorized.md 空但存在 → no knowledge warning ===')
118
- {
119
- const cwd = setup('t5'), spec = specSetup('t5')
120
- try {
121
- writeFull(cwd, spec)
122
- const knowledgeDir = join(spec, 'knowledge')
123
- mkdirSync(knowledgeDir, { recursive: true })
124
- writeFileSync(join(knowledgeDir, 'INDEX.md'),
125
- '# Knowledge Index\n\n(no entries yet)\n')
126
- writeFileSync(join(knowledgeDir, 'uncategorized.md'), '# Uncategorized\n')
127
-
128
- const result = runScanPostCheck({ cwd, specDir: spec })
129
- const knowledgeChecks = result.checks.filter(c => c.name.startsWith('knowledge_'))
130
- assert(knowledgeChecks.length === 0, 'empty uncategorized.md passes')
131
- } finally { clean(cwd, spec) }
132
- }
133
-
134
- // ── 6: categorized 文件存在但 INDEX.md 无条目 → 通过(不强求 categorized 非空)──
135
- console.log('\n=== Test 6: categorized 文件存在 + INDEX.md 有效 → no knowledge warning ===')
136
- {
137
- const cwd = setup('t6'), spec = specSetup('t6')
138
- try {
139
- writeFull(cwd, spec)
140
- const knowledgeDir = join(spec, 'knowledge')
141
- mkdirSync(knowledgeDir, { recursive: true })
142
- writeFileSync(join(knowledgeDir, 'INDEX.md'),
143
- '# Knowledge Index\n\n## Known Issues\n- [GLM proxy](known-issues.md#glm-proxy)\n')
144
- writeFileSync(join(knowledgeDir, 'known-issues.md'),
145
- '# Known Issues\n\n## glm-proxy\nGLM proxy has usage metadata limits.\n')
146
- writeFileSync(join(knowledgeDir, 'uncategorized.md'), '# Uncategorized\n')
147
-
148
- const result = runScanPostCheck({ cwd, specDir: spec })
149
- const knowledgeChecks = result.checks.filter(c => c.name.startsWith('knowledge_'))
150
- assert(knowledgeChecks.length === 0, 'at least one categorized file + valid INDEX passes')
151
- } finally { clean(cwd, spec) }
152
- }
153
-
154
- // ── 7: source_root knowledge 泄漏检测(平台模式已有检查)──
155
- console.log('\n=== Test 7: source_root knowledge 泄漏 → source_root_leak ===')
156
- {
157
- const cwd = setup('t7'), spec = specSetup('t7')
158
- try {
159
- writeFull(cwd, spec)
160
- // 写入 source_root/.sillyspec/knowledge/ 泄漏
161
- const leakedKnowledgeDir = join(cwd, '.sillyspec', 'knowledge')
162
- mkdirSync(leakedKnowledgeDir, { recursive: true })
163
- writeFileSync(join(leakedKnowledgeDir, 'INDEX.md'), '# leaked\n')
164
-
165
- const result = runScanPostCheck({ cwd, specDir: spec })
166
- const leak = result.checks.find(c => c.name === 'source_root_leak')
167
- assert(!!leak, 'source_root knowledge leak detected')
168
- assert(leak.severity === 'failed', 'leak severity is failed')
169
- } finally { clean(cwd, spec) }
170
- }
171
-
172
- // ── 汇总 ──
173
- console.log(`\n${'='.repeat(40)}`)
174
- console.log(`scan-knowledge tests: ${passed} passed, ${failed} failed, ${passed + failed} total`)
175
- if (failed > 0) process.exit(1)
@@ -1,68 +0,0 @@
1
- /**
2
- * 防回归测试:scan.js 中不允许硬编码 .sillyspec/docs/<project>/ 作为写入路径
3
- * 所有正式文档路径必须使用 {DOCS_ROOT} 占位符
4
- */
5
- import { readFileSync } from 'fs'
6
- import { join, dirname } from 'path'
7
- import { fileURLToPath } from 'url'
8
-
9
- const __dirname = dirname(fileURLToPath(import.meta.url))
10
- const scanPath = join(__dirname, '..', 'src', 'stages', 'scan.js')
11
- const content = readFileSync(scanPath, 'utf8')
12
-
13
- const banned = [
14
- '.sillyspec/docs/<project>/scan/',
15
- '.sillyspec/docs/<project>/modules/',
16
- '.sillyspec/docs/<project>/flows/',
17
- '.sillyspec/docs/<project>/glossary.md',
18
- ]
19
-
20
- const required = [
21
- '{DOCS_ROOT}/scan/',
22
- '{DOCS_ROOT}/modules/',
23
- '{DOCS_ROOT}/flows/',
24
- ]
25
-
26
- let failed = false
27
-
28
- // 禁止硬编码路径
29
- for (const pattern of banned) {
30
- if (content.includes(pattern)) {
31
- console.error(`❌ FAIL: scan.js 仍包含硬编码路径 "${pattern}"`)
32
- failed = true
33
- } else {
34
- console.log(`✅ PASS: 不包含 "${pattern}"`)
35
- }
36
- }
37
-
38
- // 必须包含占位符
39
- for (const pattern of required) {
40
- if (content.includes(pattern)) {
41
- console.log(`✅ PASS: 包含占位符 "${pattern}"`)
42
- } else {
43
- console.error(`❌ FAIL: scan.js 缺少占位符 "${pattern}"`)
44
- failed = true
45
- }
46
- }
47
-
48
- // 禁止硬编码 projects 路径
49
- if (content.includes('.sillyspec/projects/')) {
50
- console.error('❌ FAIL: scan.js 仍包含硬编码 ".sillyspec/projects/"')
51
- failed = true
52
- } else {
53
- console.log('✅ PASS: 不包含 ".sillyspec/projects/"')
54
- }
55
-
56
- if (content.includes('{PROJECTS_ROOT}/')) {
57
- console.log('✅ PASS: 包含占位符 "{PROJECTS_ROOT}/"')
58
- } else {
59
- console.error('❌ FAIL: scan.js 缺少占位符 "{PROJECTS_ROOT}/"')
60
- failed = true
61
- }
62
-
63
- if (failed) {
64
- console.error('\n💥 有测试失败!scan.js 路径占位符可能被回退为硬编码。')
65
- throw new Error("test failed")
66
- } else {
67
- console.log('\n✅ 全部通过 — scan.js 路径占位符防回归测试 OK')
68
- }
@@ -1,85 +0,0 @@
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)