@dommaker/harness 0.18.0 → 1.0.0

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 (366) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/bin/harness.js +136 -552
  3. package/dist/cli/commands/check.d.ts.map +1 -1
  4. package/dist/cli/commands/check.js +2 -13
  5. package/dist/cli/commands/check.js.map +1 -1
  6. package/dist/cli/commands/definitions.d.ts +90 -0
  7. package/dist/cli/commands/definitions.d.ts.map +1 -0
  8. package/dist/cli/commands/definitions.js +519 -0
  9. package/dist/cli/commands/definitions.js.map +1 -0
  10. package/dist/cli/commands/knowledge.js.map +1 -1
  11. package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts +3 -25
  12. package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts.map +1 -1
  13. package/dist/cli/commands/sync-docs/capabilities-syncer.js +3 -93
  14. package/dist/cli/commands/sync-docs/capabilities-syncer.js.map +1 -1
  15. package/dist/cli/commands/sync-docs/index.d.ts +1 -1
  16. package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
  17. package/dist/cli/commands/sync-docs/index.js +4 -4
  18. package/dist/cli/commands/sync-docs/index.js.map +1 -1
  19. package/dist/cli/commands/update-user-model.d.ts +1 -0
  20. package/dist/cli/commands/update-user-model.d.ts.map +1 -1
  21. package/dist/cli/commands/update-user-model.js +12 -5
  22. package/dist/cli/commands/update-user-model.js.map +1 -1
  23. package/dist/context/index.d.ts +1 -5
  24. package/dist/context/index.d.ts.map +1 -1
  25. package/dist/context/index.js +1 -5
  26. package/dist/context/index.js.map +1 -1
  27. package/dist/context/knowledge-injector.d.ts +3 -3
  28. package/dist/context/knowledge-injector.d.ts.map +1 -1
  29. package/dist/context/knowledge-injector.js +1 -1
  30. package/dist/context/session-manager.d.ts +1 -12
  31. package/dist/context/session-manager.d.ts.map +1 -1
  32. package/dist/context/session-manager.js +0 -63
  33. package/dist/context/session-manager.js.map +1 -1
  34. package/dist/context/types.d.ts +1 -39
  35. package/dist/context/types.d.ts.map +1 -1
  36. package/dist/context/types.js +2 -15
  37. package/dist/context/types.js.map +1 -1
  38. package/dist/core/constraints/agent-prompt-renderer.d.ts +32 -0
  39. package/dist/core/constraints/agent-prompt-renderer.d.ts.map +1 -0
  40. package/dist/core/constraints/agent-prompt-renderer.js +58 -0
  41. package/dist/core/constraints/agent-prompt-renderer.js.map +1 -0
  42. package/dist/core/constraints/capabilities-parser.d.ts +17 -2
  43. package/dist/core/constraints/capabilities-parser.d.ts.map +1 -1
  44. package/dist/core/constraints/capabilities-parser.js +84 -2
  45. package/dist/core/constraints/capabilities-parser.js.map +1 -1
  46. package/dist/core/constraints/check-cache.d.ts +48 -3
  47. package/dist/core/constraints/check-cache.d.ts.map +1 -1
  48. package/dist/core/constraints/check-cache.js +53 -5
  49. package/dist/core/constraints/check-cache.js.map +1 -1
  50. package/dist/core/constraints/checker.d.ts +3 -17
  51. package/dist/core/constraints/checker.d.ts.map +1 -1
  52. package/dist/core/constraints/checker.js +7 -47
  53. package/dist/core/constraints/checker.js.map +1 -1
  54. package/dist/core/constraints/definitions.d.ts +0 -7
  55. package/dist/core/constraints/definitions.d.ts.map +1 -1
  56. package/dist/core/constraints/definitions.js +1 -8
  57. package/dist/core/constraints/definitions.js.map +1 -1
  58. package/dist/core/constraints/doc-freshness/runner.d.ts +0 -2
  59. package/dist/core/constraints/doc-freshness/runner.d.ts.map +1 -1
  60. package/dist/core/constraints/doc-freshness/runner.js.map +1 -1
  61. package/dist/core/constraints/index.d.ts +5 -1
  62. package/dist/core/constraints/index.d.ts.map +1 -1
  63. package/dist/core/constraints/index.js +8 -3
  64. package/dist/core/constraints/index.js.map +1 -1
  65. package/dist/core/constraints/interceptor.d.ts.map +1 -1
  66. package/dist/core/constraints/interceptor.js +1 -5
  67. package/dist/core/constraints/interceptor.js.map +1 -1
  68. package/dist/core/effective-constraints.d.ts.map +1 -1
  69. package/dist/core/effective-constraints.js +0 -1
  70. package/dist/core/effective-constraints.js.map +1 -1
  71. package/dist/core/project-config-loader.d.ts.map +1 -1
  72. package/dist/core/project-config-loader.js +1 -8
  73. package/dist/core/project-config-loader.js.map +1 -1
  74. package/dist/dashboard/stats.d.ts.map +1 -1
  75. package/dist/dashboard/stats.js.map +1 -1
  76. package/dist/dashboard/types.d.ts +2 -2
  77. package/dist/dashboard/types.d.ts.map +1 -1
  78. package/dist/failure/constraint-handler.d.ts.map +1 -1
  79. package/dist/failure/constraint-handler.js +0 -2
  80. package/dist/failure/constraint-handler.js.map +1 -1
  81. package/dist/gates/acceptance.d.ts +9 -1
  82. package/dist/gates/acceptance.d.ts.map +1 -1
  83. package/dist/gates/acceptance.js +21 -0
  84. package/dist/gates/acceptance.js.map +1 -1
  85. package/dist/gates/checker-gate.d.ts +23 -0
  86. package/dist/gates/checker-gate.d.ts.map +1 -0
  87. package/dist/gates/checker-gate.js +53 -0
  88. package/dist/gates/checker-gate.js.map +1 -0
  89. package/dist/gates/command.d.ts +9 -2
  90. package/dist/gates/command.d.ts.map +1 -1
  91. package/dist/gates/command.js +10 -0
  92. package/dist/gates/command.js.map +1 -1
  93. package/dist/gates/contract.d.ts +8 -2
  94. package/dist/gates/contract.d.ts.map +1 -1
  95. package/dist/gates/contract.js +9 -0
  96. package/dist/gates/contract.js.map +1 -1
  97. package/dist/gates/decision.d.ts +17 -0
  98. package/dist/gates/decision.d.ts.map +1 -0
  99. package/dist/gates/decision.js +25 -0
  100. package/dist/gates/decision.js.map +1 -0
  101. package/dist/gates/definitions.d.ts +63 -0
  102. package/dist/gates/definitions.d.ts.map +1 -0
  103. package/dist/gates/definitions.js +160 -0
  104. package/dist/gates/definitions.js.map +1 -0
  105. package/dist/gates/effective-gates.d.ts +30 -0
  106. package/dist/gates/effective-gates.d.ts.map +1 -0
  107. package/dist/gates/effective-gates.js +85 -0
  108. package/dist/gates/effective-gates.js.map +1 -0
  109. package/dist/gates/index.d.ts +25 -1
  110. package/dist/gates/index.d.ts.map +1 -1
  111. package/dist/gates/index.js +37 -1
  112. package/dist/gates/index.js.map +1 -1
  113. package/dist/gates/performance.d.ts +8 -2
  114. package/dist/gates/performance.d.ts.map +1 -1
  115. package/dist/gates/performance.js +9 -0
  116. package/dist/gates/performance.js.map +1 -1
  117. package/dist/gates/registry.d.ts +32 -0
  118. package/dist/gates/registry.d.ts.map +1 -0
  119. package/dist/gates/registry.js +90 -0
  120. package/dist/gates/registry.js.map +1 -0
  121. package/dist/gates/review.d.ts +8 -2
  122. package/dist/gates/review.d.ts.map +1 -1
  123. package/dist/gates/review.js +9 -0
  124. package/dist/gates/review.js.map +1 -1
  125. package/dist/gates/runner.d.ts +30 -0
  126. package/dist/gates/runner.d.ts.map +1 -0
  127. package/dist/gates/runner.js +38 -0
  128. package/dist/gates/runner.js.map +1 -0
  129. package/dist/gates/security.d.ts +8 -2
  130. package/dist/gates/security.d.ts.map +1 -1
  131. package/dist/gates/security.js +9 -0
  132. package/dist/gates/security.js.map +1 -1
  133. package/dist/gates/types.d.ts +41 -1
  134. package/dist/gates/types.d.ts.map +1 -1
  135. package/dist/hooks/bootstrap.d.ts.map +1 -1
  136. package/dist/hooks/bootstrap.js +0 -2
  137. package/dist/hooks/bootstrap.js.map +1 -1
  138. package/dist/hooks/config.d.ts +26 -0
  139. package/dist/hooks/config.d.ts.map +1 -0
  140. package/dist/hooks/config.js +30 -0
  141. package/dist/hooks/config.js.map +1 -0
  142. package/dist/hooks/index.d.ts +2 -1
  143. package/dist/hooks/index.d.ts.map +1 -1
  144. package/dist/hooks/index.js +4 -1
  145. package/dist/hooks/index.js.map +1 -1
  146. package/dist/hooks/registry.d.ts +18 -1
  147. package/dist/hooks/registry.d.ts.map +1 -1
  148. package/dist/hooks/registry.js +45 -0
  149. package/dist/hooks/registry.js.map +1 -1
  150. package/dist/hooks/types.d.ts +20 -0
  151. package/dist/hooks/types.d.ts.map +1 -1
  152. package/dist/index.d.ts +8 -8
  153. package/dist/index.d.ts.map +1 -1
  154. package/dist/index.js +13 -21
  155. package/dist/index.js.map +1 -1
  156. package/dist/knowledge/import.d.ts +2 -2
  157. package/dist/knowledge/import.d.ts.map +1 -1
  158. package/dist/knowledge/ingest.js.map +1 -1
  159. package/dist/knowledge/lifecycle-hooks.d.ts +2 -2
  160. package/dist/knowledge/lifecycle-hooks.d.ts.map +1 -1
  161. package/dist/knowledge/types.d.ts +5 -7
  162. package/dist/knowledge/types.d.ts.map +1 -1
  163. package/dist/knowledge/types.js +1 -1
  164. package/dist/knowledge/types.js.map +1 -1
  165. package/dist/monitoring/constraint-doctor.d.ts +4 -22
  166. package/dist/monitoring/constraint-doctor.d.ts.map +1 -1
  167. package/dist/monitoring/constraint-doctor.js +7 -94
  168. package/dist/monitoring/constraint-doctor.js.map +1 -1
  169. package/dist/presets/standard.d.ts +1 -1
  170. package/dist/presets/standard.d.ts.map +1 -1
  171. package/dist/presets/standard.js +1 -2
  172. package/dist/presets/standard.js.map +1 -1
  173. package/dist/spec/annotation-checker.d.ts.map +1 -1
  174. package/dist/spec/annotation-checker.js +6 -26
  175. package/dist/spec/annotation-checker.js.map +1 -1
  176. package/dist/tools/index.d.ts +0 -4
  177. package/dist/tools/index.d.ts.map +1 -1
  178. package/dist/tools/index.js +0 -4
  179. package/dist/tools/index.js.map +1 -1
  180. package/dist/types/constraint.d.ts +1 -12
  181. package/dist/types/constraint.d.ts.map +1 -1
  182. package/dist/types/constraint.js +0 -3
  183. package/dist/types/constraint.js.map +1 -1
  184. package/dist/types/project-config.d.ts +0 -2
  185. package/dist/types/project-config.d.ts.map +1 -1
  186. package/dist/utils/file-walk.d.ts +2 -2
  187. package/dist/utils/file-walk.js +2 -2
  188. package/package.json +9 -4
  189. package/src/CONTEXT.md +14 -0
  190. package/src/__tests__/checker-extra.test.ts +2 -59
  191. package/src/__tests__/constraint-doctor.test.ts +1 -140
  192. package/src/__tests__/constraint-handler.test.ts +0 -4
  193. package/src/__tests__/project-config-loader-extra.test.ts +0 -20
  194. package/src/__tests__/project-config-loader.test.ts +0 -1
  195. package/src/cli/commands/CONTEXT.md +15 -7
  196. package/src/cli/commands/__tests__/analyze-sessions.test.ts +178 -0
  197. package/src/cli/commands/__tests__/check-drift.test.ts +0 -3
  198. package/src/cli/commands/__tests__/check-skip-output.test.ts +0 -5
  199. package/src/cli/commands/__tests__/check.test.ts +7 -75
  200. package/src/cli/commands/__tests__/registry.test.ts +201 -0
  201. package/src/cli/commands/__tests__/release.test.ts +162 -0
  202. package/src/cli/commands/__tests__/update-user-model.test.ts +205 -0
  203. package/src/cli/commands/check.ts +2 -14
  204. package/src/cli/commands/definitions.ts +593 -0
  205. package/src/cli/commands/knowledge.ts +3 -3
  206. package/src/cli/commands/sync-docs/capabilities-syncer.ts +4 -110
  207. package/src/cli/commands/sync-docs/index.ts +8 -5
  208. package/src/cli/commands/update-user-model.ts +15 -5
  209. package/src/context/CONTEXT.md +2 -7
  210. package/src/context/__tests__/session-manager.test.ts +0 -91
  211. package/src/context/index.ts +1 -5
  212. package/src/context/knowledge-injector.ts +3 -3
  213. package/src/context/session-manager.ts +0 -74
  214. package/src/context/types.ts +2 -63
  215. package/src/core/CONTEXT.md +1 -1
  216. package/src/core/constraints/__tests__/agent-prompt-renderer.test.ts +129 -0
  217. package/src/core/constraints/__tests__/check-cache.test.ts +227 -0
  218. package/src/core/constraints/__tests__/interceptor.test.ts +0 -2
  219. package/src/core/constraints/agent-prompt-renderer.ts +71 -0
  220. package/src/core/constraints/capabilities-parser.ts +95 -2
  221. package/src/core/constraints/check-cache.ts +75 -3
  222. package/src/core/constraints/checker.ts +8 -53
  223. package/src/core/constraints/definitions.ts +0 -8
  224. package/src/core/constraints/doc-freshness/runner.ts +1 -2
  225. package/src/core/constraints/index.ts +11 -2
  226. package/src/core/constraints/interceptor.ts +1 -6
  227. package/src/core/effective-constraints.ts +0 -1
  228. package/src/core/project-config-loader.ts +1 -7
  229. package/src/dashboard/stats.ts +2 -2
  230. package/src/dashboard/types.ts +2 -2
  231. package/src/failure/constraint-handler.ts +0 -2
  232. package/src/gates/CONTEXT.md +16 -13
  233. package/src/gates/__tests__/checker-gate.test.ts +45 -0
  234. package/src/gates/__tests__/contract.test.ts +73 -0
  235. package/src/gates/__tests__/effective-gates.test.ts +82 -0
  236. package/src/gates/__tests__/gate-interface.test.ts +202 -0
  237. package/src/gates/__tests__/registry.test.ts +87 -0
  238. package/src/gates/__tests__/runner.test.ts +81 -0
  239. package/src/gates/acceptance.ts +24 -1
  240. package/src/gates/checker-gate.ts +54 -0
  241. package/src/gates/command.ts +13 -2
  242. package/src/gates/contract.ts +12 -2
  243. package/src/gates/decision.ts +27 -0
  244. package/src/gates/definitions.ts +207 -0
  245. package/src/gates/effective-gates.ts +106 -0
  246. package/src/gates/index.ts +38 -0
  247. package/src/gates/performance.ts +12 -2
  248. package/src/gates/registry.ts +107 -0
  249. package/src/gates/review.ts +12 -2
  250. package/src/gates/runner.ts +57 -0
  251. package/src/gates/security.ts +12 -2
  252. package/src/gates/types.ts +48 -1
  253. package/src/hooks/CONTEXT.md +12 -2
  254. package/src/hooks/__tests__/bootstrap.test.ts +0 -1
  255. package/src/hooks/__tests__/config.test.ts +19 -0
  256. package/src/hooks/__tests__/pipeline.test.ts +79 -0
  257. package/src/hooks/__tests__/registry.test.ts +67 -0
  258. package/src/hooks/bootstrap.ts +0 -2
  259. package/src/hooks/config.ts +29 -0
  260. package/src/hooks/index.ts +2 -1
  261. package/src/hooks/registry.ts +60 -1
  262. package/src/hooks/types.ts +21 -0
  263. package/src/index.ts +13 -22
  264. package/src/knowledge/import.ts +3 -3
  265. package/src/knowledge/ingest.ts +4 -4
  266. package/src/knowledge/lifecycle-hooks.ts +3 -3
  267. package/src/knowledge/types.ts +5 -8
  268. package/src/monitoring/constraint-doctor.ts +7 -106
  269. package/src/presets/__tests__/standard.test.ts +0 -2
  270. package/src/presets/standard.ts +1 -2
  271. package/src/spec/__tests__/annotation-checker.test.ts +9 -3
  272. package/src/spec/annotation-checker.ts +7 -29
  273. package/src/tools/CONTEXT.md +7 -14
  274. package/src/tools/index.ts +0 -4
  275. package/src/types/constraint.ts +1 -14
  276. package/src/types/project-config.ts +0 -3
  277. package/src/utils/file-walk.ts +2 -2
  278. package/dist/architecture/constraint-engine.d.ts +0 -67
  279. package/dist/architecture/constraint-engine.d.ts.map +0 -1
  280. package/dist/architecture/constraint-engine.js +0 -295
  281. package/dist/architecture/constraint-engine.js.map +0 -1
  282. package/dist/architecture/cross-project-checker.d.ts +0 -54
  283. package/dist/architecture/cross-project-checker.d.ts.map +0 -1
  284. package/dist/architecture/cross-project-checker.js +0 -467
  285. package/dist/architecture/cross-project-checker.js.map +0 -1
  286. package/dist/cli/commands/index.d.ts +0 -30
  287. package/dist/cli/commands/index.d.ts.map +0 -1
  288. package/dist/cli/commands/index.js +0 -91
  289. package/dist/cli/commands/index.js.map +0 -1
  290. package/dist/context/file-budget.d.ts +0 -40
  291. package/dist/context/file-budget.d.ts.map +0 -1
  292. package/dist/context/file-budget.js +0 -147
  293. package/dist/context/file-budget.js.map +0 -1
  294. package/dist/context/progressive-loader.d.ts +0 -109
  295. package/dist/context/progressive-loader.d.ts.map +0 -1
  296. package/dist/context/progressive-loader.js +0 -166
  297. package/dist/context/progressive-loader.js.map +0 -1
  298. package/dist/context/token-pipeline.d.ts +0 -55
  299. package/dist/context/token-pipeline.d.ts.map +0 -1
  300. package/dist/context/token-pipeline.js +0 -220
  301. package/dist/context/token-pipeline.js.map +0 -1
  302. package/dist/context/tool-output-budget.d.ts +0 -59
  303. package/dist/context/tool-output-budget.d.ts.map +0 -1
  304. package/dist/context/tool-output-budget.js +0 -177
  305. package/dist/context/tool-output-budget.js.map +0 -1
  306. package/dist/core/constraints/default-executors.d.ts +0 -10
  307. package/dist/core/constraints/default-executors.d.ts.map +0 -1
  308. package/dist/core/constraints/default-executors.js +0 -120
  309. package/dist/core/constraints/default-executors.js.map +0 -1
  310. package/dist/llm/adapter.d.ts +0 -36
  311. package/dist/llm/adapter.d.ts.map +0 -1
  312. package/dist/llm/adapter.js +0 -118
  313. package/dist/llm/adapter.js.map +0 -1
  314. package/dist/llm/index.d.ts +0 -3
  315. package/dist/llm/index.d.ts.map +0 -1
  316. package/dist/llm/index.js +0 -19
  317. package/dist/llm/index.js.map +0 -1
  318. package/dist/llm/types.d.ts +0 -62
  319. package/dist/llm/types.d.ts.map +0 -1
  320. package/dist/llm/types.js +0 -8
  321. package/dist/llm/types.js.map +0 -1
  322. package/dist/tools/core/index.d.ts +0 -13
  323. package/dist/tools/core/index.d.ts.map +0 -1
  324. package/dist/tools/core/index.js +0 -198
  325. package/dist/tools/core/index.js.map +0 -1
  326. package/dist/tools/loader.d.ts +0 -21
  327. package/dist/tools/loader.d.ts.map +0 -1
  328. package/dist/tools/loader.js +0 -199
  329. package/dist/tools/loader.js.map +0 -1
  330. package/dist/tools/registry.d.ts +0 -59
  331. package/dist/tools/registry.d.ts.map +0 -1
  332. package/dist/tools/registry.js +0 -115
  333. package/dist/tools/registry.js.map +0 -1
  334. package/dist/tools/types.d.ts +0 -38
  335. package/dist/tools/types.d.ts.map +0 -1
  336. package/dist/tools/types.js +0 -6
  337. package/dist/tools/types.js.map +0 -1
  338. package/src/__tests__/architecture-engine.test.ts +0 -619
  339. package/src/__tests__/context/progressive-loader.test.ts +0 -131
  340. package/src/__tests__/cross-project-checker.test.ts +0 -318
  341. package/src/architecture/CONTEXT.md +0 -22
  342. package/src/architecture/constraint-engine.ts +0 -351
  343. package/src/architecture/cross-project-checker.ts +0 -545
  344. package/src/cli/commands/index.ts +0 -30
  345. package/src/context/__tests__/file-budget.test.ts +0 -142
  346. package/src/context/__tests__/progressive-loader.test.ts +0 -264
  347. package/src/context/__tests__/token-pipeline.test.ts +0 -178
  348. package/src/context/__tests__/tool-output-budget.test.ts +0 -95
  349. package/src/context/file-budget.ts +0 -142
  350. package/src/context/progressive-loader.ts +0 -260
  351. package/src/context/token-pipeline.ts +0 -265
  352. package/src/context/tool-output-budget.ts +0 -178
  353. package/src/core/constraints/__tests__/default-executors.test.ts +0 -57
  354. package/src/core/constraints/default-executors.ts +0 -92
  355. package/src/llm/CONTEXT.md +0 -21
  356. package/src/llm/__tests__/adapter.test.ts +0 -172
  357. package/src/llm/adapter.ts +0 -149
  358. package/src/llm/index.ts +0 -2
  359. package/src/llm/types.ts +0 -81
  360. package/src/tools/__tests__/loader.test.ts +0 -252
  361. package/src/tools/__tests__/registry.test.ts +0 -157
  362. package/src/tools/core/__tests__/core.test.ts +0 -122
  363. package/src/tools/core/index.ts +0 -201
  364. package/src/tools/loader.ts +0 -216
  365. package/src/tools/registry.ts +0 -128
  366. package/src/tools/types.ts +0 -46
