sillyspec 3.20.3 → 3.20.5

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 (225) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +21 -17
  2. package/.claude/skills/sillyspec-auto/SKILL.md +83 -77
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -17
  4. package/.claude/skills/sillyspec-commit/SKILL.md +106 -105
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -44
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +31 -22
  7. package/.claude/skills/sillyspec-execute/SKILL.md +30 -17
  8. package/.claude/skills/sillyspec-explore/SKILL.md +109 -96
  9. package/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  10. package/.claude/skills/sillyspec-plan/SKILL.md +21 -52
  11. package/.claude/skills/sillyspec-propose/SKILL.md +21 -17
  12. package/.claude/skills/sillyspec-quick/SKILL.md +21 -17
  13. package/.claude/skills/sillyspec-resume/SKILL.md +68 -111
  14. package/.claude/skills/sillyspec-scan/SKILL.md +21 -17
  15. package/.claude/skills/sillyspec-state/SKILL.md +54 -54
  16. package/.claude/skills/sillyspec-status/SKILL.md +21 -17
  17. package/.claude/skills/sillyspec-verify/SKILL.md +21 -17
  18. package/.claude/skills/sillyspec-workspace/SKILL.md +157 -149
  19. package/.husky/pre-push +13 -0
  20. package/CLAUDE.md +18 -0
  21. package/README.md +198 -186
  22. package/SKILL.md +90 -87
  23. package/bin/sillyspec.js +2 -2
  24. package/docs/brainstorm-plan-contract.md +64 -0
  25. package/docs/plan-execute-contract.md +123 -0
  26. package/docs/platform-scan-protocol.md +298 -0
  27. package/docs/revision-mode.md +115 -0
  28. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -0
  29. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -0
  30. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -0
  31. package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -0
  32. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -0
  33. package/docs/sillyspec/file-lifecycle.md +131 -0
  34. package/docs/workflow-contract-regression.md +106 -0
  35. package/docs/worktree-isolation.md +252 -0
  36. package/package.json +40 -34
  37. package/packages/dashboard/dist/assets/{index-D1EVTLmc.js → index-Bq_Z2hne.js} +7446 -7446
  38. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  39. package/packages/dashboard/dist/index.html +16 -16
  40. package/packages/dashboard/dist/prototype-dashboard.html +836 -0
  41. package/packages/dashboard/dist/prototype-overview.html +256 -0
  42. package/packages/dashboard/index.html +15 -15
  43. package/packages/dashboard/package-lock.json +2384 -2384
  44. package/packages/dashboard/package.json +25 -25
  45. package/packages/dashboard/public/prototype-dashboard.html +836 -0
  46. package/packages/dashboard/public/prototype-overview.html +256 -0
  47. package/packages/dashboard/server/executor.js +86 -86
  48. package/packages/dashboard/server/index.js +588 -508
  49. package/packages/dashboard/server/parser.js +526 -458
  50. package/packages/dashboard/server/watcher.js +350 -342
  51. package/packages/dashboard/src/App.vue +558 -325
  52. package/packages/dashboard/src/components/ActionBar.vue +93 -84
  53. package/packages/dashboard/src/components/CommandPalette.vue +96 -92
  54. package/packages/dashboard/src/components/DetailPanel.vue +137 -137
  55. package/packages/dashboard/src/components/DocPreview.vue +105 -8
  56. package/packages/dashboard/src/components/DocTree.vue +75 -19
  57. package/packages/dashboard/src/components/HResizeHandle.vue +48 -0
  58. package/packages/dashboard/src/components/LogStream.vue +65 -65
  59. package/packages/dashboard/src/components/PipelineStage.vue +95 -75
  60. package/packages/dashboard/src/components/PipelineView.vue +156 -129
  61. package/packages/dashboard/src/components/ProjectCard.vue +187 -0
  62. package/packages/dashboard/src/components/ProjectList.vue +210 -210
  63. package/packages/dashboard/src/components/ProjectOverview.vue +113 -139
  64. package/packages/dashboard/src/components/StageBadge.vue +67 -53
  65. package/packages/dashboard/src/components/StepCard.vue +94 -89
  66. package/packages/dashboard/src/components/VResizeHandle.vue +61 -0
  67. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
  68. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
  69. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
  70. package/packages/dashboard/src/composables/useDashboard.js +192 -164
  71. package/packages/dashboard/src/composables/useKeyboard.js +119 -119
  72. package/packages/dashboard/src/composables/useLayout.js +131 -0
  73. package/packages/dashboard/src/composables/useWebSocket.js +129 -129
  74. package/packages/dashboard/src/main.js +8 -8
  75. package/packages/dashboard/src/style.css +132 -132
  76. package/packages/dashboard/vite.config.js +18 -18
  77. package/src/brainstorm-postcheck.js +158 -0
  78. package/src/change-list.js +52 -0
  79. package/src/change-risk-profile.js +352 -0
  80. package/src/classify-change.js +73 -0
  81. package/src/constants.js +70 -0
  82. package/src/contract-matrix.js +278 -0
  83. package/src/db.js +201 -0
  84. package/src/endpoint-extractor.js +315 -0
  85. package/src/hooks/claude-pre-tool-use.cjs +125 -0
  86. package/src/hooks/worktree-guard.js +761 -0
  87. package/src/index.js +922 -252
  88. package/src/init.js +470 -375
  89. package/src/knowledge-match.js +130 -0
  90. package/src/migrate.js +117 -117
  91. package/src/modules.js +482 -0
  92. package/src/progress.js +1734 -508
  93. package/src/run.js +3465 -652
  94. package/src/scan-postcheck.js +387 -0
  95. package/src/setup.js +398 -460
  96. package/src/stage-contract.js +700 -0
  97. package/src/stages/archive.js +160 -54
  98. package/src/stages/brainstorm-auto.js +229 -0
  99. package/src/stages/brainstorm.js +645 -239
  100. package/src/stages/doctor.js +365 -312
  101. package/src/stages/execute.js +634 -264
  102. package/src/stages/explore.js +34 -0
  103. package/src/stages/index.js +29 -35
  104. package/src/stages/knowledge.js +498 -0
  105. package/src/stages/plan-postcheck.js +518 -0
  106. package/src/stages/plan.js +582 -279
  107. package/src/stages/propose.js +174 -115
  108. package/src/stages/quick.js +102 -63
  109. package/src/stages/scan.js +558 -141
  110. package/src/stages/status.js +65 -65
  111. package/src/stages/verify.js +322 -135
  112. package/src/sync.js +497 -0
  113. package/src/task-review.js +346 -0
  114. package/src/workflow.js +785 -0
  115. package/src/worktree-apply.js +549 -0
  116. package/src/worktree-deps.js +185 -0
  117. package/src/worktree.js +982 -0
  118. package/templates/workflows/archive-impact.yaml +79 -0
  119. package/templates/workflows/scan-docs.yaml +132 -0
  120. package/test/brainstorm-plan-contract.test.mjs +273 -0
  121. package/test/check-syntax.mjs +26 -0
  122. package/test/cli-top-level-aliases.test.mjs +174 -0
  123. package/test/contract-artifacts.test.mjs +323 -0
  124. package/test/decision-supersede.test.mjs +277 -0
  125. package/test/knowledge-match.test.mjs +231 -0
  126. package/test/plan-execute-contract.test.mjs +357 -0
  127. package/test/plan-optimization.test.mjs +572 -0
  128. package/test/platform-artifacts.test.mjs +190 -0
  129. package/test/platform-failure-samples.test.mjs +199 -0
  130. package/test/platform-recovery-chain.test.mjs +179 -0
  131. package/test/platform-recovery.test.mjs +167 -0
  132. package/test/platform-scan-p0.test.mjs +175 -0
  133. package/test/revision-v1.test.mjs +1145 -0
  134. package/test/run-sanitize-project-name.test.mjs +51 -0
  135. package/test/run-scan-postcheck-fail.test.mjs +64 -0
  136. package/test/run-scan-project-parse.test.mjs +200 -0
  137. package/test/run-tests.mjs +48 -0
  138. package/test/runtime-cleanup-keeps-worktree.test.mjs +107 -0
  139. package/test/scan-docs-yaml-placeholders.test.mjs +84 -0
  140. package/test/scan-knowledge.test.mjs +175 -0
  141. package/test/scan-paths.test.mjs +68 -0
  142. package/test/scan-postcheck-project-priority.test.mjs +85 -0
  143. package/test/scan-postcheck.test.mjs +197 -0
  144. package/test/scan-workflow-anyfailed-block.test.mjs +52 -0
  145. package/test/spec-dir.test.mjs +206 -0
  146. package/test/stage-contract-failed-post-check.test.mjs +102 -0
  147. package/test/stage-contract.test.mjs +299 -0
  148. package/test/stage-definitions.test.mjs +39 -0
  149. package/test/wait-gates.test.mjs +501 -0
  150. package/test/workflow-spec-base.test.mjs +142 -0
  151. package/test/worktree-deps-provision.test.mjs +148 -0
  152. package/test/worktree-guard.test.mjs +136 -0
  153. package/test/worktree-native-overlay.test.mjs +188 -0
  154. package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +0 -97
  155. package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +0 -51
  156. package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +0 -29
  157. package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +0 -34
  158. package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +0 -13
  159. package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +0 -43
  160. package/.sillyspec/changes/auto-mode/design.md +0 -50
  161. package/.sillyspec/changes/auto-mode/proposal.md +0 -19
  162. package/.sillyspec/changes/auto-mode/requirements.md +0 -21
  163. package/.sillyspec/changes/auto-mode/tasks.md +0 -7
  164. package/.sillyspec/changes/brainstorm-archive/2026-04-05-dashboard-design.md +0 -206
  165. package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +0 -199
  166. package/.sillyspec/changes/dashboard/design.md +0 -219
  167. package/.sillyspec/changes/dashboard/design.md.braindraft +0 -206
  168. package/.sillyspec/changes/run-command-design/design.md +0 -1230
  169. package/.sillyspec/changes/unified-docs-design/design.md +0 -199
  170. package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
  171. package/.sillyspec/knowledge/INDEX.md +0 -8
  172. package/.sillyspec/knowledge/uncategorized.md +0 -3
  173. package/.sillyspec/plans/2026-04-05-dashboard.md +0 -737
  174. package/.sillyspec/projects/sillyspec.yaml +0 -3
  175. package/dist/steps/brainstorm/01-load-context.md +0 -30
  176. package/dist/steps/brainstorm/02-reuse-check.md +0 -6
  177. package/dist/steps/brainstorm/03-prototype-analysis.md +0 -11
  178. package/dist/steps/brainstorm/04-module-split.md +0 -23
  179. package/dist/steps/brainstorm/05-dialog-explore.md +0 -8
  180. package/dist/steps/brainstorm/06-propose-approaches.md +0 -3
  181. package/dist/steps/brainstorm/07-present-design.md +0 -3
  182. package/dist/steps/brainstorm/08-write-design.md +0 -21
  183. package/dist/steps/brainstorm/09-self-review.md +0 -15
  184. package/dist/steps/brainstorm/10-user-confirm.md +0 -3
  185. package/dist/steps/brainstorm/11-output-spec.md +0 -7
  186. package/dist/steps/brainstorm/manifest.yaml +0 -26
  187. package/dist/steps/execute/01-load-context.md +0 -41
  188. package/dist/steps/execute/02-scan-conventions.md +0 -47
  189. package/dist/steps/execute/03-skill-mcp.md +0 -19
  190. package/dist/steps/execute/04-assign-task.md +0 -22
  191. package/dist/steps/execute/04b-prompt-template.md +0 -54
  192. package/dist/steps/execute/05-write-test.md +0 -7
  193. package/dist/steps/execute/06-write-code.md +0 -8
  194. package/dist/steps/execute/07-run-test.md +0 -26
  195. package/dist/steps/execute/08-fix-issues.md +0 -28
  196. package/dist/steps/execute/09-next-task.md +0 -33
  197. package/dist/steps/execute/manifest.yaml +0 -28
  198. package/dist/steps/plan/01-load-context.md +0 -22
  199. package/dist/steps/plan/02-anchor-confirm.md +0 -1
  200. package/dist/steps/plan/03-expand-tasks.md +0 -33
  201. package/dist/steps/plan/04-mark-order.md +0 -15
  202. package/dist/steps/plan/05-e2e-planning.md +0 -17
  203. package/dist/steps/plan/06-self-check.md +0 -16
  204. package/dist/steps/plan/07-save.md +0 -1
  205. package/dist/steps/plan/manifest.yaml +0 -18
  206. package/dist/steps/scan/01-env-detect.md +0 -51
  207. package/dist/steps/scan/02-tech-stack.md +0 -16
  208. package/dist/steps/scan/03-conventions.md +0 -16
  209. package/dist/steps/scan/04-structure.md +0 -19
  210. package/dist/steps/scan/05-quality.md +0 -18
  211. package/dist/steps/scan/06-complete.md +0 -49
  212. package/dist/steps/scan/manifest.yaml +0 -16
  213. package/dist/steps/verify/01-load-specs.md +0 -28
  214. package/dist/steps/verify/02-check-tasks.md +0 -1
  215. package/dist/steps/verify/03-check-design.md +0 -6
  216. package/dist/steps/verify/04-run-tests.md +0 -7
  217. package/dist/steps/verify/05-e2e-tests.md +0 -27
  218. package/dist/steps/verify/05b-e2e-fix.md +0 -33
  219. package/dist/steps/verify/06-code-quality.md +0 -25
  220. package/dist/steps/verify/07-lint-check.md +0 -27
  221. package/dist/steps/verify/08-output-report.md +0 -14
  222. package/dist/steps/verify/manifest.yaml +0 -22
  223. package/packages/dashboard/dist/assets/index-DGe8CqeP.css +0 -1
  224. package/src/derive.js +0 -147
  225. package/src/step.js +0 -543
