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,148 @@
1
+ /**
2
+ * worktree-deps-provision 测试
3
+ * 覆盖 FR-01/02/03/05:lockfileHash + provisionDeps(junction 快路径 / install 兜底判定 / generic→n/a)
4
+ * change 2026-06-28-worktree-deps-provision
5
+ */
6
+ import { provisionDeps, lockfileHash } from '../src/worktree-deps.js'
7
+ import { mkdtempSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'fs'
8
+ import { join } from 'path'
9
+ import { tmpdir } from 'os'
10
+
11
+ let failed = 0
12
+ let passed = 0
13
+ const failures = []
14
+ function assert(cond, msg) {
15
+ if (cond) { passed++; console.log(` ✅ PASS: ${msg}`) }
16
+ else { failed++; failures.push(msg); console.log(` ❌ FAIL: ${msg}`) }
17
+ }
18
+
19
+ const tmpDirs = []
20
+ function mkTmp(prefix) {
21
+ const d = mkdtempSync(join(tmpdir(), `wtdeps-${prefix}-`))
22
+ tmpDirs.push(d)
23
+ return d
24
+ }
25
+ function cleanup() {
26
+ for (const d of tmpDirs) {
27
+ try { rmSync(d, { recursive: true, force: true }) } catch {}
28
+ }
29
+ }
30
+
31
+ console.log('=== worktree-deps-provision 测试 ===\n')
32
+
33
+ // ── lockfileHash ──
34
+ console.log('--- lockfileHash ---')
35
+ {
36
+ const d = mkTmp('lh')
37
+ assert(lockfileHash(d) === null, '空目录返回 null')
38
+ writeFileSync(join(d, 'pnpm-lock.yaml'), 'lockversion: 6.0\npackages: []\n')
39
+ const h1 = lockfileHash(d)
40
+ assert(typeof h1 === 'string' && h1.length === 16, `pnpm-lock 返回 16 位 hash: ${h1}`)
41
+
42
+ const d2 = mkTmp('lh2')
43
+ writeFileSync(join(d2, 'package-lock.json'), '{"lockfileVersion":3}')
44
+ const h2 = lockfileHash(d2)
45
+ assert(typeof h2 === 'string' && h2.length === 16, `package-lock 返回 hash: ${h2}`)
46
+ assert(h1 !== h2, '不同 lockfile 内容产生不同 hash')
47
+
48
+ const d3 = mkTmp('lh3')
49
+ writeFileSync(join(d3, 'yarn.lock'), '# yarn lock v1\n')
50
+ assert(typeof lockfileHash(d3) === 'string', 'yarn.lock 返回 hash')
51
+
52
+ const d4 = mkTmp('lh4')
53
+ writeFileSync(join(d4, 'package.json'), '{"name":"x"}')
54
+ assert(typeof lockfileHash(d4) === 'string', '无 lockfile 时回退 hash package.json')
55
+
56
+ // 一致性:相同内容相同 hash
57
+ const d5 = mkTmp('lh5')
58
+ writeFileSync(join(d5, 'pnpm-lock.yaml'), 'lockversion: 6.0\npackages: []\n')
59
+ assert(lockfileHash(d5) === h1, '相同 lockfile 内容产生相同 hash(快路径判定基础)')
60
+ }
61
+
62
+ // ── provisionDeps: generic → n/a ──
63
+ console.log('\n--- provisionDeps: generic / 无依赖 → n/a ---')
64
+ {
65
+ const wt = mkTmp('gen-wt')
66
+ const main = mkTmp('gen-main')
67
+ const r = provisionDeps(wt, main, {})
68
+ assert(r.depsStatus === 'n/a', `无 package.json/lockfile 的目录 → n/a(实际 ${r.depsStatus})`)
69
+ assert(!existsSync(join(wt, 'node_modules')), 'n/a 不创建 node_modules')
70
+ }
71
+
72
+ // ── provisionDeps: nodejs junction 快路径(lockfile 一致)──
73
+ console.log('\n--- provisionDeps: nodejs lockfile 一致 → linked (junction/symlink) ---')
74
+ {
75
+ const main = mkTmp('link-main')
76
+ const wt = mkTmp('link-wt')
77
+ // 主 checkout:有 node_modules + lockfile
78
+ mkdirSync(join(main, 'node_modules'))
79
+ writeFileSync(join(main, 'node_modules', '.placeholder'), 'x')
80
+ const lockContent = 'lockversion: 6.0\npackages: []\n'
81
+ writeFileSync(join(main, 'package-lock.json'), lockContent)
82
+ writeFileSync(join(main, 'package.json'), '{"name":"main","scripts":{}}')
83
+ // worktree:相同 lockfile(hash 一致)+ package.json
84
+ writeFileSync(join(wt, 'package-lock.json'), lockContent)
85
+ writeFileSync(join(wt, 'package.json'), '{"name":"wt","scripts":{}}')
86
+
87
+ const r = provisionDeps(wt, main, {})
88
+ assert(r.depsStatus === 'linked', `lockfile 一致 → linked(实际 ${r.depsStatus})`)
89
+ assert(['junction', 'symlink', 'install'].includes(r.depsMethod), `depsMethod 合法(${r.depsMethod})`)
90
+ assert(r.depsSource === 'main-checkout', `depsSource=main-checkout(${r.depsSource})`)
91
+ assert(existsSync(join(wt, 'node_modules')), 'worktree/node_modules 已创建(junction/symlink)')
92
+ assert(existsSync(join(wt, 'node_modules', '.placeholder')), 'junction 指向 main 的内容可访问')
93
+ assert(typeof r.depsLockHash === 'string', 'depsLockHash 已记录')
94
+ }
95
+
96
+ // ── provisionDeps: lockfile 不一致 → 走 install 兜底(不验证真实 install,只验证不走 linked)──
97
+ console.log('\n--- provisionDeps: lockfile 不一致 → 不走 linked 快路径 ---')
98
+ {
99
+ const main = mkTmp('mis-main')
100
+ const wt = mkTmp('mis-wt')
101
+ mkdirSync(join(main, 'node_modules'))
102
+ writeFileSync(join(main, 'package-lock.json'), 'lockversion-A')
103
+ writeFileSync(join(main, 'package.json'), '{"name":"main"}')
104
+ writeFileSync(join(wt, 'package-lock.json'), 'lockversion-B-DIFFERENT') // 不一致
105
+ writeFileSync(join(wt, 'package.json'), '{"name":"wt"}')
106
+
107
+ const r = provisionDeps(wt, main, {})
108
+ // lockfile 不一致 → 不会 linked;会尝试 install(真实 install 在 CI 可能失败/超时)
109
+ assert(r.depsStatus !== 'linked', `lockfile 不一致时不应 linked(实际 ${r.depsStatus})`)
110
+ assert(['installed', 'failed'].includes(r.depsStatus), `兜底为 installed 或 failed(实际 ${r.depsStatus})`)
111
+ // install 失败时应带 depsError
112
+ if (r.depsStatus === 'failed') {
113
+ assert(typeof r.depsError === 'string' && r.depsError.length > 0, 'failed 带 depsError 信息')
114
+ }
115
+ }
116
+
117
+ // ── provisionDeps: main 无 node_modules → 不走 linked ──
118
+ console.log('\n--- provisionDeps: main 无 node_modules → 不走 linked ---')
119
+ {
120
+ const main = mkTmp('nomain-main')
121
+ const wt = mkTmp('nomain-wt')
122
+ const lockContent = 'same-lock\n'
123
+ writeFileSync(join(main, 'package-lock.json'), lockContent)
124
+ writeFileSync(join(main, 'package.json'), '{"name":"main"}')
125
+ writeFileSync(join(wt, 'package-lock.json'), lockContent)
126
+ writeFileSync(join(wt, 'package.json'), '{"name":"wt"}')
127
+ // main 无 node_modules
128
+
129
+ const r = provisionDeps(wt, main, {})
130
+ assert(r.depsStatus !== 'linked', `main 无 node_modules 时不应 linked(实际 ${r.depsStatus})`)
131
+ }
132
+
133
+ // ── provisionDeps: maven/gradle 推断(无 node_modules,验证不崩)──
134
+ console.log('\n--- provisionDeps: 非 nodejs 项目类型不崩 ---')
135
+ {
136
+ const wt = mkTmp('maven-wt')
137
+ writeFileSync(join(wt, 'pom.xml'), '<project></project>')
138
+ const r = provisionDeps(wt, mkTmp('maven-main'), {})
139
+ assert(['installed', 'failed'].includes(r.depsStatus), `maven 项目返回 installed/failed(${r.depsStatus},mvn 可能不可用)`)
140
+ }
141
+
142
+ cleanup()
143
+
144
+ console.log(`\n==================================================`)
145
+ console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
146
+ console.log(failed === 0 ? '全部通过' : `❌ 失败项: ${failures.join('; ')}`)
147
+ console.log(`==================================================`)
148
+ process.exit(failed === 0 ? 0 : 1)
@@ -0,0 +1,136 @@
1
+ import assert from 'node:assert/strict'
2
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { tmpdir } from 'node:os'
5
+ import { shouldBlock } from '../src/hooks/worktree-guard.js'
6
+
7
+ const root = join(tmpdir(), `sillyspec-guard-test-${Date.now()}`)
8
+ const changeName = '2026-06-04-guard-test'
9
+ const runtimeDir = join(root, '.sillyspec', '.runtime')
10
+ const registeredWorktree = join(runtimeDir, 'worktrees', changeName)
11
+ const unregisteredWorktree = join(runtimeDir, 'worktrees', 'other-change')
12
+
13
+ mkdirSync(registeredWorktree, { recursive: true })
14
+ mkdirSync(unregisteredWorktree, { recursive: true })
15
+ writeFileSync(join(runtimeDir, 'gate-status.json'), JSON.stringify({
16
+ stage: 'execute',
17
+ changes: [changeName],
18
+ updatedAt: new Date().toISOString(),
19
+ }, null, 2))
20
+ writeFileSync(join(registeredWorktree, 'meta.json'), JSON.stringify({
21
+ changeName,
22
+ worktreePath: registeredWorktree,
23
+ mode: 'worktree',
24
+ }, null, 2))
25
+
26
+ try {
27
+ assert.equal(
28
+ shouldBlock({ tool: 'Write', filePath: join(registeredWorktree, 'src', 'ok.js'), cwd: root }).blocked,
29
+ false,
30
+ 'registered worktree writes should be allowed'
31
+ )
32
+
33
+ assert.equal(
34
+ shouldBlock({ tool: 'Bash', command: 'npm run build', cwd: registeredWorktree }).blocked,
35
+ false,
36
+ 'bash commands from a registered worktree cwd should be allowed'
37
+ )
38
+
39
+ assert.equal(
40
+ shouldBlock({ tool: 'Write', filePath: join(unregisteredWorktree, 'src', 'blocked.js'), cwd: root }).blocked,
41
+ true,
42
+ 'unregistered worktree storage writes should be blocked'
43
+ )
44
+
45
+ assert.equal(
46
+ shouldBlock({ tool: 'Write', filePath: join(root, '.sillyspec', 'docs', 'note.md'), cwd: root }).blocked,
47
+ false,
48
+ 'ordinary .sillyspec docs should remain writable'
49
+ )
50
+
51
+ writeFileSync(join(runtimeDir, 'gate-status.json'), JSON.stringify({
52
+ stage: 'scan',
53
+ changes: [changeName],
54
+ updatedAt: new Date().toISOString(),
55
+ }, null, 2))
56
+ writeFileSync(join(runtimeDir, 'scan-guard.json'), JSON.stringify({
57
+ sourceCommit: 'new-head',
58
+ startedAt: '2026-06-16T10:00:00.000Z',
59
+ forceRescan: false,
60
+ }, null, 2))
61
+ const scanDoc = join(root, '.sillyspec', 'docs', 'app', 'scan', 'ARCHITECTURE.md')
62
+ mkdirSync(join(root, '.sillyspec', 'docs', 'app', 'scan'), { recursive: true })
63
+ writeFileSync(scanDoc, [
64
+ '---',
65
+ 'source_commit: old-head',
66
+ 'updated_at: 2026-06-16T09:00:00.000Z',
67
+ '---',
68
+ '# Architecture',
69
+ '',
70
+ ].join('\n'))
71
+ assert.equal(
72
+ shouldBlock({ tool: 'Write', filePath: scanDoc, cwd: root }).blocked,
73
+ true,
74
+ 'scan overwrite should block stale source_commit without --force-rescan'
75
+ )
76
+ writeFileSync(join(runtimeDir, 'scan-guard.json'), JSON.stringify({
77
+ sourceCommit: 'new-head',
78
+ startedAt: '2026-06-16T10:00:00.000Z',
79
+ forceRescan: true,
80
+ }, null, 2))
81
+ assert.equal(
82
+ shouldBlock({ tool: 'Write', filePath: scanDoc, cwd: root }).blocked,
83
+ false,
84
+ 'scan overwrite should allow stale source_commit with --force-rescan'
85
+ )
86
+ const externalSpec = join(root, 'external-spec')
87
+ const externalScanDoc = join(externalSpec, 'docs', 'app', 'scan', 'ARCHITECTURE.md')
88
+ mkdirSync(join(externalSpec, '.runtime'), { recursive: true })
89
+ mkdirSync(join(externalSpec, 'docs', 'app', 'scan'), { recursive: true })
90
+ writeFileSync(join(externalSpec, '.runtime', 'scan-guard.json'), JSON.stringify({
91
+ sourceCommit: 'external-new-head',
92
+ startedAt: '2026-06-16T10:00:00.000Z',
93
+ forceRescan: false,
94
+ }, null, 2))
95
+ writeFileSync(externalScanDoc, [
96
+ '---',
97
+ 'source_commit: external-old-head',
98
+ 'updated_at: 2026-06-16T09:00:00.000Z',
99
+ '---',
100
+ '# Architecture',
101
+ '',
102
+ ].join('\n'))
103
+ assert.equal(
104
+ shouldBlock({ tool: 'Write', filePath: externalScanDoc, cwd: root }).blocked,
105
+ true,
106
+ 'external specRoot scan overwrite should read specRoot/.runtime/scan-guard.json'
107
+ )
108
+
109
+ rmSync(join(runtimeDir, 'gate-status.json'), { force: true })
110
+ writeFileSync(join(root, '.sillyspec', 'local.yaml'), [
111
+ 'worktreeHook:',
112
+ ' readonlyCommands:',
113
+ ' - custom-read',
114
+ '',
115
+ ].join('\n'))
116
+ assert.equal(
117
+ shouldBlock({ tool: 'Bash', command: 'custom-read status', cwd: root }).blocked,
118
+ false,
119
+ '.sillyspec/local.yaml readonlyCommands should extend the bash whitelist'
120
+ )
121
+
122
+ writeFileSync(join(runtimeDir, 'gate-status.json'), JSON.stringify({
123
+ stage: 'quick',
124
+ changes: [changeName],
125
+ updatedAt: new Date().toISOString(),
126
+ }, null, 2))
127
+ assert.equal(
128
+ shouldBlock({ tool: 'Write', filePath: join(root, 'src', 'quick.js'), cwd: root }).blocked,
129
+ false,
130
+ 'quick writes should still be allowed in the main workspace'
131
+ )
132
+
133
+ console.log('✅ worktree guard regression checks passed')
134
+ } finally {
135
+ rmSync(root, { recursive: true, force: true })
136
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * worktree native-worktree overlay regression tests
3
+ *
4
+ * 验证 Bug 1 + Bug 2 修复:
5
+ * 1. worktreeBase 固定到主仓库路径,不跟着 cwd 变化
6
+ * 2. 禁止 self-overlay(source === target 时)
7
+ * 3. native-worktree meta 缺失时 recover,不 overlay
8
+ * 4. meta 已存在时幂等返回,不重建
9
+ */
10
+
11
+ import fs from 'fs'
12
+ import path from 'path'
13
+ import os from 'os'
14
+ import { execSync } from 'child_process'
15
+
16
+ // ── Test 1: _resolveMainRepoRoot 在主仓库内返回 cwd ──
17
+
18
+ async function test1_mainRepoRoot() {
19
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'wt-'))
20
+ execSync('git init', { cwd: d, stdio: 'pipe' })
21
+ execSync('git config user.email "test@test.com"', { cwd: d, stdio: 'pipe' })
22
+ execSync('git config user.name "test"', { cwd: d, stdio: 'pipe' })
23
+ execSync('git commit --allow-empty -m "init"', { cwd: d, stdio: 'pipe' })
24
+
25
+ // Add .gitignore to allow worktreeBase path
26
+ fs.mkdirSync(path.join(d, '.sillyspec'), { recursive: true })
27
+ fs.writeFileSync(path.join(d, '.gitignore'), '.sillyspec/\n')
28
+
29
+ const { WorktreeManager } = await import('../src/worktree.js')
30
+ const wm = new WorktreeManager({ cwd: d })
31
+ const root = wm._resolveMainRepoRoot()
32
+ console.assert(root === d, `Test 1 FAIL: expected ${d}, got ${root}`)
33
+ console.log('✅ Test 1: main repo root resolves to cwd')
34
+
35
+ fs.rmSync(d, { recursive: true })
36
+ }
37
+
38
+ // ── Test 2: native-worktree 检测 + 幂等守卫 ──
39
+
40
+ async function test2_nativeWorktreeIdempotent() {
41
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'wt-'))
42
+ execSync('git init', { cwd: d, stdio: 'pipe' })
43
+ execSync('git config user.email "test@test.com"', { cwd: d, stdio: 'pipe' })
44
+ execSync('git config user.name "test"', { cwd: d, stdio: 'pipe' })
45
+ execSync('git commit --allow-empty -m "init"', { cwd: d, stdio: 'pipe' })
46
+
47
+ const wtDir = path.join(d, 'wt')
48
+ execSync(`git worktree add ${wtDir} -b test-branch`, { cwd: d, stdio: 'pipe' })
49
+
50
+ // wtDir is a linked worktree
51
+ const { WorktreeManager } = await import('../src/worktree.js')
52
+
53
+ // First: create meta from inside worktree (simulating native-worktree mode)
54
+ const wm1 = new WorktreeManager({ cwd: wtDir })
55
+ // This simulates what happens when Claude Code runs in the worktree
56
+ // and SillySpec calls create() — detectIsolation should return inWorktree=true
57
+ // and meta should be written to main repo's worktreeBase, not worktree's
58
+
59
+ // Verify worktreeBase points to main repo
60
+ const expectedBase = path.join(d, '.sillyspec', '.runtime', 'worktrees')
61
+ console.assert(wm1.worktreeBase === expectedBase, `Test 2 FAIL: worktreeBase=${wm1.worktreeBase}, expected=${expectedBase}`)
62
+ console.log('✅ Test 2: worktreeBase fixed to main repo path')
63
+
64
+ fs.rmSync(d, { recursive: true })
65
+ }
66
+
67
+ // ── Test 3: self-overlay 禁止 ──
68
+
69
+ async function test3_selfOverlayBlocked() {
70
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'wt-'))
71
+ execSync('git init', { cwd: d, stdio: 'pipe' })
72
+ execSync('git config user.email "test@test.com"', { cwd: d, stdio: 'pipe' })
73
+ execSync('git config user.name "test"', { cwd: d, stdio: 'pipe' })
74
+ execSync('git commit --allow-empty -m "init"', { cwd: d, stdio: 'pipe' })
75
+ fs.mkdirSync(path.join(d, '.sillyspec'), { recursive: true })
76
+ fs.writeFileSync(path.join(d, '.gitignore'), '.sillyspec/\n')
77
+
78
+ const { WorktreeManager } = await import('../src/worktree.js')
79
+ const wm = new WorktreeManager({ cwd: d })
80
+
81
+ // Create with in-place-fallback mode (source === target)
82
+ const result = wm._createInPlaceMeta('test-change', {
83
+ worktreePath: d,
84
+ branch: 'test-branch',
85
+ mode: 'in-place-fallback',
86
+ })
87
+
88
+ console.assert(result.mode === 'in-place-fallback', `Test 3 FAIL: mode=${result.mode}`)
89
+ // 返回值不包含 baselineFiles(是简化的返回结构),验证 meta 文件本身
90
+ const meta3 = wm.getMeta('test-change')
91
+ console.assert(meta3 && meta3.baselineFiles.length === 0, `Test 3 FAIL: meta baselineFiles should be empty`)
92
+ console.log('✅ Test 3: self-overlay blocked, baselineFiles empty')
93
+
94
+ fs.rmSync(d, { recursive: true })
95
+ }
96
+
97
+ // ── Test 4: meta 幂等,不重复 overlay ──
98
+
99
+ async function test4_metaIdempotent() {
100
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'wt-'))
101
+ execSync('git init', { cwd: d, stdio: 'pipe' })
102
+ execSync('git config user.email "test@test.com"', { cwd: d, stdio: 'pipe' })
103
+ execSync('git config user.name "test"', { cwd: d, stdio: 'pipe' })
104
+ execSync('git commit --allow-empty -m "init"', { cwd: d, stdio: 'pipe' })
105
+ fs.mkdirSync(path.join(d, '.sillyspec'), { recursive: true })
106
+ fs.writeFileSync(path.join(d, '.gitignore'), '.sillyspec/\n')
107
+
108
+ const { WorktreeManager } = await import('../src/worktree.js')
109
+ const wm = new WorktreeManager({ cwd: d })
110
+
111
+ // First create
112
+ const r1 = wm._createInPlaceMeta('test-change', {
113
+ worktreePath: d,
114
+ branch: 'test-branch',
115
+ mode: 'in-place-fallback',
116
+ })
117
+
118
+ // Second create (should return existing, not re-overlay)
119
+ const r2 = wm._createInPlaceMeta('test-change', {
120
+ worktreePath: d,
121
+ branch: 'test-branch',
122
+ mode: 'in-place-fallback',
123
+ })
124
+
125
+ console.assert(r1.baseHash === r2.baseHash, `Test 4 FAIL: hashes differ`)
126
+ console.log('✅ Test 4: _createInPlaceMeta idempotent')
127
+
128
+ fs.rmSync(d, { recursive: true })
129
+ }
130
+
131
+ // ── Test 5: native-worktree meta 恢复 ──
132
+
133
+ async function test5_nativeRecovery() {
134
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'wt-'))
135
+ execSync('git init', { cwd: d, stdio: 'pipe' })
136
+ execSync('git config user.email "test@test.com"', { cwd: d, stdio: 'pipe' })
137
+ execSync('git config user.name "test"', { cwd: d, stdio: 'pipe' })
138
+ execSync('git commit --allow-empty -m "init"', { cwd: d, stdio: 'pipe' })
139
+
140
+ const wtDir = path.join(d, 'wt')
141
+ execSync(`git worktree add ${wtDir} -b test-branch`, { cwd: d, stdio: 'pipe' })
142
+
143
+ const { WorktreeManager } = await import('../src/worktree.js')
144
+ const wm = new WorktreeManager({ cwd: wtDir })
145
+
146
+ // Simulate meta missing in native-worktree (should recover, not overlay)
147
+ const result = wm._recoverNativeWorktreeMeta('test-change', {
148
+ worktreePath: wtDir,
149
+ branch: 'test-branch',
150
+ })
151
+
152
+ console.assert(result.mode === 'native-worktree', `Test 5 FAIL: mode=${result.mode}`)
153
+ // 返回值不包含 baselineFiles,验证 meta 文件本身
154
+ const meta5 = wm.getMeta('test-change')
155
+ console.assert(meta5 && meta5.baselineFiles.length === 0, `Test 5 FAIL: meta baselineFiles should be empty`)
156
+
157
+ // Verify meta is readable now
158
+ const meta = wm.getMeta('test-change')
159
+ console.assert(meta !== null, `Test 5 FAIL: meta should exist after recovery`)
160
+ console.assert(meta.mode === 'native-worktree', `Test 5 FAIL: meta.mode=${meta.mode}`)
161
+ console.log('✅ Test 5: native-worktree meta recovery without overlay')
162
+
163
+ fs.rmSync(d, { recursive: true })
164
+ }
165
+
166
+ // ── Run all ──
167
+
168
+ const tests = [
169
+ ['main repo root', test1_mainRepoRoot],
170
+ ['native worktree worktreeBase', test2_nativeWorktreeIdempotent],
171
+ ['self-overlay blocked', test3_selfOverlayBlocked],
172
+ ['meta idempotent', test4_metaIdempotent],
173
+ ['native recovery', test5_nativeRecovery],
174
+ ]
175
+
176
+ let passed = 0, failed = 0
177
+ for (const [name, fn] of tests) {
178
+ try {
179
+ await fn()
180
+ passed++
181
+ } catch (e) {
182
+ console.log(`❌ ${name}: ${e.message}`)
183
+ failed++
184
+ }
185
+ }
186
+
187
+ console.log(`\n${passed}/${tests.length} passed`)
188
+ if (failed) process.exit(1)
@@ -1,97 +0,0 @@
1
- # deriveState 状态推导
2
-
3
- author: qinyi
4
- created_at: 2026-04-08 07:10:00
5
-
6
- ## 背景
7
-
8
- 当前 sillyspec 的状态管理依赖 `progress.json` 作为唯一数据源。如果 AI 崩溃或异常中断,progress.json 可能与实际产出不一致(如 artifacts 文件已生成但步骤未标记完成)。
9
-
10
- 借鉴 GSD v2 的 deriveState 架构,从文件系统反推状态,与 progress.json 交叉校验。
11
-
12
- ## 需求
13
-
14
- 1. `--done` 完成步骤时轻量校验当前步骤
15
- 2. `doctor` 自检时全量扫描所有阶段
16
- 3. `progress validate --deep` 支持手动触发全量校验
17
- 4. 安全修复策略:明显正确的情况自动修复,有歧义的不动
18
-
19
- ## 设计
20
-
21
- ### 新增文件:`src/derive.js`
22
-
23
- 纯函数模块,零外部依赖(仅 fs/path)。
24
-
25
- ```js
26
- export function deriveState(cwd, options = {}) {
27
- // options.mode: 'light' | 'full'(默认 light)
28
- // options.fix: boolean(默认 false,只报告不修复)
29
- // 返回 { issues: [{type, severity, step, artifact, suggestion}], fixed: number }
30
- }
31
- ```
32
-
33
- #### 扫描逻辑
34
-
35
- 1. 读取 progress.json,获取所有阶段步骤状态
36
- 2. 扫描 `.sillyspec/.runtime/artifacts/` 目录
37
- 3. 文件名格式:`{stage}-step{N}-{timestamp}.txt`
38
- 4. 解析文件名提取 stage、stepIndex 信息
39
- 5. 对比规则:
40
-
41
- | 情况 | 严重度 | 自动修复 |
42
- |------|--------|----------|
43
- | artifacts 有文件但 progress 标记未完成 | issue | ✅ 标记为 done |
44
- | progress 标记已完成但 artifacts 无文件 | warning | ❌ 可能被手动清理 |
45
- | artifacts 有 step5 但 progress 只到 step3 | issue | ✅ 补齐中间步骤 |
46
-
47
- #### 模式
48
-
49
- - **light**:只检查 currentStage 的当前步骤和前一步
50
- - **full**:检查所有阶段所有步骤
51
-
52
- ### 集成方式
53
-
54
- #### 1. run.js — `--done` 轻量校验
55
-
56
- 在 `completeStep` 末尾:
57
- ```js
58
- import { deriveState } from './derive.js';
59
- const result = deriveState(cwd, { mode: 'light', fix: true });
60
- if (result.fixed > 0) {
61
- console.log(`⚠️ 状态修复:${result.fixed} 个步骤已从 artifacts 恢复`);
62
- }
63
- ```
64
-
65
- #### 2. doctor.js — 全量扫描
66
-
67
- 第一步(SillySpec 内部检查)调用:
68
- ```js
69
- const result = deriveState(cwd, { mode: 'full', fix: false });
70
- // 将 issues 加入自检报告
71
- ```
72
-
73
- #### 3. progress.js — `validate --deep`
74
-
75
- validate 方法支持 deep 参数:
76
- ```js
77
- validate(cwd, deep = false) {
78
- // ...现有校验逻辑...
79
- if (deep) {
80
- const result = deriveState(cwd, { mode: 'full', fix: true });
81
- // 输出校验结果
82
- }
83
- }
84
- ```
85
-
86
- CLI:`sillyspec progress validate --deep`
87
-
88
- ## 改动范围
89
-
90
- - 新增:`src/derive.js`(~80 行)
91
- - 修改:`src/run.js`(2 行)、`src/stages/doctor.js`(3 行)、`src/progress.js`(5 行)、`src/index.js`(parse --deep)
92
-
93
- ## 不做的事
94
-
95
- - 不引入 SQLite 或其他新依赖
96
- - 不改变 progress.json 的数据结构
97
- - 不自动删除 progress 中有但 artifacts 无的步骤(可能被手动清理)
@@ -1,51 +0,0 @@
1
- # deriveState 状态推导 — 实现计划
2
-
3
- author: qinyi
4
- created_at: 2026-04-08 07:12:00
5
-
6
- ## Wave 1(核心,无依赖)
7
-
8
- - [ ] 实现 derive.js 核心函数
9
- - 新增: `src/derive.js`
10
- - 步骤:
11
- 1. 实现 `deriveState(cwd, options)` 纯函数
12
- 2. 实现 artifacts 文件名解析(`{stage}-step{N}-{timestamp}.txt`)
13
- 3. 实现 light/full 模式扫描逻辑
14
- 4. 实现安全修复策略(issues 分类 + fix 逻辑)
15
- 5. 验证: 在 sillyspec 项目上手动创建测试 artifacts,运行 `node -e "import('./src/derive.js').then(m => console.log(m.deriveState(process.cwd(), {mode:'full'})))"` 确认输出
16
-
17
- ## Wave 2(集成,依赖 Wave 1)
18
-
19
- - [ ] 集成 run.js --done 轻量校验
20
- - 修改: `src/run.js`
21
- - 参考: `completeStep` 函数末尾
22
- - 步骤:
23
- 1. 在 completeStep 末尾 import 并调用 deriveState(cwd, {mode:'light', fix:true})
24
- 2. 有修复时输出警告信息
25
- 3. 验证: 运行 brainstorm 完成 --done,确认无报错
26
-
27
- - [ ] 扩展 validate 支持 --deep
28
- - 修改: `src/progress.js`
29
- - 参考: `validate()` 方法
30
- - 步骤:
31
- 1. validate 方法加 deep 参数
32
- 2. deep=true 时调用 deriveState(cwd, {mode:'full', fix:true})
33
- 3. 验证: `sillyspec progress validate --deep` 确认输出校验结果
34
-
35
- - [ ] CLI parse --deep 参数
36
- - 修改: `src/index.js`
37
- - 参考: progress 子命令的参数解析
38
- - 步骤:
39
- 1. 在 progress validate 命令中解析 --deep flag
40
- 2. 传递给 validate 方法
41
- 3. 验证: `sillyspec progress validate --deep` 确认 flag 生效
42
-
43
- ## Wave 3(集成,依赖 Wave 1)
44
-
45
- - [ ] 集成 doctor.js 全量扫描
46
- - 修改: `src/stages/doctor.js`
47
- - 参考: doctor 第一步(SillySpec 内部检查)的 prompt
48
- - 步骤:
49
- 1. 在第一步 prompt 中加入 deriveState 全量扫描指令
50
- 2. 将 issues 列表纳入自检报告
51
- 3. 验证: `sillyspec run doctor` 确认第一步输出包含状态一致性检查
@@ -1,29 +0,0 @@
1
- # deriveState 状态推导 — 提案
2
-
3
- author: qinyi
4
- created_at: 2026-04-08 07:11:00
5
-
6
- ## 动机
7
-
8
- 当前 progress.json 是 sillyspec 唯一的状态数据源。AI 崩溃或异常中断时,progress.json 可能与实际产出不一致(artifacts 已生成但步骤未标记完成)。需要从文件系统反推状态,交叉校验。
9
-
10
- ## 变更范围
11
-
12
- - 新增 `src/derive.js`(状态推导纯函数)
13
- - 修改 `src/run.js`(--done 轻量校验)
14
- - 修改 `src/stages/doctor.js`(全量扫描)
15
- - 修改 `src/progress.js`(validate --deep)
16
- - 修改 `src/index.js`(parse --deep 参数)
17
-
18
- ## 不在范围内
19
-
20
- - 不改 progress.json 数据结构
21
- - 不引入新依赖
22
- - 不自动删除 progress 中有但 artifacts 无的步骤
23
-
24
- ## 成功标准
25
-
26
- 1. `--done` 完成步骤时自动校验并修复当前步骤
27
- 2. `doctor` 输出全量状态一致性报告
28
- 3. `sillyspec progress validate --deep` 可手动触发全量校验
29
- 4. 所有校验通过现有测试
@@ -1,34 +0,0 @@
1
- # deriveState 状态推导 — 需求
2
-
3
- author: qinyi
4
- created_at: 2026-04-08 07:11:00
5
-
6
- ## 功能需求
7
-
8
- ### FR1: deriveState 核心函数
9
- - 从 artifacts 目录扫描文件,解析 `{stage}-step{N}-{timestamp}.txt` 格式
10
- - 与 progress.json 步骤状态对比
11
- - 返回 issues 列表和修复计数
12
-
13
- ### FR2: 轻量模式(light)
14
- - 只检查 currentStage 的当前步骤和前一步
15
- - 用于 `--done` 完成时
16
-
17
- ### FR3: 全量模式(full)
18
- - 检查所有阶段所有步骤
19
- - 用于 doctor 和 validate --deep
20
-
21
- ### FR4: 安全修复策略
22
- - artifacts 有但 progress 漏记 → 自动修复为 done
23
- - artifacts 有 step5 但 progress 只到 step3 → 自动补齐
24
- - progress 有但 artifacts 无 → 只警告,不修复
25
-
26
- ### FR5: CLI 集成
27
- - `--done` 时静默调用轻量校验,有修复才输出
28
- - `doctor` 第一步输出全量报告
29
- - `sillyspec progress validate --deep` 手动触发
30
-
31
- ## 非功能需求
32
- - 零外部依赖(仅 fs/path)
33
- - 纯函数,易于测试
34
- - 不改变现有 API 行为