@@ -0,0 +1,178 @@
1
+ /**
2
+ * analyze-sessions 命令测试(O6)
3
+ *
4
+ * Seam:analyzeSessions(options) 公开入口。
5
+ * 隔离面:
6
+ * - os.homedir → 测试临时目录(memory 规则库落 TEST_HOME)
7
+ * - readTranscriptSessions → fixture 会话(extractCorrectionMatches/tokenize 等纯函数保持真实)
8
+ * - CLAUDE_TRANSCRIPTS_DIR → 测试临时目录(决定 transcripts 目录是否存在)
9
+ */
10
+
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { analyzeSessions } from '../analyze-sessions';
14
+ import { readTranscriptSessions, type MinedSession } from '../../session-mining';
15
+
16
+ const TEST_HOME = '/tmp/harness-analyze-test-home';
17
+
18
+ jest.mock('os', () => ({
19
+ ...jest.requireActual('os'),
20
+ homedir: () => '/tmp/harness-analyze-test-home',
21
+ }));
22
+
23
+ jest.mock('../../session-mining', () => ({
24
+ ...jest.requireActual('../../session-mining'),
25
+ readTranscriptSessions: jest.fn(),
26
+ }));
27
+
28
+ jest.mock('chalk', () => {
29
+ const id = (s: string) => s;
30
+ const chalkFn = Object.assign(id, {
31
+ gray: id,
32
+ blue: id,
33
+ green: id,
34
+ yellow: id,
35
+ cyan: id,
36
+ bold: id,
37
+ red: id,
38
+ });
39
+ return {
40
+ __esModule: true,
41
+ default: chalkFn,
42
+ gray: id,
43
+ blue: id,
44
+ green: id,
45
+ yellow: id,
46
+ cyan: id,
47
+ bold: id,
48
+ red: id,
49
+ };
50
+ });
51
+
52
+ const mockReadTranscriptSessions = readTranscriptSessions as jest.MockedFunction<
53
+ typeof readTranscriptSessions
54
+ >;
55
+
56
+ const TRANSCRIPTS_DIR = path.join(TEST_HOME, 'transcripts');
57
+
58
+ function mkSession(partial: Partial<MinedSession> = {}): MinedSession {
59
+ return {
60
+ id: 'session-1',
61
+ date: '2026-08-15',
62
+ mtimeMs: Date.now(),
63
+ turns: [
64
+ { role: 'user', content: '我不是说要用中文吗' },
65
+ { role: 'assistant', content: '收到,改用中文' },
66
+ ],
67
+ toolCalls: [],
68
+ ...partial,
69
+ };
70
+ }
71
+
72
+ function lastJsonOutput(consoleSpy: jest.SpyInstance): Record<string, unknown> {
73
+ const jsonLine = consoleSpy.mock.calls.map(c => c[0]).join('\n');
74
+ return JSON.parse(jsonLine);
75
+ }
76
+
77
+ describe('analyze-sessions command', () => {
78
+ let consoleSpy: jest.SpyInstance;
79
+
80
+ beforeEach(() => {
81
+ jest.clearAllMocks();
82
+ fs.rmSync(TEST_HOME, { recursive: true, force: true });
83
+ fs.mkdirSync(path.join(TEST_HOME, '.claude', 'projects', '-root-projects', 'memory'), { recursive: true });
84
+ fs.mkdirSync(TRANSCRIPTS_DIR, { recursive: true });
85
+ process.env.CLAUDE_TRANSCRIPTS_DIR = TRANSCRIPTS_DIR;
86
+ consoleSpy = jest.spyOn(console, 'log').mockImplementation();
87
+ });
88
+
89
+ afterEach(() => {
90
+ consoleSpy.mockRestore();
91
+ delete process.env.CLAUDE_TRANSCRIPTS_DIR;
92
+ });
93
+
94
+ test('transcripts 目录不存在:提示 No transcripts directory found', async () => {
95
+ process.env.CLAUDE_TRANSCRIPTS_DIR = path.join(TEST_HOME, 'missing-dir');
96
+
97
+ await analyzeSessions({});
98
+
99
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No transcripts directory found'));
100
+ expect(mockReadTranscriptSessions).not.toHaveBeenCalled();
101
+ });
102
+
103
+ test('窗口内无会话:提示 No sessions found(默认最近 7 天)', async () => {
104
+ mockReadTranscriptSessions.mockReturnValue([
105
+ mkSession({ id: 'old', mtimeMs: Date.now() - 30 * 86_400_000 }),
106
+ ]);
107
+
108
+ await analyzeSessions({});
109
+
110
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No sessions found in the last 7 days'));
111
+ });
112
+
113
+ test('--days 1:只统计最近 1 天(mtimeMs 窗口过滤)', async () => {
114
+ mockReadTranscriptSessions.mockReturnValue([
115
+ mkSession({ id: 'today' }),
116
+ mkSession({ id: 'two-days-ago', mtimeMs: Date.now() - 2 * 86_400_000 }),
117
+ ]);
118
+
119
+ await analyzeSessions({ days: 1 });
120
+
121
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Analyzing 1 sessions (last 1 days)'));
122
+ });
123
+
124
+ test('缺省 --days:窗口为 7 天,两天前会话仍计入', async () => {
125
+ mockReadTranscriptSessions.mockReturnValue([
126
+ mkSession({ id: 'today' }),
127
+ mkSession({ id: 'two-days-ago', mtimeMs: Date.now() - 2 * 86_400_000 }),
128
+ ]);
129
+
130
+ await analyzeSessions({});
131
+
132
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Analyzing 2 sessions (last 7 days)'));
133
+ });
134
+
135
+ test('--json:纠正句跨 3 会话聚合为 correction 候选', async () => {
136
+ mockReadTranscriptSessions.mockReturnValue([
137
+ mkSession({ id: 's1' }),
138
+ mkSession({ id: 's2' }),
139
+ mkSession({ id: 's3' }),
140
+ ]);
141
+
142
+ await analyzeSessions({ json: true });
143
+
144
+ const output = lastJsonOutput(consoleSpy);
145
+ expect(output.sessions).toBe(3);
146
+ expect(output.corrections).toBe(3);
147
+ expect(output.candidates).toEqual(
148
+ expect.arrayContaining([
149
+ expect.objectContaining({
150
+ source: 'correction',
151
+ frequency: 3,
152
+ pattern: '我不是说',
153
+ }),
154
+ ]),
155
+ );
156
+ });
157
+
158
+ test('--json:跨会话重复概念产出 ngram 候选', async () => {
159
+ const ngramText = { role: 'user', content: '数据库迁移方案的详细讨论' };
160
+ mockReadTranscriptSessions.mockReturnValue([
161
+ mkSession({
162
+ id: 's1',
163
+ turns: [ngramText, { role: 'assistant', content: '好的' }],
164
+ }),
165
+ mkSession({
166
+ id: 's2',
167
+ turns: [ngramText, { role: 'assistant', content: '明白' }],
168
+ }),
169
+ ]);
170
+
171
+ await analyzeSessions({ json: true });
172
+
173
+ const output = lastJsonOutput(consoleSpy);
174
+ const ngramCandidates = (output.candidates as Array<{ source: string }>)
175
+ .filter(c => c.source === 'ngram');
176
+ expect(ngramCandidates.length).toBeGreaterThan(0);
177
+ });
178
+ });
@@ -16,7 +16,6 @@ import { getEffectiveConstraints } from '../../../core/effective-constraints';
16
16
 
