sillyspec 3.20.2 → 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 (208) 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 +1 -7
  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 -688
  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 -3008
  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 -485
  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/CLAUDE.md +0 -18
  131. package/docs/brainstorm-plan-contract.md +0 -64
  132. package/docs/plan-execute-contract.md +0 -123
  133. package/docs/platform-scan-protocol.md +0 -298
  134. package/docs/revision-mode.md +0 -115
  135. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +0 -99
  136. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +0 -218
  137. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +0 -167
  138. package/docs/sillyspec/file-lifecycle/storage-and-state.md +0 -148
  139. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +0 -193
  140. package/docs/sillyspec/file-lifecycle.md +0 -125
  141. package/docs/workflow-contract-regression.md +0 -106
  142. package/docs/worktree-isolation.md +0 -252
  143. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +0 -1
  144. package/packages/dashboard/dist/prototype-dashboard.html +0 -836
  145. package/packages/dashboard/dist/prototype-overview.html +0 -256
  146. package/packages/dashboard/public/prototype-dashboard.html +0 -836
  147. package/packages/dashboard/public/prototype-overview.html +0 -256
  148. package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
  149. package/packages/dashboard/src/components/ProjectCard.vue +0 -187
  150. package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
  151. package/packages/dashboard/src/composables/useLayout.js +0 -131
  152. package/src/brainstorm-postcheck.js +0 -158
  153. package/src/change-list.js +0 -52
  154. package/src/change-risk-profile.js +0 -352
  155. package/src/classify-change.js +0 -73
  156. package/src/constants.js +0 -70
  157. package/src/contract-matrix.js +0 -278
  158. package/src/db.js +0 -201
  159. package/src/endpoint-extractor.js +0 -315
  160. package/src/hooks/claude-pre-tool-use.cjs +0 -125
  161. package/src/hooks/worktree-guard.js +0 -761
  162. package/src/knowledge-match.js +0 -130
  163. package/src/modules.js +0 -482
  164. package/src/scan-postcheck.js +0 -383
  165. package/src/stage-contract.js +0 -700
  166. package/src/stages/brainstorm-auto.js +0 -229
  167. package/src/stages/explore.js +0 -34
  168. package/src/stages/knowledge.js +0 -498
  169. package/src/stages/plan-postcheck.js +0 -513
  170. package/src/sync.js +0 -497
  171. package/src/task-review.js +0 -346
  172. package/src/workflow.js +0 -785
  173. package/src/worktree-apply.js +0 -549
  174. package/src/worktree.js +0 -932
  175. package/templates/workflows/archive-impact.yaml +0 -79
  176. package/templates/workflows/scan-docs.yaml +0 -132
  177. package/test/brainstorm-plan-contract.test.mjs +0 -273
  178. package/test/check-syntax.mjs +0 -26
  179. package/test/cli-top-level-aliases.test.mjs +0 -174
  180. package/test/contract-artifacts.test.mjs +0 -323
  181. package/test/decision-supersede.test.mjs +0 -277
  182. package/test/knowledge-match.test.mjs +0 -231
  183. package/test/plan-execute-contract.test.mjs +0 -330
  184. package/test/plan-optimization.test.mjs +0 -572
  185. package/test/platform-artifacts.test.mjs +0 -190
  186. package/test/platform-failure-samples.test.mjs +0 -199
  187. package/test/platform-recovery-chain.test.mjs +0 -179
  188. package/test/platform-recovery.test.mjs +0 -167
  189. package/test/platform-scan-p0.test.mjs +0 -175
  190. package/test/revision-v1.test.mjs +0 -1145
  191. package/test/run-sanitize-project-name.test.mjs +0 -51
  192. package/test/run-scan-postcheck-fail.test.mjs +0 -64
  193. package/test/run-scan-project-parse.test.mjs +0 -200
  194. package/test/run-tests.mjs +0 -48
  195. package/test/scan-docs-yaml-placeholders.test.mjs +0 -84
  196. package/test/scan-knowledge.test.mjs +0 -175
  197. package/test/scan-paths.test.mjs +0 -68
  198. package/test/scan-postcheck-project-priority.test.mjs +0 -85
  199. package/test/scan-postcheck.test.mjs +0 -197
  200. package/test/scan-workflow-anyfailed-block.test.mjs +0 -52
  201. package/test/spec-dir.test.mjs +0 -206
  202. package/test/stage-contract-failed-post-check.test.mjs +0 -102
  203. package/test/stage-contract.test.mjs +0 -299
  204. package/test/stage-definitions.test.mjs +0 -39
  205. package/test/wait-gates.test.mjs +0 -501
  206. package/test/workflow-spec-base.test.mjs +0 -142
  207. package/test/worktree-guard.test.mjs +0 -136
  208. package/test/worktree-native-overlay.test.mjs +0 -188