@@ -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,200 @@
1
+ /**
2
+ * scan step 2 项目列表解析 — 强契约测试
3
+ *
4
+ * 解析只接受两种结构化格式:
5
+ * A) YAML block: scan_projects:\n - id: name
6
+ * B) BEGIN_PROJECT_LIST ... END_PROJECT_LIST
7
+ *
8
+ * 自由文本列表不再解析,防止误识别垃圾项目名。
9
+ */
10
+ import { sanitizeProjectName, validateParsedProjects } 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
+ function assertEqual (actual, expected, msg) {
21
+ if (actual === expected) { console.log(`✅ PASS: ${msg}`); passed++ }
22
+ else { console.error(`❌ FAIL: ${msg}\n expected: ${expected}\n actual: ${actual}`); failed++ }
23
+ }
24
+ function assertIncludes (str, sub, msg) {
25
+ if (str.includes(sub)) { console.log(`✅ PASS: ${msg}`); passed++ }
26
+ else { console.error(`❌ FAIL: ${msg}\n expected to include: ${sub}\n actual: ${str}`); failed++ }
27
+ }
28
+
29
+ // ---- 复刻 run.js 中新的解析逻辑 ----
30
+ function parseProjectListFromOutput(outputText) {
31
+ let parsedProjects = [] // Array<{id, path?}>
32
+ let parsedFromStructuredOutput = false
33
+ if (outputText) {
34
+ // 格式 A: YAML block
35
+ const yamlBlock = outputText.match(/scan_projects:\s*\n([\s\S]+?)(?=$|\n[^\s])/)
36
+ if (yamlBlock) {
37
+ const entries = [...yamlBlock[1].matchAll(/-\s+id:\s*(\S+)(?:[\s\S]*?)(?=\n\s+-\s+id:|$)/g)]
38
+ for (const m of entries) {
39
+ const id = sanitizeProjectName(m[1])
40
+ if (!id) continue
41
+ const pathMatch = m[0].match(/path:\s*(\S+)/)
42
+ const entry = pathMatch ? { id, path: pathMatch[1].trim() } : { id }
43
+ parsedProjects.push(entry)
44
+ }
45
+ parsedFromStructuredOutput = parsedProjects.length > 0
46
+ }
47
+ // 格式 B: BEGIN_PROJECT_LIST
48
+ if (!parsedFromStructuredOutput) {
49
+ const blockMatch = outputText.match(/BEGIN_PROJECT_LIST\s*\n([\s\S]*?)\n*END_PROJECT_LIST/)
50
+ if (blockMatch) {
51
+ const raw = [...blockMatch[1].matchAll(/^-\s+(\S+)/gm)].map(m => m[1])
52
+ parsedProjects = raw.map(s => sanitizeProjectName(s)).filter(Boolean).map(id => ({ id }))
53
+ parsedFromStructuredOutput = parsedProjects.length > 0
54
+ }
55
+ }
56
+ }
57
+ return { parsedProjects, parsedFromStructuredOutput }
58
+ }
59
+
60
+ // ======== 解析测试 ========
61
+
62
+ // 1. YAML block 正常解析
63
+ assertDeepEqual(
64
+ parseProjectListFromOutput(`scan_projects:
65
+ - id: backend
66
+ - id: frontend
67
+ - id: daemon
68
+ `).parsedProjects,
69
+ [{ id: 'backend' }, { id: 'frontend' }, { id: 'daemon' }],
70
+ 'YAML block 正常解析 3 个项目'
71
+ )
72
+
73
+ // 2. YAML block 带 path 字段(多行属性)
74
+ const r2 = parseProjectListFromOutput(`scan_projects:
75
+ - id: api
76
+ path: backend/
77
+ - id: web
78
+ path: frontend/
79
+ `).parsedProjects
80
+ assertDeepEqual(r2, [{ id: 'api', path: 'backend/' }, { id: 'web', path: 'frontend/' }],
81
+ 'YAML block 带 path 字段正常解析')
82
+
83
+ // 3. BEGIN_PROJECT_LIST 正常解析
84
+ assertDeepEqual(
85
+ parseProjectListFromOutput(`BEGIN_PROJECT_LIST
86
+ - backend
87
+ - frontend
88
+ - daemon
89
+ END_PROJECT_LIST`).parsedProjects,
90
+ [{ id: 'backend' }, { id: 'frontend' }, { id: 'daemon' }],
91
+ 'BEGIN_PROJECT_LIST 标记块正常解析'
92
+ )
93
+
94
+ // 4. 自由文本编号列表 → 不解析
95
+ const { parsedProjects: fn, parsedFromStructuredOutput: fnParsed } =
96
+ parseProjectListFromOutput('扫描项目列表:\n1. scan1backendmulti-agent-platform-api FastAPI Python\n2. frontendmulti-agent-platform-web Next.js TS\n3. sillyhub-daemon Node.js TS')
97
+ assertEqual(fnParsed, false, '自由文本编号列表不触发解析')
98
+ assertDeepEqual(fn, [], '自由文本编号列表不产生任何项目名')
99
+
100
+ // 5. 自由文本括号枚举 → 不解析
101
+ const { parsedProjects: fn2, parsedFromStructuredOutput: fn2Parsed } =
102
+ parseProjectListFromOutput('子项目: backend / frontend / user-service')
103
+ assertEqual(fn2Parsed, false, '自由文本括号枚举不触发解析')
104
+ assertDeepEqual(fn2, [], '自由文本括号枚举不产生任何项目名')
105
+
106
+ // 6. 空 outputText → 不解析
107
+ const { parsedFromStructuredOutput: emptyParsed } = parseProjectListFromOutput('')
108
+ assertEqual(emptyParsed, false, '空 outputText 不触发解析')
109
+
110
+ // 7. 普通摘要文本 → 不解析
111
+ const { parsedProjects: summary, parsedFromStructuredOutput: summaryParsed } =
112
+ parseProjectListFromOutput('确认扫描 3 个子项目:backend、frontend、daemon,全部重新扫描')
113
+ assertEqual(summaryParsed, false, '普通摘要文本不触发解析')
114
+ assertDeepEqual(summary, [], '普通摘要文本不产生任何项目名')
115
+
116
+ // ======== validateParsedProjects — 基础校验 ========
117
+
118
+ // 8. 正常列表(无 path)
119
+ assertDeepEqual(
120
+ validateParsedProjects([{ id: 'backend' }, { id: 'frontend' }, { id: 'daemon' }], '/tmp/proj'),
121
+ { ok: true, errors: [] },
122
+ '正常列表校验通过(无 path)'
123
+ )
124
+
125
+ // 9. 正常列表(有合法 path)——需要用真实路径
126
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
127
+ import { join } from 'path'
128
+ import os from 'os'
129
+ const tmpDir = mkdtempSync(join(os.tmpdir(), 'sillyspec-test-'))
130
+ mkdirSync(join(tmpDir, 'backend'))
131
+ mkdirSync(join(tmpDir, 'frontend'))
132
+ assertDeepEqual(
133
+ validateParsedProjects([
134
+ { id: 'backend', path: 'backend' },
135
+ { id: 'frontend', path: 'frontend' }
136
+ ], tmpDir),
137
+ { ok: true, errors: [] },
138
+ '合法 path 校验通过'
139
+ )
140
+
141
+ // 10. path 不存在 → 失败
142
+ const v10 = validateParsedProjects([{ id: 'backend', path: 'nonexistent' }], tmpDir)
143
+ assertEqual(v10.ok, false, 'path 不存在校验失败')
144
+ assertIncludes(v10.errors[0], '不存在', '错误提示包含"不存在"')
145
+
146
+ // 11. path 含 .. 越界 → 失败
147
+ const v11 = validateParsedProjects([{ id: 'backend', path: '../etc/passwd' }], tmpDir)
148
+ assertEqual(v11.ok, false, 'path 包含 .. 校验失败')
149
+ assertIncludes(v11.errors[0], '..', '错误提示包含 ..')
150
+
151
+ // 12. path 解析后超出 source_root → 失败
152
+ const v12 = validateParsedProjects([{ id: 'backend', path: '/etc/passwd' }], tmpDir)
153
+ assertEqual(v12.ok, false, '绝对路径超出 source_root 校验失败')
154
+ assertIncludes(v12.errors[0], '超出', '错误提示包含"超出"')
155
+
156
+ // 13. 超过 10 个项目 → 失败
157
+ const many = Array.from({ length: 11 }, (_, i) => ({ id: `project${i}` }))
158
+ const v13 = validateParsedProjects(many, tmpDir)
159
+ assertEqual(v13.ok, false, '超过 10 个项目校验失败')
160
+ assertIncludes(v13.errors[0], '超过上限', '错误提示包含"超过上限"')
161
+
162
+ // 14. 重复项目名 → 失败
163
+ const v14 = validateParsedProjects([{ id: 'backend' }, { id: 'frontend' }, { id: 'backend' }], tmpDir)
164
+ assertEqual(v14.ok, false, '重复项目名校验失败')
165
+ assertIncludes(v14.errors[0], '重复', '错误提示包含"重复"')
166
+
167
+ // 15. 非法 slug(中文)→ 失败
168
+ const v15 = validateParsedProjects([{ id: 'backend' }, { id: '前端服务' }], tmpDir)
169
+ assertEqual(v15.ok, false, '非法 slug 校验失败')
170
+
171
+ // 16. 空列表 → 失败
172
+ const v16 = validateParsedProjects([], tmpDir)
173
+ assertEqual(v16.ok, false, '空列表校验失败')
174
+
175
+ // 17. null 列表 → 失败
176
+ const v17 = validateParsedProjects(null, tmpDir)
177
+ assertEqual(v17.ok, false, 'null 列表校验失败')
178
+
179
+ // 18. 单字符 → 失败
180
+ const v18 = validateParsedProjects([{ id: 'a' }], tmpDir)
181
+ assertEqual(v18.ok, false, '单字符项目名校验失败')
182
+
183
+ // 19. 纯数字 → 失败
184
+ const v19 = validateParsedProjects([{ id: '123' }], tmpDir)
185
+ assertEqual(v19.ok, false, '纯数字项目名校验失败')
186
+
187
+ // 20. 兼容旧 API(纯字符串数组)
188
+ assertDeepEqual(
189
+ validateParsedProjects(['backend', 'frontend'], tmpDir),
190
+ { ok: true, errors: [] },
191
+ '旧 API 兼容:纯字符串数组仍通过'
192
+ )
193
+
194
+ // 清理
195
+ rmSync(tmpDir, { recursive: true, force: true })
196
+
197
+ console.log(`\n${'='.repeat(50)}`)
198
+ console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
199
+ console.log(`${'='.repeat(50)}`)
200
+ if (failed > 0) process.exit(1)
@@ -0,0 +1,48 @@
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)
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Bug A 回归:清理逻辑不得摧毁 worktree meta / 进度状态
3
+ *
4
+ * 平台模式(specRoot 指向外部)下,源码目录 .sillyspec/ 含真实资产时,
5
+ * cleanupRuntimeResidue 应只清缓存,保留 worktrees/、sillyspec.db、
6
+ * global.json、gate-status.json、contract-artifacts/、execute-runs/。
7
+ * 详见 docs/sillyspec/runtime-cleanup-destroys-worktree-meta.md
8
+ */
9
+ import { cleanupRuntimeResidue } from '../src/init.js'
10
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs'
11
+ import { join } from 'path'
12
+ import os from 'os'
13
+
14
+ let failed = 0
15
+ const failures = []
16
+ function assert(condition, msg) {
17
+ if (!condition) { failed++; failures.push(msg); console.log(` ❌ FAIL: ${msg}`) }
18
+ else console.log(` ✅ PASS: ${msg}`)
19
+ }
20
+
21
+ function makeTempLegacy() {
22
+ const root = join(os.tmpdir(), `sillyspec-bugA-${Date.now()}-${Math.random().toString(36).slice(2)}`)
23
+ const legacyDir = join(root, '.sillyspec')
24
+ mkdirSync(join(legacyDir, '.runtime'), { recursive: true })
25
+ return { root, legacyDir }
26
+ }
27
+
28
+ console.log('=== Bug A 回归: cleanupRuntimeResidue 保留权威状态 ===\n')
29
+
30
+ // ── Case 1: 权威状态全部保留 ──
31
+ console.log('--- Case 1: worktrees / db / global.json / gate-status 保留 ---')
32
+ {
33
+ const { root, legacyDir } = makeTempLegacy()
34
+ const runtime = join(legacyDir, '.runtime')
35
+ // 权威状态
36
+ mkdirSync(join(runtime, 'worktrees', 'my-change'), { recursive: true })
37
+ writeFileSync(join(runtime, 'worktrees', 'my-change', 'meta.json'), '{"depsStatus":"installed"}')
38
+ writeFileSync(join(runtime, 'sillyspec.db'), 'sqlite-bytes')
39
+ writeFileSync(join(runtime, 'global.json'), '{"projectName":"x"}')
40
+ writeFileSync(join(runtime, 'gate-status.json'), '{}')
41
+ mkdirSync(join(runtime, 'contract-artifacts'), { recursive: true })
42
+ writeFileSync(join(runtime, 'contract-artifacts', 'foo.json'), '{}')
43
+ mkdirSync(join(runtime, 'execute-runs'), { recursive: true })
44
+ // 可删缓存
45
+ mkdirSync(join(runtime, 'artifacts'), { recursive: true })
46
+ mkdirSync(join(runtime, 'scan-runs'), { recursive: true })
47
+ writeFileSync(join(runtime, 'scan-projects.json'), '{}')
48
+ writeFileSync(join(runtime, 'user-inputs.md'), 'x')
49
+ writeFileSync(join(runtime, 'postcheck-result.json'), '{}')
50
+ // local.yaml / codebase(非权威,应删)
51
+ writeFileSync(join(legacyDir, 'local.yaml'), 'x')
52
+ mkdirSync(join(legacyDir, 'codebase'), { recursive: true })
53
+
54
+ cleanupRuntimeResidue(legacyDir)
55
+
56
+ assert(existsSync(join(runtime, 'worktrees', 'my-change', 'meta.json')), 'worktrees/meta.json 应保留')
57
+ assert(existsSync(join(runtime, 'sillyspec.db')), 'sillyspec.db 应保留')
58
+ assert(existsSync(join(runtime, 'global.json')), 'global.json 应保留')
59
+ assert(existsSync(join(runtime, 'gate-status.json')), 'gate-status.json 应保留')
60
+ assert(existsSync(join(runtime, 'contract-artifacts', 'foo.json')), 'contract-artifacts/ 应保留')
61
+ assert(existsSync(join(runtime, 'execute-runs')), 'execute-runs/ 应保留')
62
+ assert(!existsSync(join(runtime, 'artifacts')), 'artifacts/ 缓存应删除')
63
+ assert(!existsSync(join(runtime, 'scan-runs')), 'scan-runs/ 缓存应删除')
64
+ assert(!existsSync(join(runtime, 'scan-projects.json')), 'scan-projects.json 缓存应删除')
65
+ assert(!existsSync(join(runtime, 'user-inputs.md')), 'user-inputs.md 缓存应删除')
66
+ assert(!existsSync(join(legacyDir, 'local.yaml')), 'local.yaml 应删除')
67
+ assert(!existsSync(join(legacyDir, 'codebase')), 'codebase/ 应删除')
68
+
69
+ rmSync(root, { recursive: true, force: true })
70
+ }
71
+
72
+ // ── Case 2: worktree meta 内容不被破坏(depsStatus 不丢) ──
73
+ console.log('\n--- Case 2: 清理后 worktree meta 内容完整 ---')
74
+ {
75
+ const { root, legacyDir } = makeTempLegacy()
76
+ const runtime = join(legacyDir, '.runtime')
77
+ mkdirSync(join(runtime, 'worktrees', 'c1'), { recursive: true })
78
+ const metaPath = join(runtime, 'worktrees', 'c1', 'meta.json')
79
+ const original = '{"depsStatus":"installed","worktreePath":"/x","branch":"sillyspec/c1"}'
80
+ writeFileSync(metaPath, original)
81
+
82
+ cleanupRuntimeResidue(legacyDir)
83
+
84
+ assert(existsSync(metaPath), 'meta.json 应仍存在')
85
+ const read = readFileSync(metaPath, 'utf8')
86
+ assert(read === original, 'meta.json 内容应未被改写')
87
+
88
+ rmSync(root, { recursive: true, force: true })
89
+ }
90
+
91
+ // ── Case 3: .runtime/ 不存在时不报错 ──
92
+ console.log('\n--- Case 3: 无 .runtime/ 时安全无副作用 ---')
93
+ {
94
+ const { root, legacyDir } = makeTempLegacy()
95
+ rmSync(join(legacyDir, '.runtime'), { recursive: true, force: true })
96
+ let threw = false
97
+ try { cleanupRuntimeResidue(legacyDir) } catch { threw = true }
98
+ assert(!threw, '无 .runtime/ 时不应抛错')
99
+ rmSync(root, { recursive: true, force: true })
100
+ }
101
+
102
+ // ── 结果 ──
103
+ console.log(`\n${'='.repeat(50)}`)
104
+ console.log(`✅ 通过: ${28 - failed} ❌ 失败: ${failed}`)
105
+ if (failures.length > 0) { console.log('失败项:'); failures.forEach(f => console.log(` - ${f}`)) }
106
+ console.log('='.repeat(50))
107
+ 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)