17
17
  jest.mock('../../../core/constraints/checker', () => ({
18
18
  constraintChecker: {
19
- setCustomConfig: jest.fn(),
20
19
  setTraceRecorder: jest.fn(),
21
20
  checkConstraints: jest.fn(),
22
21
  },
@@ -58,9 +57,7 @@ describe('check 命令注入漂移警告', () => {
58
57
  passed: true,
59
58
  ironLaws: [],
60
59
  guidelines: [],
61
- tips: [],
62
60
  warningCount: 0,
63
- tipCount: 0,
64
61
  });
65
62
  });
66
63
 
@@ -29,7 +29,6 @@ jest.mock('child_process', () => ({
29
29
  // Mock constraintChecker
30
30
  jest.mock('../../../core/constraints/checker', () => ({
31
31
  constraintChecker: {
32
- setCustomConfig: jest.fn(),
33
32
  setTraceRecorder: jest.fn(),
34
33
  checkConstraints: jest.fn(),
35
34
  },
@@ -89,9 +88,7 @@ describe('check 命令 skip 输出', () => {
89
88
  { id: 'law_skip', level: 'iron_law', satisfied: true, skipped: true, checkedAt: new Date(), constraint: fakeConstraint('law_skip', 'iron_law') },
90
89
  ],
91
90
  guidelines: [],
92
- tips: [],
93
91
  warningCount: 0,
94
- tipCount: 0,
95
92
  });
96
93
 
97
94
  await check({ preset: 'default', staged: false });
@@ -113,9 +110,7 @@ describe('check 命令 skip 输出', () => {
113
110
  { id: 'g_pass', level: 'guideline', satisfied: true, checkedAt: new Date(), constraint: fakeConstraint('g_pass', 'guideline') },
114
111
  { id: 'g_skip', level: 'guideline', satisfied: true, skipped: true, checkedAt: new Date(), constraint: fakeConstraint('g_skip', 'guideline') },
115
112
  ],
116
- tips: [],
117
113
  warningCount: 0,
118
- tipCount: 0,
119
114
  });
120
115
 
121
116
  await check({ preset: 'default', staged: false });
@@ -40,7 +40,6 @@ jest.mock('child_process', () => ({
40
40
  // Mock constraintChecker
41
41
  jest.mock('../../../core/constraints/checker', () => ({
42
42
  constraintChecker: {
43
- setCustomConfig: jest.fn(),
44
43
  setTraceRecorder: jest.fn(),
45
44
  checkConstraints: jest.fn(),
46
45
  },
@@ -89,9 +88,7 @@ describe('check command', () => {
89
88
  passed: true,
90
89
  ironLaws: [{ id: 'test', level: 'iron_law', satisfied: true, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'test', rule: 'test', message: 'test', level: 'iron_law', trigger: 'code_implementation', enforcement: 'checkpoint-required' } }],
91
90
  guidelines: [],
92
- tips: [],
93
91
  warningCount: 0,
94
- tipCount: 0,
95
92
  });
96
93
 
97
94
  await check({ preset: 'default', staged: false });
@@ -103,9 +100,7 @@ describe('check command', () => {
103
100
  passed: false,
104
101
  ironLaws: [{ id: 'no_bypass_checkpoint', level: 'iron_law', satisfied: false, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'no_bypass_checkpoint', rule: 'test', message: 'test', level: 'iron_law', trigger: 'code_implementation', enforcement: 'checkpoint-required' } }],
105
102
  guidelines: [],
106
- tips: [],
107
103
  warningCount: 0,
108
- tipCount: 0,
109
104
  });
110
105
 
111
106
  const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
@@ -120,9 +115,7 @@ describe('check command', () => {
120
115
  passed: false,
121
116
  ironLaws: [],
122
117
  guidelines: [{ id: 'test_guideline', level: 'guideline', satisfied: false, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'test_guideline', rule: 'test', message: 'test', level: 'guideline', trigger: 'code_implementation', enforcement: 'warning' } }],
123
- tips: [],
124
118
  warningCount: 1,
125
- tipCount: 0,
126
119
  });
127
120
 
128
121
  await check({ preset: 'default', staged: false });
@@ -141,38 +134,23 @@ describe('check command', () => {
141
134
  passed: true,
142
135
  ironLaws: [],
143
136
  guidelines: [],
144
- tips: [],
145
137
  warningCount: 0,
146
- tipCount: 0,
147
138
  });
148
139
 
149
140
  await check({ preset: 'default', staged: false, projectPath: '/project' });
150
- expect(mockChecker.setCustomConfig).toHaveBeenCalled();
141
+ expect(mockChecker.checkConstraints).toHaveBeenCalledWith(
142
+ expect.anything(),
143
+ expect.objectContaining({ disabled: ['disabled_constraint'] })
144
+ );
151
145
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('已禁用约束'));
152
146
  });
153
147
 
154
- it('应该显示提示信息', async () => {
155
- mockChecker.checkConstraints.mockResolvedValue({
156
- passed: true,
157
- ironLaws: [],
158
- guidelines: [],
159
- tips: [{ id: 'test_tip', level: 'tip', satisfied: false, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'test_tip', rule: 'test', message: 'test tip', level: 'tip', trigger: 'code_implementation', enforcement: 'info' } }],
160
- warningCount: 0,
161
- tipCount: 1,
162
- });
163
-
164
- await check({ preset: 'default', staged: false });
165
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('提示'));
166
- });
167
-
168
148
  it('应该显示通过的指导原则', async () => {
169
149
  mockChecker.checkConstraints.mockResolvedValue({
170
150
  passed: true,
171
151
  ironLaws: [],
172
152
  guidelines: [{ id: 'test_guideline', level: 'guideline', satisfied: true, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'test_guideline', rule: 'test', message: 'test', level: 'guideline', trigger: 'code_implementation', enforcement: 'warning' } }],
173
- tips: [],
174
153
  warningCount: 0,
175
- tipCount: 0,
176
154
  });
177
155
 
178
156
  await check({ preset: 'default', staged: false });
@@ -186,9 +164,7 @@ describe('check command', () => {
186
164
  passed: true,
187
165
  ironLaws: [],
188
166
  guidelines: [],
189
- tips: [],
190
167
  warningCount: 0,
191
- tipCount: 0,
192
168
  });
193
169
 
194
170
  await check({ preset: 'default', staged: true });
@@ -201,9 +177,7 @@ describe('check command', () => {
201
177
  passed: true,
202
178
  ironLaws: [],
203
179
  guidelines: [],
204
- tips: [],
205
180
  warningCount: 0,
206
- tipCount: 0,
207
181
  });
208
182
 
209
183
  await check({ preset: 'default', staged: false });
@@ -216,9 +190,7 @@ describe('check command', () => {
216
190
  passed: true,
217
191
  ironLaws: [],
218
192
  guidelines: [],
219
- tips: [],
220
193
  warningCount: 0,
221
- tipCount: 0,
222
194
  });
223
195
 
224
196
  await check({ preset: 'default', staged: true });
@@ -231,9 +203,7 @@ describe('check command', () => {
231
203
  passed: true,
232
204
  ironLaws: [],
233
205
  guidelines: [],
234
- tips: [],
235
206
  warningCount: 0,
236
- tipCount: 0,
237
207
  });
238
208
 
239
209
  await check({ preset: 'default', staged: false });
@@ -244,7 +214,7 @@ describe('check command', () => {
244
214
  it('应该检测 module_modification 触发条件(.ts 文件)', async () => {
245
215
  mockExecAsync.mockResolvedValue({ stdout: 'src/foo.ts\nsrc/bar.tsx\n', stderr: '' });
246
216
  mockChecker.checkConstraints.mockResolvedValue({
247
- passed: true, ironLaws: [], guidelines: [], tips: [], warningCount: 0, tipCount: 0,
217
+ passed: true, ironLaws: [], guidelines: [], warningCount: 0,
248
218
  });
249
219
 
250
220
  // Mock filesystem so detectSourceRoots finds src/ as a source root
@@ -267,7 +237,7 @@ describe('check command', () => {
267
237
  it('应该检测 module_modification 触发条件(.js 文件)', async () => {
268
238
  mockExecAsync.mockResolvedValue({ stdout: 'src/foo.js\nsrc/bar.jsx\n', stderr: '' });
269
239
  mockChecker.checkConstraints.mockResolvedValue({
270
- passed: true, ironLaws: [], guidelines: [], tips: [], warningCount: 0, tipCount: 0,
240
+ passed: true, ironLaws: [], guidelines: [], warningCount: 0,
271
241
  });
272
242
 
273
243
  // Mock filesystem so detectSourceRoots finds src/ as a source root
@@ -290,7 +260,7 @@ describe('check command', () => {
290
260
  it('应该检测 file_modification 触发条件(非 src/ 目录)', async () => {
291
261
  mockExecAsync.mockResolvedValue({ stdout: 'docs/bar.md\n', stderr: '' });
292
262
  mockChecker.checkConstraints.mockResolvedValue({
293
- passed: true, ironLaws: [], guidelines: [], tips: [], warningCount: 0, tipCount: 0,
263
+ passed: true, ironLaws: [], guidelines: [], warningCount: 0,
294
264
  });
295
265
 
296
266
  await check({ preset: 'default', staged: false });
@@ -304,9 +274,7 @@ describe('check command', () => {
304
274
  passed: true,
305
275
  ironLaws: [],
306
276
  guidelines: [],
307
- tips: [],
308
277
  warningCount: 0,
309
- tipCount: 0,
310
278
  });
311
279
 
312
280
  // Mock filesystem so detectSourceRoots finds src/ as a source root
@@ -332,9 +300,7 @@ describe('check command', () => {
332
300
  passed: true,
333
301
  ironLaws: [],
334
302
  guidelines: [],
335
- tips: [],
336
303
  warningCount: 0,
337
- tipCount: 0,
338
304
  });
339
305
 
340
306
  await check({ preset: 'default', staged: false, trigger: 'code_implementation' });
@@ -361,9 +327,7 @@ describe('check command', () => {
361
327
  },
362
328
  }],
363
329
  guidelines: [],
364
- tips: [],
365
330
  warningCount: 0,
366
- tipCount: 0,
367
331
  });
368
332
 
369
333
  const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
@@ -393,43 +357,13 @@ describe('check command', () => {
393
357
  enforcement: 'warning',
394
358
  },
395
359
  }],
396
- tips: [],
397
360
  warningCount: 1,
398
- tipCount: 0,
399
361
  });
400
362
 
401
363
  await check({ preset: 'default', staged: false });
402
364
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('prefer_composition'));
403
365
  });
404
366
 
405
- it('应该显示提示的约束详情', async () => {
406
- mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' });
407
- mockChecker.checkConstraints.mockResolvedValue({
408
- passed: true,
409
- ironLaws: [],
410
- guidelines: [],
411
- tips: [{
412
- id: 'consider_reuse',
413
- level: 'tip',
414
- satisfied: false,
415
- checkedAt: new Date(),
416
- constraint: {
417
- kind: 'check' as const,
418
- id: 'consider_reuse',
419
- rule: 'CONSIDER REUSE',
420
- message: '考虑复用',
421
- level: 'tip',
422
- trigger: 'code_implementation',
423
- enforcement: 'info',
424
- },
425
- }],
426
- warningCount: 0,
427
- tipCount: 1,
428
- });
429
-
430
- await check({ preset: 'default', staged: false });
431
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('consider_reuse'));
432
- });
433
367
  });
434
368
 
435
369
  describe('getSmartHint (通过 check 间接测试)', () => {
@@ -439,9 +373,7 @@ describe('check command', () => {
439
373
  passed: true,
440
374
  ironLaws: [],
441
375
  guidelines: [],
442
- tips: [],
443
376
  warningCount: 0,
444
- tipCount: 0,
445
377
  });
446
378
  });
447
379
 
@@ -0,0 +1,201 @@
1
+ /**
2
+ * 命令注册表闭环测试(H5:命令定义即注册 + per-command 懒加载)
3
+ *
4
+ * - 定义表内每个实现引用(module+export)都必须可解析(构建/测试期断言,
5
+ * 取代 commands barrel 的编译期保护——R6 消灭手工同步后的闭环兜底)
6
+ * - definitions 是纯数据模块:require 它不加载任何命令实现(保 --help/--version 懒加载)
7
+ * - 命令面与 CAPABILITIES.md 的 24 顶层命令一致(防误删回归)
8
+ * - bin/harness.js 端到端冒烟(dist 存在时):--version/--help 零命令实现模块,
9
+ * 单命令执行只加载该命令模块(O2 per-command 懒加载)
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+ import * as os from 'os';
14
+ import * as path from 'path';
15
+ import { spawnSync } from 'child_process';
16
+ import { COMMAND_DEFINITIONS, type CommandDefinition, type CommandImplRef } from '../definitions';
17
+ import { GATE_DEFINITIONS } from '../../../gates/definitions';
18
+
19
+ function collectRefs(defs: CommandDefinition[]): CommandImplRef[] {
20
+ const refs: CommandImplRef[] = [];
21
+ const walk = (def: CommandDefinition) => {
22
+ if (def.action) refs.push(def.action);
23
+ if (def.subcommands) {
24
+ for (const entry of Object.values(def.subcommands)) refs.push(entry.impl);
25
+ }
26
+ if (def.optionRoutes) {
27
+ for (const route of def.optionRoutes) refs.push(route.impl);
28
+ }
29
+ for (const child of def.children || []) walk(child);
30
+ };
31
+ for (const def of defs) walk(def);
32
+ return refs;
33
+ }
34
+
35
+ const EXPECTED_TOP_LEVEL_COMMANDS = [
36
+ 'check', 'validate', 'passes-gate', 'init', 'report', 'status', 'spec',
37
+ 'sync-docs', 'knowledge', 'sdd', 'failure', 'posteval-plan',
38
+ 'update-user-model', 'release', 'analyze-sessions', 'constraints',
39
+ 'doc-freshness-check', 'spec-baseline-check',
40
+ // 6 门禁命令(GATE_DEFINITIONS 驱动)
41
+ 'acceptance', 'command', 'contract', 'performance', 'review', 'security',
42
+ ];
43
+
44
+ describe('命令注册表闭环', () => {
45
+ it('definitions 是纯数据模块:加载时不引入任何命令实现', () => {
46
+ const loaded = Object.keys(require.cache)
47
+ .filter(k => k.includes('/cli/commands/') && k.endsWith('.ts') && !k.includes('__tests__'));
48
+ expect(loaded).toEqual([
49
+ expect.stringContaining('/cli/commands/definitions.ts'),
50
+ ]);
51
+ });
52
+
53
+ it('全部实现引用可解析(module 存在且 export 是函数)', () => {
54
+ const refs = collectRefs(COMMAND_DEFINITIONS);
55
+ for (const def of GATE_DEFINITIONS) {
56
+ refs.push(def.cli.action);
57
+ for (const entry of Object.values(def.cli.subcommands || {})) refs.push(entry);
58
+ }
59
+ expect(refs.length).toBeGreaterThan(0);
60
+
61
+ for (const ref of refs) {
62
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
63
+ const mod = require('../' + ref.module);
64
+ if (typeof mod[ref.export] !== 'function') {
65
+ throw new Error(`实现引用 ${ref.module}.${ref.export} 不存在或不是函数`);
66
+ }
67
+ }
68
+ });
69
+
70
+ it('顶层命令面与预期 24 命令一致(含 6 门禁)', () => {
71
+ const names = [
72
+ ...COMMAND_DEFINITIONS.map(d => d.command.split(' ')[0]),
73
+ ...GATE_DEFINITIONS.map(d => d.cli.command.split(' ')[0]),
74
+ ];
75
+ expect(new Set(names).size).toBe(names.length);
76
+ expect(names.sort()).toEqual([...EXPECTED_TOP_LEVEL_COMMANDS].sort());
77
+ });
78
+
79
+ it('命令定义无重复注册名', () => {
80
+ const names = COMMAND_DEFINITIONS.map(d => d.command);
81
+ expect(new Set(names).size).toBe(names.length);
82
+ });
83
+
84
+ it('全部命令定义的路由构造可执行(mapActionArgs/subcommands.args/optionRoutes.args/afterRun 冒烟)', () => {
85
+ const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
86
+ try {
87
+ const walk = (def: CommandDefinition) => {
88
+ if (def.mapActionArgs) {
89
+ def.mapActionArgs(['positional'], {});
90
+ def.mapActionArgs([], {});
91
+ }
92
+ for (const entry of Object.values(def.subcommands || {})) {
93
+ if (entry.args) {
94
+ entry.args(['positional'], {});
95
+ entry.args([], {});
96
+ }
97
+ }
98
+ for (const route of def.optionRoutes || []) {
99
+ if (route.args) {
100
+ route.args([], {});
101
+ route.args(['positional'], {});
102
+ }
103
+ }
104
+ if (def.afterRun) {
105
+ def.afterRun(true, {});
106
+ def.afterRun(false, { check: true });
107
+ }
108
+ for (const child of def.children || []) walk(child);
109
+ };
110
+ for (const def of COMMAND_DEFINITIONS) walk(def);
111
+ // sync-docs --check 失败路径确实会请求退出
112
+ expect(exitSpy).toHaveBeenCalled();
113
+ } finally {
114
+ exitSpy.mockRestore();
115
+ }
116
+ });
117
+ });
118
+
119
+ const repoRoot = path.join(__dirname, '..', '..', '..', '..');
120
+ const distBin = path.join(repoRoot, 'bin', 'harness.js');
121
+ const distCommands = path.join(repoRoot, 'dist', 'cli', 'commands');
122
+ const hasDist = fs.existsSync(distCommands);
123
+
124
+ /**
125
+ * 经 NODE_OPTIONS=--require 预加载探针 spawn bin(进程退出时打印加载的
126
+ * 命令实现模块清单)。不用 node -e(commander 在 -e 下走 eval 分支,
127
+ * argv 解析不同,会误判位置参数)。
128
+ */
129
+ function runWithModuleProbe(argv: string[]): { status: number | null; implModules: string[]; stdout: string; stderr: string } {
130
+ const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-lazy-probe-'));
131
+ const probeFile = path.join(probeDir, 'probe.js');
132
+ fs.writeFileSync(probeFile, [
133
+ `process.on('exit', () => {`,
134
+ ` const impls = Object.keys(require.cache).filter(k => /dist[\\\\/]cli[\\\\/]commands[\\\\/]/.test(k) && !/definitions\\.js/.test(k)).sort();`,
135
+ ` console.error('IMPL_MODULES=' + impls.join(','));`,
136
+ `});`,
137
+ ].join('\n'));
138
+ try {
139
+ const r = spawnSync(process.execPath, [distBin, ...argv], {
140
+ cwd: repoRoot,
141
+ encoding: 'utf-8',
142
+ timeout: 30000,
143
+ env: { ...process.env, NODE_OPTIONS: `--require ${probeFile}` },
144
+ });
145
+ const match = r.stderr.match(/IMPL_MODULES=(.*)/);
146
+ return {
147
+ status: r.status,
148
+ implModules: match && match[1] ? match[1].split(',').filter(Boolean) : [],
149
+ stdout: r.stdout,
150
+ stderr: r.stderr,
151
+ };
152
+ } finally {
153
+ fs.rmSync(probeDir, { recursive: true, force: true });
154
+ }
155
+ }
156
+
157
+ const smoke = hasDist ? describe : describe.skip;
158
+
159
+ smoke('bin/harness.js 端到端(dist 存在时)', () => {
160
+ it('--version:退出 0,零命令实现模块加载', () => {
161
+ const r = runWithModuleProbe(['--version']);
162
+ expect(r.status).toBe(0);
163
+ expect(r.implModules).toEqual([]);
164
+ });
165
+
166
+ it('--help:退出 0,零命令实现模块加载', () => {
167
+ const r = runWithModuleProbe(['--help']);
168
+ expect(r.status).toBe(0);
169
+ expect(r.implModules).toEqual([]);
170
+ });
171
+
172
+ it('harness constraints --json:只加载 constraints 命令模块', () => {
173
+ const r = runWithModuleProbe(['constraints', '--json']);
174
+ expect(r.status).toBe(0);
175
+ expect(r.implModules).toEqual([
176
+ expect.stringContaining('/dist/cli/commands/constraints.js'),
177
+ ]);
178
+ });
179
+
180
+ it('harness check --list:只加载 check 命令模块(optionRoutes)', () => {
181
+ const r = runWithModuleProbe(['check', '--list']);
182
+ expect(r.status).toBe(0);
183
+ expect(r.implModules).toEqual([
184
+ expect.stringContaining('/dist/cli/commands/check.js'),
185
+ ]);
186
+ });
187
+
188
+ it('harness acceptance:只加载 acceptance 命令模块(门禁注册表)', () => {
189
+ const r = runWithModuleProbe(['acceptance', '--check-all']);
190
+ expect(r.status).toBe(0);
191
+ expect(r.implModules).toEqual([
192
+ expect.stringContaining('/dist/cli/commands/acceptance.js'),
193
+ ]);
194
+ });
195
+
196
+ it('未知子命令报错退出(knowledge 兜底行为保留)', () => {
197
+ const r = runWithModuleProbe(['knowledge', 'bogus']);
198
+ expect(r.status).toBe(1);
199
+ expect(r.stderr).toContain('未知子命令: bogus');
200
+ });
201
+ });