sillyspec 3.27.2 → 3.27.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 (221) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +8 -0
  2. package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
  3. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  4. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
  5. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  6. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  7. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
  8. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  9. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
  10. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
  11. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
  12. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
  13. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  14. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  15. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  16. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
  17. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  18. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
  19. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
  20. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  21. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
  22. package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
  23. package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
  24. package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
  25. package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
  26. package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
  27. package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
  28. package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
  29. package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
  30. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
  31. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  32. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
  33. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
  34. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
  35. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
  36. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
  37. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
  38. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
  39. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
  40. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
  41. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
  42. package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
  43. package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
  44. package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
  45. package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
  46. package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
  47. package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
  48. package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
  49. package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
  50. package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
  51. package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
  52. package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
  53. package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
  54. package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
  55. package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
  56. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
  57. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
  58. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
  59. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
  60. package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
  61. package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
  62. package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
  63. package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
  64. package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
  65. package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
  66. package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
  67. package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
  68. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
  69. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
  70. package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
  71. package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
  72. package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
  73. package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
  74. package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
  75. package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
  76. package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
  77. package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
  78. package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
  79. package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
  80. package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
  81. package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
  82. package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
  83. package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
  84. package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
  85. package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
  86. package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
  87. package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
  88. package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
  89. package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
  90. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
  91. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
  92. package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
  93. package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
  94. package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
  95. package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
  96. package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
  97. package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
  98. package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
  99. package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
  100. package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
  101. package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
  102. package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
  103. package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
  104. package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
  105. package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
  106. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
  107. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
  108. package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
  109. package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
  110. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
  111. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
  112. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
  113. package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
  114. package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
  115. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
  116. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
  117. package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
  118. package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
  119. package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
  120. package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
  121. package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
  122. package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
  123. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
  124. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
  125. package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
  126. package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
  127. package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
  128. package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
  129. package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
  130. package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
  131. package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
  132. package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
  133. package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
  134. package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
  135. package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
  136. package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
  137. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
  138. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
  139. package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
  140. package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
  141. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
  142. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
  143. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
  144. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
  145. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
  146. package/eval/README.md +86 -0
  147. package/eval/eval.config.json +28 -0
  148. package/eval/lib/adapters/cli.mjs +63 -0
  149. package/eval/lib/adapters/dryrun.mjs +27 -0
  150. package/eval/lib/config.mjs +82 -0
  151. package/eval/lib/db.mjs +76 -0
  152. package/eval/lib/report.mjs +120 -0
  153. package/eval/lib/runner.mjs +157 -0
  154. package/eval/lib/tasks.mjs +73 -0
  155. package/eval/lib/util.mjs +107 -0
  156. package/eval/lib/workdir.mjs +30 -0
  157. package/eval/run.mjs +83 -0
  158. package/eval/tasks/001-slugify/files/package.json +6 -0
  159. package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  160. package/eval/tasks/001-slugify/task.json +6 -0
  161. package/eval/tasks/001-slugify/verify.mjs +28 -0
  162. package/eval/tasks/002-each-day/files/package.json +6 -0
  163. package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  164. package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  165. package/eval/tasks/002-each-day/task.json +6 -0
  166. package/eval/tasks/002-each-day/verify.mjs +31 -0
  167. package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  168. package/eval/tasks/003-word-report/files/package.json +6 -0
  169. package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  170. package/eval/tasks/003-word-report/task.json +6 -0
  171. package/eval/tasks/003-word-report/verify.mjs +38 -0
  172. package/package.json +4 -2
  173. package/src/agent-session-log.js +45 -5
  174. package/src/change-risk-profile.js +83 -14
  175. package/src/config-schema.js +18 -3
  176. package/src/contract-matrix.js +90 -29
  177. package/src/datetime.js +20 -0
  178. package/src/decision-distill.js +517 -0
  179. package/src/dispatch/backends/local-agent.js +4 -4
  180. package/src/docs-check.js +227 -0
  181. package/src/docs-debt.js +187 -24
  182. package/src/doctor-diagnostics.js +102 -2
  183. package/src/foreign-declared.js +78 -0
  184. package/src/index.js +89 -9
  185. package/src/init.js +9 -7
  186. package/src/knowledge-match.js +107 -6
  187. package/src/local-register.js +7 -1
  188. package/src/module-resolve.js +12 -1
  189. package/src/modules.js +2 -1
  190. package/src/plan-adopt-waves.js +126 -0
  191. package/src/quicklog.js +50 -4
  192. package/src/run/command.js +122 -43
  193. package/src/run/complete-handlers.js +48 -19
  194. package/src/run/complete.js +5 -2
  195. package/src/run/gates.js +87 -22
  196. package/src/run/prompt.js +84 -42
  197. package/src/run/quick-audit.js +11 -0
  198. package/src/run/shared.js +152 -10
  199. package/src/run.js +2 -2
  200. package/src/scan-postcheck.js +30 -17
  201. package/src/stage-contract-spec.js +2 -1
  202. package/src/stage-contract.js +68 -4
  203. package/src/stages/archive.js +53 -12
  204. package/src/stages/brainstorm-auto.js +7 -0
  205. package/src/stages/brainstorm.js +11 -0
  206. package/src/stages/doctor.js +157 -1
  207. package/src/stages/execute.js +32 -6
  208. package/src/stages/plan-postcheck.js +76 -17
  209. package/src/stages/plan.js +1 -0
  210. package/src/stages/quick.js +6 -1
  211. package/src/stages/verify.js +31 -14
  212. package/src/sync.js +41 -3
  213. package/src/task-review.js +2 -2
  214. package/src/taskcard-placeholders.js +21 -0
  215. package/src/taskcard.js +10 -1
  216. package/src/verify-postcheck.js +315 -13
  217. package/src/verify-probes.js +46 -4
  218. package/src/worktree-apply.js +184 -7
  219. package/src/worktree-deps.js +57 -0
  220. package/src/worktree.js +201 -58
  221. package/templates/prompts/taskcard-rules.md +1 -0