@@ -1,315 +0,0 @@
1
- /**
2
- * endpoint-extractor.js — 从代码中提取 HTTP 端点定义和调用
3
- *
4
- * provider 端:扫描 router 文件,提取注册的 API 路径
5
- * consumer 端:扫描前端文件,提取 apiFetch/request 调用路径
6
- *
7
- * 契约对账时:provider 产出 ≠ consumer 消费 → gap
8
- */
9
-
10
- import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
11
- import { join, extname, basename, dirname, resolve } from 'path'
12
- import { execSync } from 'child_process'
13
-
14
- // ─── Provider: 扫描后端 router 注册的端点 ───────────────────────────────
15
-
16
- /**
17
- * 从单个文件提取 FastAPI router 端点
18
- * 支持 APIRouter(prefix=...) 和 @router.get/post/put/delete/patch("/path")
19
- *
20
- * @param {string} filePath - 文件绝对路径
21
- * @returns {Array<{ method: string, path: string, source: string, line: number }>}
22
- */
23
- export function extractFastApiEndpoints(filePath) {
24
- const content = readFileSync(filePath, 'utf8')
25
- const lines = content.split('\n')
26
- const endpoints = []
27
-
28
- // 1. 提取 router prefix
29
- let routerPrefix = ''
30
- for (const line of lines) {
31
- const prefixMatch = line.match(/(?:APIRouter|router)\s*\(\s*(?:prefix\s*=\s*)?["'`]([^"'`]+)["'`]/)
32
- || line.match(/\.include_router\s*\([^)]*prefix\s*=\s*["'`]([^"'`]+)["'`]/)
33
- if (prefixMatch) {
34
- routerPrefix = prefixMatch[1]
35
- }
36
- }
37
-
38
- // 2. 提取 @router.method("/path") 或分散式定义
39
- // FastAPI 支持两种写法:
40
- // a) @router.get("/path", ...) 下一行 def func():
41
- // b) @router.get 下一行 ("/path", ...)
42
- for (let i = 0; i < lines.length; i++) {
43
- const line = lines[i]
44
- const decoratorMatch = line.match(
45
- /@(?:router|api_router)\.(get|post|put|delete|patch)\s*\(\s*["'`]([^"'`]+)["'`]/
46
- )
47
- if (decoratorMatch) {
48
- const method = decoratorMatch[1].toUpperCase()
49
- const rawPath = decoratorMatch[2]
50
- endpoints.push({
51
- method,
52
- path: routerPrefix + rawPath,
53
- source: filePath,
54
- line: i + 1,
55
- })
56
- continue
57
- }
58
- // 分散式: @router.get\n ("/path",
59
- const splitMatch = line.match(/@(?:router|api_router)\.(get|post|put|delete|patch)\s*$/)
60
- if (splitMatch && i + 1 < lines.length) {
61
- const nextLine = lines[i + 1]
62
- const pathMatch = nextLine.match(/\(\s*["'`]([^"'`]+)["'`]/)
63
- if (pathMatch) {
64
- endpoints.push({
65
- method: splitMatch[1].toUpperCase(),
66
- path: routerPrefix + pathMatch[1],
67
- source: filePath,
68
- line: i + 1,
69
- })
70
- }
71
- }
72
- }
73
-
74
- return endpoints
75
- }
76
-
77
- /**
78
- * 从目录递归扫描所有 Python router 文件的端点
79
- * @param {string} dir
80
- * @param {{ filePattern?: RegExp, excludePatterns?: RegExp[] }} opts
81
- * @returns {Array<{ method: string, path: string, source: string, line: number }>}
82
- */
83
- export function scanBackendEndpoints(dir, opts = {}) {
84
- const filePattern = opts.filePattern || /(?:router|routes|api|endpoint|controller)\.py$/i
85
- const excludePatterns = opts.excludePatterns || [/__pycache__/, /node_modules/, /\.venv/, /test/i]
86
-
87
- const results = []
88
- if (!existsSync(dir)) return results
89
-
90
- function walk(d) {
91
- for (const entry of readdirSync(d, { withFileTypes: true })) {
92
- const full = join(d, entry.name)
93
- if (entry.isDirectory()) {
94
- if (excludePatterns.some(p => p.test(entry.name))) continue
95
- walk(full)
96
- } else if (entry.isFile() && filePattern.test(entry.name)) {
97
- try {
98
- results.push(...extractFastApiEndpoints(full))
99
- } catch {}
100
- }
101
- }
102
- }
103
- walk(dir)
104
- return results
105
- }
106
-
107
- // ─── Consumer: 扫描前端 API 调用路径 ─────────────────────────────────────
108
-
109
- /**
110
- * 从前端文件提取 API 调用路径
111
- * 支持:
112
- * apiFetch("/api/xxx")
113
- * request("/api/xxx")
114
- * axios.get("/api/xxx")
115
- * axios.post("/api/xxx")
116
- * fetch("/api/xxx")
117
- *
118
- * @param {string} filePath
119
- * @returns {Array<{ method: string, path: string, source: string, line: number, raw: string }>}
120
- */
121
- export function extractFrontendApiCalls(filePath) {
122
- const content = readFileSync(filePath, 'utf8')
123
- const lines = content.split('\n')
124
- const results = []
125
-
126
- for (let i = 0; i < lines.length; i++) {
127
- const line = lines[i]
128
-
129
- // Pattern 1: apiFetch<T>("/path", { method: "POST" }) — default GET
130
- const apiFetchMatch = line.match(/apiFetch\s*(?:<[^>]*>)?\s*\(\s*["'`]([^"'`]+)["'`]/)
131
- if (apiFetchMatch) {
132
- // 检查是否有 method 字段(在后续行或同一行)
133
- let method = 'GET'
134
- const snippet = lines.slice(i, Math.min(i + 3, lines.length)).join(' ')
135
- const methodMatch = snippet.match(/method\s*:\s*["'`](GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)["'`]/i)
136
- if (methodMatch) method = methodMatch[1].toUpperCase()
137
-
138
- results.push({
139
- method,
140
- path: normalizePath(apiFetchMatch[1]),
141
- source: filePath,
142
- line: i + 1,
143
- raw: apiFetchMatch[1],
144
- })
145
- continue
146
- }
147
-
148
- // Pattern 2: axios.get/post/put/delete("/path")
149
- const axiosMatch = line.match(/axios\.(get|post|put|delete|patch)\s*\(\s*["'`]([^"'`]+)["'`]/i)
150
- if (axiosMatch) {
151
- results.push({
152
- method: axiosMatch[1].toUpperCase(),
153
- path: normalizePath(axiosMatch[2]),
154
- source: filePath,
155
- line: i + 1,
156
- raw: axiosMatch[2],
157
- })
158
- continue
159
- }
160
-
161
- // Pattern 3: fetch("/api/xxx", { method: "POST" })
162
- const fetchMatch = line.match(/(?:api_?)?fetch\s*\(\s*["'`]([^"'`]+)["'`]/)
163
- if (fetchMatch && !apiFetchMatch) {
164
- let method = 'GET'
165
- const snippet = lines.slice(i, Math.min(i + 3, lines.length)).join(' ')
166
- const methodMatch = snippet.match(/method\s*:\s*["'`](GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)["'`]/i)
167
- if (methodMatch) method = methodMatch[1].toUpperCase()
168
-
169
- results.push({
170
- method,
171
- path: normalizePath(fetchMatch[1]),
172
- source: filePath,
173
- line: i + 1,
174
- raw: fetchMatch[1],
175
- })
176
- continue
177
- }
178
- }
179
-
180
- return results
181
- }
182
-
183
- /**
184
- * 递归扫描前端目录的 API 调用
185
- * @param {string} dir
186
- * @param {{ filePattern?: RegExp, excludePatterns?: RegExp[] }} opts
187
- * @returns {Array<{ method: string, path: string, source: string, line: number, raw: string }>}
188
- */
189
- export function scanFrontendApiCalls(dir, opts = {}) {
190
- const filePattern = opts.filePattern || /\.(ts|tsx|js|jsx)$/
191
- const excludePatterns = opts.excludePatterns || [/node_modules/, /\.next/, /dist/, /__tests__/, /\.d\.ts$/]
192
-
193
- const results = []
194
- if (!existsSync(dir)) return results
195
-
196
- function walk(d) {
197
- for (const entry of readdirSync(d, { withFileTypes: true })) {
198
- const full = join(d, entry.name)
199
- if (entry.isDirectory()) {
200
- if (excludePatterns.some(p => p.test(entry.name))) continue
201
- walk(full)
202
- } else if (entry.isFile() && filePattern.test(entry.name)) {
203
- try {
204
- results.push(...extractFrontendApiCalls(full))
205
- } catch {}
206
- }
207
- }
208
- }
209
- walk(dir)
210
- return results
211
- }
212
-
213
- // ─── 路径归一化 ─────────────────────────────────────────────────────────
214
-
215
- /**
216
- * 将动态路径归一化为参数占位符
217
- * /api/ppm/project-plan/${id}/plan-nodes → /api/ppm/project-plan/{param}/plan-nodes
218
- * /api/ppm/project-plan/:planId/plan-nodes → /api/ppm/project-plan/{param}/plan-nodes
219
- * @param {string} rawPath
220
- * @returns {string}
221
- */
222
- export function normalizePath(rawPath) {
223
- return rawPath
224
- .replace(/\$\{[^}]+\}/g, '{param}')
225
- .replace(/:\w+/g, '{param}')
226
- .replace(/\$\w+/g, '{param}')
227
- }
228
-
229
- // ─── 对账 ───────────────────────────────────────────────────────────────
230
-
231
- /**
232
- * 比较前端调用的路径和后端注册的端点,返回差异
233
- *
234
- * @param {Array<{ path: string, method: string, source: string }>} frontendCalls
235
- * @param {Array<{ path: string, method: string, source: string }>} backendEndpoints
236
- * @returns {{
237
- * missingBackend: Array<{ path: string, method: string, consumerFile: string, consumerLine: number }>,
238
- * unusedBackend: Array<{ path: string, method: string, providerFile: string }>
239
- * }}
240
- */
241
- export function diffApiParity(frontendCalls, backendEndpoints) {
242
- // 构建 backend 注册表:归一化 path + method → endpoint
243
- const backendMap = new Map()
244
- for (const ep of backendEndpoints) {
245
- const key = `${ep.method}:${normalizePath(ep.path)}`
246
- if (!backendMap.has(key)) backendMap.set(key, ep)
247
- }
248
-
249
- const missingBackend = []
250
- for (const call of frontendCalls) {
251
- const key = `${call.method}:${normalizePath(call.path)}`
252
- if (!backendMap.has(key)) {
253
- missingBackend.push({
254
- path: normalizePath(call.path),
255
- method: call.method,
256
- consumerFile: call.source,
257
- consumerLine: call.line,
258
- })
259
- }
260
- }
261
-
262
- // 构建 frontend 调用表
263
- const frontendSet = new Set(
264
- frontendCalls.map(c => `${c.method}:${normalizePath(c.path)}`)
265
- )
266
-
267
- const unusedBackend = []
268
- for (const ep of backendEndpoints) {
269
- const key = `${ep.method}:${normalizePath(ep.path)}`
270
- if (!frontendSet.has(key)) {
271
- unusedBackend.push({
272
- path: normalizePath(ep.path),
273
- method: ep.method,
274
- providerFile: ep.source,
275
- })
276
- }
277
- }
278
-
279
- return { missingBackend, unusedBackend, ok: missingBackend.length === 0 }
280
- }
281
-
282
- // ─── CLI 入口 ────────────────────────────────────────────────────────────
283
-
284
- /**
285
- * CLI 子命令入口:sillyspec contract scan [--backend dir] [--frontend dir]
286
- * 输出 JSON 格式的端点清单和对账结果
287
- */
288
- export async function contractScan(args, cwd) {
289
- const backendIdx = args.indexOf('--backend')
290
- const frontendIdx = args.indexOf('--frontend')
291
- const backendDir = backendIdx !== -1 && args[backendIdx + 1]
292
- ? resolve(cwd, args[backendIdx + 1])
293
- : resolve(cwd, 'backend')
294
- const frontendDir = frontendIdx !== -1 && args[frontendIdx + 1]
295
- ? resolve(cwd, args[frontendIdx + 1])
296
- : resolve(cwd, 'frontend')
297
-
298
- const backendEndpoints = scanBackendEndpoints(backendDir)
299
- const frontendCalls = scanFrontendApiCalls(frontendDir)
300
- const { missingBackend, unusedBackend } = diffApiParity(frontendCalls, backendEndpoints)
301
-
302
- return {
303
- backend: backendEndpoints.map(e => ({ method: e.method, path: normalizePath(e.path), file: e.source })),
304
- frontend: frontendCalls.map(c => ({ method: c.method, path: normalizePath(c.path), file: c.source })),
305
- missingBackend,
306
- unusedBackend,
307
- summary: {
308
- backendEndpointCount: backendEndpoints.length,
309
- frontendCallCount: frontendCalls.length,
310
- missingBackendCount: missingBackend.length,
311
- unusedBackendCount: unusedBackend.length,
312
- ok: missingBackend.length === 0,
313
- },
314
- }
315
- }
@@ -1,125 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Claude Code PreToolUse hook 入口
4
- *
5
- * 从 stdin 读取 JSON,调用 worktree-guard 判断是否拦截。
6
- *
7
- * Claude Code hooks.json 配置:
8
- * {
9
- * "hooks": {
10
- * "PreToolUse": [
11
- * {
12
- * "matcher": "Edit|Write|MultiEdit|Bash",
13
- * "hooks": [
14
- * {
15
- * "type": "command",
16
- * "command": "node /path/to/sillyspec/src/hooks/claude-pre-tool-use.cjs"
17
- * }
18
- * ]
19
- * }
20
- * ]
21
- * }
22
- * }
23
- */
24
-
25
- 'use strict'
26
-
27
- const path = require('path')
28
- const fs = require('fs')
29
-
30
- // 动态 import ESM 模块
31
- async function main() {
32
- // Claude Code hook 通过 stdin 传入 JSON
33
- let input = ''
34
- try {
35
- input = await readStdin()
36
- } catch {
37
- // 无法读取 stdin → 放行(安全优先于阻断)
38
- process.exit(0)
39
- }
40
-
41
- let parsed
42
- try {
43
- parsed = JSON.parse(input)
44
- } catch {
45
- // 非法 JSON → 放行
46
- process.exit(0)
47
- }
48
-
49
- // 解析 Claude Code hook 输入格式
50
- // 格式:{ tool_name: string, tool_input: { ... } }
51
- const toolName = parsed.tool_name || parsed.tool || ''
52
- const toolInput = parsed.tool_input || {}
53
-
54
- // 转换为 shouldBlock 的输入格式
55
- const toolMap = {
56
- 'Write': 'Write',
57
- 'Edit': 'Edit',
58
- 'MultiEdit': 'MultiEdit',
59
- 'Bash': 'Bash',
60
- }
61
-
62
- const mappedTool = toolMap[toolName]
63
- if (!mappedTool) {
64
- // 不在拦截范围内的工具 → 放行
65
- process.exit(0)
66
- }
67
-
68
- // 构造 opts
69
- const opts = { tool: mappedTool }
70
- if (mappedTool === 'Bash') {
71
- opts.command = toolInput.command || ''
72
- } else {
73
- // Write/Edit/MultiEdit
74
- const fp = toolInput.file_path || toolInput.filePath
75
- if (fp) opts.filePath = fp
76
- // MultiEdit 可能有多个文件
77
- if (mappedTool === 'MultiEdit') {
78
- const fps = toolInput.edits
79
- ? toolInput.edits.map(e => e.file_path || e.filePath).filter(Boolean)
80
- : []
81
- if (fps.length > 0) opts.filePaths = fps
82
- }
83
- }
84
-
85
- // 加载 worktree-guard(ESM)
86
- let shouldBlock
87
- try {
88
- const mod = await import('./worktree-guard.js')
89
- shouldBlock = mod.shouldBlock
90
- } catch (e) {
91
- // 模块加载失败 → 放行(不因为 hook 出错阻断工作流)
92
- // eslint-disable-next-line no-console
93
- console.error(`[sillyspec-hook] 模块加载失败: ${e.message}`)
94
- process.exit(0)
95
- }
96
-
97
- const result = shouldBlock(opts)
98
-
99
- if (result.blocked) {
100
- // 输出错误信息(Claude Code 会显示给 agent)
101
- // eslint-disable-next-line no-console
102
- console.error(`[sillyspec] ❌ ${result.reason || 'blocked by worktree guard'}`)
103
- process.exit(2) // exit code 2 = 阻止工具执行
104
- }
105
-
106
- // 放行
107
- process.exit(0)
108
- }
109
-
110
- function readStdin() {
111
- return new Promise((resolve, reject) => {
112
- const chunks = []
113
- process.stdin.setEncoding('utf8')
114
- process.stdin.on('data', chunk => chunks.push(chunk))
115
- process.stdin.on('end', () => resolve(chunks.join('')))
116
- process.stdin.on('error', reject)
117
- // 超时保护(3秒)
118
- setTimeout(() => {
119
- process.stdin.destroy()
120
- resolve('')
121
- }, 3000)
122
- })
123
- }
124
-
125
- main().catch(() => process.exit(0))