@@ -0,0 +1,43 @@
1
+ export const definition = {
2
+ name: 'explore',
3
+ title: '自由探索',
4
+ description: '讨论、调研、画图,不写实现代码',
5
+ auxiliary: true,
6
+ steps: [
7
+ {
8
+ name: '自由探索',
9
+ prompt: `围绕用户给出的话题做技术探索,不进入实现。
10
+
11
+ ### 操作
12
+ 1. 明确探索边界:这次只讨论、调研、画图和识别风险
13
+ 2. 如果需要代码库上下文,可以读取:
14
+ - \`.sillyspec/projects/*.yaml\`
15
+ - \`.sillyspec/docs/<project>/scan/ARCHITECTURE.md\`
16
+ - \`.sillyspec/docs/<project>/scan/CONVENTIONS.md\`
17
+ - \`.sillyspec/changes/<change-name>/design.md\`
18
+ 3. 可以用 \`rg\` / \`ls\` / \`cat\` 调查已有结构和集成点
19
+ 4. 输出 2-3 个有价值方向、关键风险和下一步建议
20
+ 5. 如果用户要求保存结论,先明确保存位置,再写入对应文档
21
+ 6. 讨论 UI/交互方案且用户明确要求原型时,按下方「HTML 原型生成」执行
22
+
23
+ ### HTML 原型生成(仅用户明确要求时)
24
+ 探索保持只读姿态:不主动写原型文件;UI 讨论成形时可以提议("要不要出个原型看效果"),用户同意后再生成。生成要求与 brainstorm 阶段的原型对齐:
25
+ - 单文件 HTML(内联 CSS + JS),浏览器直接打开
26
+ - 高保真呈现讨论中的布局/组件/交互流程——用户看到的是"这个方案做出来长什么样",不是示意图
27
+ - 与项目现有界面风格一致:先读 scan 文档与现有前端代码,复用现有组件库/设计 token 的观感,不另起风格
28
+ - 保存到 \`{SPEC_ROOT}/explore/prototype-<名称>.html\`(目录不存在则先创建;用户指定了其他位置则从其指定)
29
+ - 原型是探索讨论工具、不是交付物,也不是实现代码——不要顺手把原型接进项目源码或构建;话题后续进入 brainstorm 时,把原型文件复制到 \`{SPEC_ROOT}/changes/<变更名>/\` 可直接复用为该变更的原型
30
+
31
+ ### 输出
32
+ 探索结论、选项对比、风险清单或 ASCII 图;如生成了原型,附原型文件路径
33
+
34
+ ### 铁律
35
+ - 不写实现代码
36
+ - 不安装依赖
37
+ - 不修改文件,除非用户明确要求保存探索结论或生成原型
38
+ - 不强行推进到 brainstorm/plan/execute`,
39
+ outputHint: '探索结论',
40
+ optional: false
41
+ }
42
+ ]
43
+ }
@@ -0,0 +1,29 @@
1
+ import { definition as brainstorm } from './brainstorm.js'
2
+ import { definition as plan } from './plan.js'
3
+ import { definition as execute } from './execute.js'
4
+ import { definition as verify } from './verify.js'
5
+ import { definition as scan } from './scan.js'
6
+ import { definition as quick } from './quick.js'
7
+ import { definition as explore } from './explore.js'
8
+ import { definition as archive } from './archive.js'
9
+ import { definition as status } from './status.js'
10
+ import { definition as doctor } from './doctor.js'
11
+
12
+ // 标记辅助阶段
13
+ const auxiliaryFlag = { auxiliary: true }
14
+
15
+ export const stageRegistry = {
16
+ brainstorm,
17
+ plan,
18
+ execute,
19
+ verify,
20
+ scan: { ...scan, ...auxiliaryFlag },
21
+ quick: { ...quick, ...auxiliaryFlag },
22
+ explore: { ...explore, ...auxiliaryFlag },
23
+ archive: { ...archive, ...auxiliaryFlag },
24
+ status: { ...status, ...auxiliaryFlag },
25
+ doctor: { ...doctor, ...auxiliaryFlag }
26
+ }
27
+
28
+ // 辅助命令(在无活跃变更时也可执行)
29
+ export { AUXILIARY_STAGES as auxiliaryStages } from '../constants.js'
@@ -0,0 +1,514 @@
1
+ /**
2
+ * knowledge.js — agent-safe knowledge 管理命令
3
+ *
4
+ * 设计原则:
5
+ * - 所有输出为 JSON(--json 是默认行为,不需要显式传)
6
+ * - agent 可安全调用,不打开编辑器,不直接覆盖人工区
7
+ * - 失败有明确错误码
8
+ *
9
+ * 子命令:
10
+ * search — 关键词搜索知识库
11
+ * inspect — 读取单条知识详情
12
+ * validate — 校验知识库完整性
13
+ * refresh — 从 scan 文档刷新自动知识(仅写 generated/)
14
+ * propose — 提议新知识(写入 proposed/)
15
+ */
16
+
17
+ import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, statSync } from 'fs'
18
+ import { join, basename } from 'path'
19
+ import { parseKnowledgeIndex, matchKnowledge } from '../knowledge-match.js'
20
+
21
+ // ── 工具函数 ──
22
+
23
+ function resolveKnowledgeDir(dir, specDir) {
24
+ const base = specDir || join(dir, '.sillyspec')
25
+ return join(base, 'knowledge')
26
+ }
27
+
28
+ function output(ok, data, error) {
29
+ const result = { ok, ...data }
30
+ if (error) result.error = error
31
+ console.log(JSON.stringify(result, null, 2))
32
+ }
33
+
34
+ /**
35
+ * 从文件路径生成知识 ID
36
+ * knowledge/conventions.md → conventions
37
+ * knowledge/generated/patterns.md → generated/patterns
38
+ */
39
+ function pathToId(basePath, knowledgeDir) {
40
+ let rel = basePath
41
+ if (rel.startsWith(knowledgeDir + '/')) rel = rel.slice(knowledgeDir.length + 1)
42
+ if (rel.endsWith('.md')) rel = rel.slice(0, -3)
43
+ return rel
44
+ }
45
+
46
+ /**
47
+ * 从知识条目提取摘要(取第一段非空非标题文本)
48
+ */
49
+ function extractSummary(content, maxLen = 120) {
50
+ const lines = content.split('\n')
51
+ for (const line of lines) {
52
+ const trimmed = line.trim()
53
+ if (!trimmed) continue
54
+ if (trimmed.startsWith('#')) continue
55
+ if (trimmed.startsWith('---')) continue
56
+ if (trimmed.startsWith('author:') || trimmed.startsWith('created_at:')) continue
57
+ if (trimmed.startsWith('>')) continue
58
+ if (trimmed.startsWith('<!--')) continue
59
+ return trimmed.slice(0, maxLen) + (trimmed.length > maxLen ? '...' : '')
60
+ }
61
+ return ''
62
+ }
63
+
64
+ /**
65
+ * 提取文件标题(第一个 # 标题)
66
+ */
67
+ function extractTitle(content) {
68
+ const match = content.match(/^#\s+(.+)$/m)
69
+ return match ? match[1].trim() : ''
70
+ }
71
+
72
+ // ── search ──
73
+
74
+ export async function cmdSearch(dir, args, opts = {}) {
75
+ const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
76
+ const queryIdx = args.indexOf('--query')
77
+ const query = queryIdx >= 0 && args[queryIdx + 1] ? args[queryIdx + 1] : ''
78
+ const limitIdx = args.indexOf('--limit')
79
+ const limit = limitIdx >= 0 && args[limitIdx + 1] ? parseInt(args[limitIdx + 1]) : 10
80
+
81
+ if (!query) {
82
+ output(false, {}, '--query is required')
83
+ return
84
+ }
85
+
86
+ if (!existsSync(knowledgeDir)) {
87
+ output(false, {}, { code: 'knowledge_dir_missing', path: knowledgeDir })
88
+ return
89
+ }
90
+
91
+ // 复用 knowledge-match 引擎
92
+ const result = matchKnowledge(knowledgeDir, query)
93
+
94
+ if (!result.matched) {
95
+ output(true, { query, matches: [] })
96
+ return
97
+ }
98
+
99
+ // 增强:为每个匹配项补充 path、title、summary、score
100
+ const matches = result.entries.slice(0, limit).map(entry => {
101
+ const filePath = join(knowledgeDir, entry.file)
102
+ let title = entry.display
103
+ let summary = ''
104
+ let score = entry.keywords.length // 简单评分:匹配关键词数量
105
+
106
+ if (existsSync(filePath)) {
107
+ const content = readFileSync(filePath, 'utf8')
108
+ title = extractTitle(content) || entry.display
109
+ summary = extractSummary(content)
110
+ }
111
+
112
+ return {
113
+ id: pathToId(entry.file, knowledgeDir),
114
+ path: join('knowledge', entry.file),
115
+ title,
116
+ summary,
117
+ score,
118
+ tags: entry.keywords,
119
+ category: entry.category,
120
+ }
121
+ })
122
+
123
+ output(true, { query, matches })
124
+ }
125
+
126
+ // ── inspect ──
127
+
128
+ export async function cmdInspect(dir, args, opts = {}) {
129
+ const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
130
+ const idIdx = args.indexOf('--id')
131
+ const id = idIdx >= 0 && args[idIdx + 1] ? args[idIdx + 1] : ''
132
+
133
+ if (!id) {
134
+ output(false, {}, '--id is required')
135
+ return
136
+ }
137
+
138
+ if (!existsSync(knowledgeDir)) {
139
+ output(false, {}, { code: 'knowledge_dir_missing', path: knowledgeDir })
140
+ return
141
+ }
142
+
143
+ // ID → 文件路径:conventions → knowledge/conventions.md
144
+ // generated/patterns → knowledge/generated/patterns.md
145
+ const filePath = join(knowledgeDir, id + '.md')
146
+
147
+ if (!existsSync(filePath)) {
148
+ output(false, {}, { code: 'not_found', id, path: filePath })
149
+ return
150
+ }
151
+
152
+ const content = readFileSync(filePath, 'utf8')
153
+ const title = extractTitle(content)
154
+ const summary = extractSummary(content)
155
+
156
+ // 元数据
157
+ const meta = {}
158
+ const authorMatch = content.match(/^author:\s*(.+)$/m)
159
+ const createdAtMatch = content.match(/^created_at:\s*(.+)$/m)
160
+ if (authorMatch) meta.author = authorMatch[1].trim()
161
+ if (createdAtMatch) meta.created_at = createdAtMatch[1].trim()
162
+
163
+ // 文件修改时间
164
+ const stat = statSync(filePath)
165
+ meta.updated_at = stat.mtime.toISOString()
166
+
167
+ // 分区检测
168
+ let zone = 'manual'
169
+ if (id.startsWith('generated/')) zone = 'generated'
170
+ else if (id.startsWith('proposed/')) zone = 'proposed'
171
+
172
+ output(true, {
173
+ entry: {
174
+ id,
175
+ title,
176
+ summary,
177
+ zone,
178
+ path: join('knowledge', id + '.md'),
179
+ meta,
180
+ body: content,
181
+ }
182
+ })
183
+ }
184
+
185
+ // ── validate ──
186
+
187
+ export async function cmdValidate(dir, args, opts = {}) {
188
+ const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
189
+ const errors = []
190
+ const warnings = []
191
+
192
+ if (!existsSync(knowledgeDir)) {
193
+ output(false, { errors, warnings }, { code: 'knowledge_dir_missing', path: knowledgeDir })
194
+ return
195
+ }
196
+
197
+ // 1. INDEX.md 存在
198
+ const indexPath = join(knowledgeDir, 'INDEX.md')
199
+ if (!existsSync(indexPath)) {
200
+ errors.push({ code: 'missing_index', path: 'knowledge/INDEX.md' })
201
+ } else {
202
+ // 2. INDEX.md 引用的文件是否存在
203
+ const entries = parseKnowledgeIndex(knowledgeDir)
204
+ for (const entry of entries) {
205
+ const refPath = join(knowledgeDir, entry.file)
206
+ if (!existsSync(refPath)) {
207
+ errors.push({
208
+ code: 'broken_reference',
209
+ path: join('knowledge', entry.file),
210
+ referenced_in: 'INDEX.md',
211
+ display: entry.display,
212
+ })
213
+ }
214
+ }
215
+ }
216
+
217
+ // 3. 扫描所有 .md 文件,检查是否在 INDEX 中注册
218
+ const allMdFiles = scanMdFiles(knowledgeDir, '')
219
+ const indexedFiles = new Set(
220
+ parseKnowledgeIndex(knowledgeDir).map(e => e.file)
221
+ )
222
+
223
+ for (const mdFile of allMdFiles) {
224
+ if (mdFile === 'INDEX.md') continue
225
+ // generated/ 和 proposed/ 区不强制要求 INDEX 注册
226
+ if (mdFile.startsWith('generated/') || mdFile.startsWith('proposed/')) continue
227
+ if (!indexedFiles.has(mdFile)) {
228
+ warnings.push({
229
+ code: 'unregistered_file',
230
+ path: join('knowledge', mdFile),
231
+ })
232
+ }
233
+ }
234
+
235
+ // 4. uncategorized.md 条目数(多了提示需清理)。条目行契约是 `## <qlId> | <标题>`
236
+ //(quick 自动候选与手工条目同格式),h3 形态一并兼容;原 `^###` 只数 h3 恒为 0
237
+ const uncategorizedPath = join(knowledgeDir, 'uncategorized.md')
238
+ if (existsSync(uncategorizedPath)) {
239
+ const content = readFileSync(uncategorizedPath, 'utf8')
240
+ const entryCount = (content.match(/^#{2,3}\s+\S/gm) || []).length
241
+ if (entryCount >= 10) {
242
+ warnings.push({
243
+ code: 'too_many_uncategorized',
244
+ count: entryCount,
245
+ path: 'knowledge/uncategorized.md',
246
+ })
247
+ }
248
+ }
249
+
250
+ // 5. 空文件检测
251
+ for (const mdFile of allMdFiles) {
252
+ if (mdFile === 'INDEX.md') continue
253
+ const fullPath = join(knowledgeDir, mdFile)
254
+ const content = readFileSync(fullPath, 'utf8')
255
+ if (content.trim().length === 0) {
256
+ errors.push({
257
+ code: 'empty_file',
258
+ path: join('knowledge', mdFile),
259
+ })
260
+ }
261
+ }
262
+
263
+ output(errors.length === 0, { errors, warnings })
264
+ }
265
+
266
+ // ── refresh ──
267
+
268
+ export async function cmdRefresh(dir, args, opts = {}) {
269
+ const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
270
+ const generatedDir = join(knowledgeDir, 'generated')
271
+
272
+ if (!existsSync(knowledgeDir)) {
273
+ output(false, {}, { code: 'knowledge_dir_missing', path: knowledgeDir })
274
+ return
275
+ }
276
+
277
+ // refresh 只写 generated/ 区
278
+ mkdirSync(generatedDir, { recursive: true })
279
+
280
+ // 扫描 scan 文档。项目名用 DB 注册名优先(坑 knowledge-basename-project-mismatch):
281
+ // scan 写侧 docs/<project> 的 project 来自 dbProjectName || basename(cwd),注册名 ≠ 目录名
282
+ // (projects yaml name/path 分离、平台模式 specRoot≠sourceRoot)时,用 basename(dir) 拼
283
+ // scanDir 永远找不到 → refresh 恒报 scan_docs_missing。DB 读失败回退 basename(与写侧
284
+ // default 同口径)。查不到注册名对应目录时再回退 basename 试一次(双形态兼容)。
285
+ const base = opts.specDir || join(dir, '.sillyspec')
286
+ let projectName = null
287
+ try {
288
+ const { DatabaseSync } = await import('node:sqlite')
289
+ const db = new DatabaseSync(join(base, '.runtime', 'sillyspec.db'), { readOnly: true })
290
+ const row = db.prepare('SELECT name FROM project LIMIT 1').get()
291
+ db.close()
292
+ if (row && row.name) projectName = row.name
293
+ } catch { /* db 不可读 → 走 basename 兜底 */ }
294
+ if (!projectName) projectName = basename(dir)
295
+ let scanDir = join(base, 'docs', projectName, 'scan')
296
+ if (!existsSync(scanDir) && projectName !== basename(dir)) {
297
+ scanDir = join(base, 'docs', basename(dir), 'scan')
298
+ }
299
+
300
+ if (!existsSync(scanDir)) {
301
+ output(false, {}, { code: 'scan_docs_missing', path: scanDir })
302
+ return
303
+ }
304
+
305
+ const scanFiles = readdirSync(scanDir).filter(f => f.endsWith('.md'))
306
+ const generated = []
307
+ const overwritten = []
308
+
309
+ for (const scanFile of scanFiles) {
310
+ const scanPath = join(scanDir, scanFile)
311
+ const content = readFileSync(scanPath, 'utf8')
312
+ const title = extractTitle(content) || scanFile.replace('.md', '')
313
+
314
+ // 简单提取:取每个 ## 章节作为一个知识条目候选
315
+ const sections = extractSections(content)
316
+ for (const section of sections) {
317
+ if (section.lines < 3) continue // 太短的章节跳过
318
+
319
+ const slug = section.heading.toLowerCase()
320
+ .replace(/[^\w\u4e00-\u9fff]+/g, '-')
321
+ .replace(/^-|-$/g, '')
322
+ .slice(0, 60)
323
+
324
+ const fileName = `${slug}.md`
325
+ const genPath = join(generatedDir, fileName)
326
+ const isNew = !existsSync(genPath)
327
+
328
+ const entryContent = [
329
+ `---`,
330
+ `source: scan/${scanFile}`,
331
+ `section: ${section.heading}`,
332
+ `generated_at: ${new Date().toISOString()}`,
333
+ `---`,
334
+ ``,
335
+ `# ${title} — ${section.heading}`,
336
+ ``,
337
+ section.body,
338
+ ].join('\n')
339
+
340
+ writeFileSync(genPath, entryContent)
341
+ generated.push({ file: `generated/${fileName}`, title: `${title} — ${section.heading}`, new: isNew })
342
+ if (!isNew) overwritten.push(`generated/${fileName}`)
343
+ }
344
+ }
345
+
346
+ // 更新 generated/INDEX.md
347
+ const genIndexPath = join(generatedDir, 'INDEX.md')
348
+ const indexLines = [
349
+ '---',
350
+ `generated_at: ${new Date().toISOString()}`,
351
+ '---',
352
+ '',
353
+ '# Generated Knowledge Index',
354
+ '',
355
+ '> Auto-generated from scan documents. Do not edit manually.',
356
+ '',
357
+ ]
358
+ for (const g of generated) {
359
+ indexLines.push(`- ${g.title} → [${g.file}](${g.file})`)
360
+ }
361
+ writeFileSync(genIndexPath, indexLines.join('\n'))
362
+
363
+ output(true, {
364
+ generated_count: generated.length,
365
+ new_count: generated.filter(g => g.new).length,
366
+ overwritten_count: overwritten.length,
367
+ files: generated,
368
+ })
369
+ }
370
+
371
+ // ── propose ──
372
+
373
+ export async function cmdPropose(dir, args, opts = {}) {
374
+ const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
375
+ const proposedDir = join(knowledgeDir, 'proposed')
376
+
377
+ const titleIdx = args.indexOf('--title')
378
+ const title = titleIdx >= 0 && args[titleIdx + 1] ? args[titleIdx + 1] : ''
379
+ const categoryIdx = args.indexOf('--category')
380
+ const category = categoryIdx >= 0 && args[categoryIdx + 1] ? args[categoryIdx + 1] : 'uncategorized'
381
+ const bodyIdx = args.indexOf('--body')
382
+ const body = bodyIdx >= 0 && args[bodyIdx + 1] ? args[bodyIdx + 1] : ''
383
+ const fromIdx = args.indexOf('--from')
384
+ const from = fromIdx >= 0 && args[fromIdx + 1] ? args[fromIdx + 1] : ''
385
+
386
+ if (!title) {
387
+ output(false, {}, '--title is required')
388
+ return
389
+ }
390
+
391
+ mkdirSync(proposedDir, { recursive: true })
392
+
393
+ // 生成 slug
394
+ const slug = title.toLowerCase()
395
+ .replace(/[^\w\u4e00-\u9fff]+/g, '-')
396
+ .replace(/^-|-$/g, '')
397
+ .slice(0, 60)
398
+
399
+ const fileName = `${slug}.md`
400
+ const propPath = join(proposedDir, fileName)
401
+ const isNew = !existsSync(propPath)
402
+
403
+ const content = [
404
+ '---',
405
+ `proposed_at: ${new Date().toISOString()}`,
406
+ `category: ${category}`,
407
+ ...(from ? [`source: ${from}`] : []),
408
+ 'status: pending_review',
409
+ '---',
410
+ '',
411
+ `# ${title}`,
412
+ '',
413
+ body || '(no body provided)',
414
+ '',
415
+ '---',
416
+ '> This is a proposed knowledge entry. Review and merge into manual/ or generated/.',
417
+ ].join('\n')
418
+
419
+ writeFileSync(propPath, content)
420
+
421
+ output(true, {
422
+ id: `proposed/${slug}`,
423
+ path: `knowledge/proposed/${fileName}`,
424
+ title,
425
+ category,
426
+ new: isNew,
427
+ action: isNew ? 'created' : 'updated',
428
+ })
429
+ }
430
+
431
+ // ── 辅助函数 ──
432
+
433
+ function scanMdFiles(baseDir, prefix) {
434
+ const results = []
435
+ const items = readdirSync(baseDir, { withFileTypes: true })
436
+ for (const item of items) {
437
+ const rel = prefix ? `${prefix}/${item.name}` : item.name
438
+ if (item.isDirectory()) {
439
+ results.push(...scanMdFiles(join(baseDir, item.name), rel))
440
+ } else if (item.name.endsWith('.md')) {
441
+ results.push(rel)
442
+ }
443
+ }
444
+ return results
445
+ }
446
+
447
+ function extractSections(content) {
448
+ const lines = content.split('\n')
449
+ const sections = []
450
+ let currentHeading = null
451
+ let currentLines = []
452
+ let currentBody = []
453
+
454
+ for (const line of lines) {
455
+ if (line.startsWith('## ')) {
456
+ if (currentHeading) {
457
+ sections.push({
458
+ heading: currentHeading,
459
+ body: currentBody.join('\n').trim(),
460
+ lines: currentLines.length,
461
+ })
462
+ }
463
+ currentHeading = line.slice(3).trim()
464
+ currentLines = [line]
465
+ currentBody = []
466
+ } else if (currentHeading) {
467
+ currentLines.push(line)
468
+ // 跳过 frontmatter 和标题行
469
+ if (!line.startsWith('---') && !line.startsWith('#')) {
470
+ currentBody.push(line)
471
+ }
472
+ }
473
+ }
474
+
475
+ if (currentHeading) {
476
+ sections.push({
477
+ heading: currentHeading,
478
+ body: currentBody.join('\n').trim(),
479
+ lines: currentLines.length,
480
+ })
481
+ }
482
+
483
+ return sections.filter(s => s.lines >= 3)
484
+ }
485
+
486
+ // ── 入口路由 ──
487
+
488
+ /**
489
+ * @param {string[]} args - filteredArgs.slice(1)(去掉 'knowledge')
490
+ * @param {string} dir - 项目根目录
491
+ * @param {object} opts - { specDir }
492
+ */
493
+ export async function cmdKnowledge(args, dir, opts = {}) {
494
+ const subCommand = args[0] || ''
495
+
496
+ switch (subCommand) {
497
+ case 'search':
498
+ return cmdSearch(dir, args.slice(1), opts)
499
+ case 'inspect':
500
+ return cmdInspect(dir, args.slice(1), opts)
501
+ case 'validate':
502
+ return cmdValidate(dir, args.slice(1), opts)
503
+ case 'refresh':
504
+ return cmdRefresh(dir, args.slice(1), opts)
505
+ case 'propose':
506
+ return cmdPropose(dir, args.slice(1), opts)
507
+ default:
508
+ output(false, {}, {
509
+ code: 'unknown_subcommand',
510
+ subcommand: subCommand,
511
+ available: ['search', 'inspect', 'validate', 'refresh', 'propose'],
512
+ })
513
+ }
514
+ }