@dommaker/harness 1.3.0 → 1.4.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 (517) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +0 -2
  3. package/bin/harness.js +86 -32
  4. package/dist/cli/command-contract.d.ts +83 -0
  5. package/dist/cli/command-contract.d.ts.map +1 -0
  6. package/dist/cli/command-contract.js +66 -0
  7. package/dist/cli/command-contract.js.map +1 -0
  8. package/dist/cli/commands/acceptance.d.ts +3 -2
  9. package/dist/cli/commands/acceptance.d.ts.map +1 -1
  10. package/dist/cli/commands/acceptance.js +28 -24
  11. package/dist/cli/commands/acceptance.js.map +1 -1
  12. package/dist/cli/commands/analyze-sessions.d.ts +2 -1
  13. package/dist/cli/commands/analyze-sessions.d.ts.map +1 -1
  14. package/dist/cli/commands/analyze-sessions.js +25 -24
  15. package/dist/cli/commands/analyze-sessions.js.map +1 -1
  16. package/dist/cli/commands/check.d.ts +14 -2
  17. package/dist/cli/commands/check.d.ts.map +1 -1
  18. package/dist/cli/commands/check.js +76 -61
  19. package/dist/cli/commands/check.js.map +1 -1
  20. package/dist/cli/commands/command.d.ts +5 -1
  21. package/dist/cli/commands/command.d.ts.map +1 -1
  22. package/dist/cli/commands/command.js +26 -17
  23. package/dist/cli/commands/command.js.map +1 -1
  24. package/dist/cli/commands/constraints-report.d.ts +2 -1
  25. package/dist/cli/commands/constraints-report.d.ts.map +1 -1
  26. package/dist/cli/commands/constraints-report.js +37 -33
  27. package/dist/cli/commands/constraints-report.js.map +1 -1
  28. package/dist/cli/commands/constraints-retire.d.ts +6 -5
  29. package/dist/cli/commands/constraints-retire.d.ts.map +1 -1
  30. package/dist/cli/commands/constraints-retire.js +50 -52
  31. package/dist/cli/commands/constraints-retire.js.map +1 -1
  32. package/dist/cli/commands/constraints.d.ts +2 -1
  33. package/dist/cli/commands/constraints.d.ts.map +1 -1
  34. package/dist/cli/commands/constraints.js +10 -27
  35. package/dist/cli/commands/constraints.js.map +1 -1
  36. package/dist/cli/commands/contract.d.ts +3 -2
  37. package/dist/cli/commands/contract.d.ts.map +1 -1
  38. package/dist/cli/commands/contract.js +45 -42
  39. package/dist/cli/commands/contract.js.map +1 -1
  40. package/dist/cli/commands/definitions.d.ts +11 -11
  41. package/dist/cli/commands/definitions.d.ts.map +1 -1
  42. package/dist/cli/commands/definitions.js +29 -214
  43. package/dist/cli/commands/definitions.js.map +1 -1
  44. package/dist/cli/commands/failure.d.ts +5 -4
  45. package/dist/cli/commands/failure.d.ts.map +1 -1
  46. package/dist/cli/commands/failure.js +31 -25
  47. package/dist/cli/commands/failure.js.map +1 -1
  48. package/dist/cli/commands/init.d.ts +7 -5
  49. package/dist/cli/commands/init.d.ts.map +1 -1
  50. package/dist/cli/commands/init.js +147 -211
  51. package/dist/cli/commands/init.js.map +1 -1
  52. package/dist/cli/commands/knowledge.d.ts +21 -15
  53. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  54. package/dist/cli/commands/knowledge.js +214 -199
  55. package/dist/cli/commands/knowledge.js.map +1 -1
  56. package/dist/cli/commands/passes-gate.d.ts +8 -2
  57. package/dist/cli/commands/passes-gate.d.ts.map +1 -1
  58. package/dist/cli/commands/passes-gate.js +44 -66
  59. package/dist/cli/commands/passes-gate.js.map +1 -1
  60. package/dist/cli/commands/performance.d.ts +2 -1
  61. package/dist/cli/commands/performance.d.ts.map +1 -1
  62. package/dist/cli/commands/performance.js +20 -18
  63. package/dist/cli/commands/performance.js.map +1 -1
  64. package/dist/cli/commands/posteval-plan.d.ts +5 -1
  65. package/dist/cli/commands/posteval-plan.d.ts.map +1 -1
  66. package/dist/cli/commands/posteval-plan.js +26 -15
  67. package/dist/cli/commands/posteval-plan.js.map +1 -1
  68. package/dist/cli/commands/release.d.ts +6 -1
  69. package/dist/cli/commands/release.d.ts.map +1 -1
  70. package/dist/cli/commands/release.js +69 -59
  71. package/dist/cli/commands/release.js.map +1 -1
  72. package/dist/cli/commands/report.d.ts +2 -1
  73. package/dist/cli/commands/report.d.ts.map +1 -1
  74. package/dist/cli/commands/report.js +9 -5
  75. package/dist/cli/commands/report.js.map +1 -1
  76. package/dist/cli/commands/review.d.ts +3 -2
  77. package/dist/cli/commands/review.d.ts.map +1 -1
  78. package/dist/cli/commands/review.js +33 -29
  79. package/dist/cli/commands/review.js.map +1 -1
  80. package/dist/cli/commands/sdd.d.ts +2 -1
  81. package/dist/cli/commands/sdd.d.ts.map +1 -1
  82. package/dist/cli/commands/sdd.js +7 -5
  83. package/dist/cli/commands/sdd.js.map +1 -1
  84. package/dist/cli/commands/security.d.ts +3 -2
  85. package/dist/cli/commands/security.d.ts.map +1 -1
  86. package/dist/cli/commands/security.js +29 -26
  87. package/dist/cli/commands/security.js.map +1 -1
  88. package/dist/cli/commands/spec-baseline-check.d.ts +2 -1
  89. package/dist/cli/commands/spec-baseline-check.d.ts.map +1 -1
  90. package/dist/cli/commands/spec-baseline-check.js +15 -11
  91. package/dist/cli/commands/spec-baseline-check.js.map +1 -1
  92. package/dist/cli/commands/spec.d.ts +7 -2
  93. package/dist/cli/commands/spec.d.ts.map +1 -1
  94. package/dist/cli/commands/spec.js +61 -51
  95. package/dist/cli/commands/spec.js.map +1 -1
  96. package/dist/cli/commands/status.d.ts +2 -1
  97. package/dist/cli/commands/status.d.ts.map +1 -1
  98. package/dist/cli/commands/status.js +57 -53
  99. package/dist/cli/commands/status.js.map +1 -1
  100. package/dist/cli/commands/sync-docs/agents-syncer.d.ts.map +1 -1
  101. package/dist/cli/commands/sync-docs/agents-syncer.js +5 -2
  102. package/dist/cli/commands/sync-docs/agents-syncer.js.map +1 -1
  103. package/dist/cli/commands/sync-docs/context-syncer.d.ts.map +1 -1
  104. package/dist/cli/commands/sync-docs/context-syncer.js +3 -2
  105. package/dist/cli/commands/sync-docs/context-syncer.js.map +1 -1
  106. package/dist/cli/commands/sync-docs/index.d.ts +2 -1
  107. package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
  108. package/dist/cli/commands/sync-docs/index.js +68 -88
  109. package/dist/cli/commands/sync-docs/index.js.map +1 -1
  110. package/dist/cli/commands/sync-docs/project-reader.d.ts +6 -1
  111. package/dist/cli/commands/sync-docs/project-reader.d.ts.map +1 -1
  112. package/dist/cli/commands/sync-docs/project-reader.js +14 -16
  113. package/dist/cli/commands/sync-docs/project-reader.js.map +1 -1
  114. package/dist/cli/commands/update-user-model.d.ts +2 -1
  115. package/dist/cli/commands/update-user-model.d.ts.map +1 -1
  116. package/dist/cli/commands/update-user-model.js +31 -45
  117. package/dist/cli/commands/update-user-model.js.map +1 -1
  118. package/dist/cli/commands/validate.d.ts +4 -3
  119. package/dist/cli/commands/validate.d.ts.map +1 -1
  120. package/dist/cli/commands/validate.js +28 -26
  121. package/dist/cli/commands/validate.js.map +1 -1
  122. package/dist/cli/session-mining/transcript.d.ts +7 -1
  123. package/dist/cli/session-mining/transcript.d.ts.map +1 -1
  124. package/dist/cli/session-mining/transcript.js +21 -14
  125. package/dist/cli/session-mining/transcript.js.map +1 -1
  126. package/dist/context/session-manager.d.ts.map +1 -1
  127. package/dist/context/session-manager.js +19 -32
  128. package/dist/context/session-manager.js.map +1 -1
  129. package/dist/core/constraints/agent-prompt-renderer.d.ts.map +1 -1
  130. package/dist/core/constraints/agent-prompt-renderer.js +1 -4
  131. package/dist/core/constraints/agent-prompt-renderer.js.map +1 -1
  132. package/dist/core/constraints/capabilities-parser.d.ts +24 -7
  133. package/dist/core/constraints/capabilities-parser.d.ts.map +1 -1
  134. package/dist/core/constraints/capabilities-parser.js +32 -28
  135. package/dist/core/constraints/capabilities-parser.js.map +1 -1
  136. package/dist/core/constraints/capabilities-reconcile.d.ts +67 -0
  137. package/dist/core/constraints/capabilities-reconcile.d.ts.map +1 -0
  138. package/dist/core/constraints/capabilities-reconcile.js +105 -0
  139. package/dist/core/constraints/capabilities-reconcile.js.map +1 -0
  140. package/dist/core/constraints/checker.d.ts +21 -43
  141. package/dist/core/constraints/checker.d.ts.map +1 -1
  142. package/dist/core/constraints/checker.js +64 -115
  143. package/dist/core/constraints/checker.js.map +1 -1
  144. package/dist/core/constraints/checkers/capability-sync.d.ts +4 -3
  145. package/dist/core/constraints/checkers/capability-sync.d.ts.map +1 -1
  146. package/dist/core/constraints/checkers/capability-sync.js +29 -69
  147. package/dist/core/constraints/checkers/capability-sync.js.map +1 -1
  148. package/dist/core/constraints/checkers/context-doc-sync.d.ts +4 -2
  149. package/dist/core/constraints/checkers/context-doc-sync.d.ts.map +1 -1
  150. package/dist/core/constraints/checkers/context-doc-sync.js +13 -22
  151. package/dist/core/constraints/checkers/context-doc-sync.js.map +1 -1
  152. package/dist/core/constraints/checkers/docs-freshness.d.ts.map +1 -1
  153. package/dist/core/constraints/checkers/docs-freshness.js +37 -63
  154. package/dist/core/constraints/checkers/docs-freshness.js.map +1 -1
  155. package/dist/core/constraints/checkers/governance-presence.d.ts +2 -2
  156. package/dist/core/constraints/checkers/governance-presence.d.ts.map +1 -1
  157. package/dist/core/constraints/checkers/governance-presence.js +7 -28
  158. package/dist/core/constraints/checkers/governance-presence.js.map +1 -1
  159. package/dist/core/constraints/checkers/no-test-simplification.d.ts.map +1 -1
  160. package/dist/core/constraints/checkers/no-test-simplification.js +4 -3
  161. package/dist/core/constraints/checkers/no-test-simplification.js.map +1 -1
  162. package/dist/core/constraints/checkers/types.d.ts +2 -2
  163. package/dist/core/constraints/context-builder.d.ts +15 -14
  164. package/dist/core/constraints/context-builder.d.ts.map +1 -1
  165. package/dist/core/constraints/context-builder.js +32 -96
  166. package/dist/core/constraints/context-builder.js.map +1 -1
  167. package/dist/core/constraints/doc-freshness/runner.d.ts.map +1 -1
  168. package/dist/core/constraints/doc-freshness/runner.js +2 -1
  169. package/dist/core/constraints/doc-freshness/runner.js.map +1 -1
  170. package/dist/core/constraints/git-evidence.d.ts +60 -0
  171. package/dist/core/constraints/git-evidence.d.ts.map +1 -0
  172. package/dist/core/constraints/git-evidence.js +88 -0
  173. package/dist/core/constraints/git-evidence.js.map +1 -0
  174. package/dist/core/constraints/injection-drift.d.ts +3 -20
  175. package/dist/core/constraints/injection-drift.d.ts.map +1 -1
  176. package/dist/core/constraints/injection-drift.js +10 -57
  177. package/dist/core/constraints/injection-drift.js.map +1 -1
  178. package/dist/core/constraints/injection-writer.d.ts +113 -0
  179. package/dist/core/constraints/injection-writer.d.ts.map +1 -0
  180. package/dist/core/constraints/injection-writer.js +228 -0
  181. package/dist/core/constraints/injection-writer.js.map +1 -0
  182. package/dist/core/constraints/usage-report.d.ts +22 -1
  183. package/dist/core/constraints/usage-report.d.ts.map +1 -1
  184. package/dist/core/constraints/usage-report.js +27 -20
  185. package/dist/core/constraints/usage-report.js.map +1 -1
  186. package/dist/core/index.d.ts +2 -2
  187. package/dist/core/index.d.ts.map +1 -1
  188. package/dist/core/index.js +4 -2
  189. package/dist/core/index.js.map +1 -1
  190. package/dist/core/project-config-loader.d.ts +32 -1
  191. package/dist/core/project-config-loader.d.ts.map +1 -1
  192. package/dist/core/project-config-loader.js +39 -9
  193. package/dist/core/project-config-loader.js.map +1 -1
  194. package/dist/core/validators/passes-gate.d.ts +15 -64
  195. package/dist/core/validators/passes-gate.d.ts.map +1 -1
  196. package/dist/core/validators/passes-gate.js +84 -222
  197. package/dist/core/validators/passes-gate.js.map +1 -1
  198. package/dist/core/validators/test-output.d.ts +58 -0
  199. package/dist/core/validators/test-output.d.ts.map +1 -0
  200. package/dist/core/validators/test-output.js +109 -0
  201. package/dist/core/validators/test-output.js.map +1 -0
  202. package/dist/failure/recorder.d.ts +6 -0
  203. package/dist/failure/recorder.d.ts.map +1 -1
  204. package/dist/failure/recorder.js +12 -8
  205. package/dist/failure/recorder.js.map +1 -1
  206. package/dist/gates/acceptance.d.ts +1 -48
  207. package/dist/gates/acceptance.d.ts.map +1 -1
  208. package/dist/gates/acceptance.js +6 -38
  209. package/dist/gates/acceptance.js.map +1 -1
  210. package/dist/gates/checker-gate.d.ts.map +1 -1
  211. package/dist/gates/checker-gate.js +6 -11
  212. package/dist/gates/checker-gate.js.map +1 -1
  213. package/dist/gates/command.d.ts.map +1 -1
  214. package/dist/gates/command.js +10 -22
  215. package/dist/gates/command.js.map +1 -1
  216. package/dist/gates/contract.d.ts.map +1 -1
  217. package/dist/gates/contract.js +22 -61
  218. package/dist/gates/contract.js.map +1 -1
  219. package/dist/gates/performance.d.ts.map +1 -1
  220. package/dist/gates/performance.js +13 -31
  221. package/dist/gates/performance.js.map +1 -1
  222. package/dist/gates/review.d.ts.map +1 -1
  223. package/dist/gates/review.js +21 -53
  224. package/dist/gates/review.js.map +1 -1
  225. package/dist/gates/security.d.ts.map +1 -1
  226. package/dist/gates/security.js +14 -32
  227. package/dist/gates/security.js.map +1 -1
  228. package/dist/gates/types.d.ts +28 -5
  229. package/dist/gates/types.d.ts.map +1 -1
  230. package/dist/gates/types.js +38 -1
  231. package/dist/gates/types.js.map +1 -1
  232. package/dist/hooks/bootstrap.d.ts +2 -0
  233. package/dist/hooks/bootstrap.d.ts.map +1 -1
  234. package/dist/hooks/bootstrap.js +5 -2
  235. package/dist/hooks/bootstrap.js.map +1 -1
  236. package/dist/index.d.ts +1 -2
  237. package/dist/index.d.ts.map +1 -1
  238. package/dist/index.js.map +1 -1
  239. package/dist/knowledge/audit.d.ts +7 -2
  240. package/dist/knowledge/audit.d.ts.map +1 -1
  241. package/dist/knowledge/audit.js +59 -79
  242. package/dist/knowledge/audit.js.map +1 -1
  243. package/dist/knowledge/flywheel-metrics.d.ts +39 -0
  244. package/dist/knowledge/flywheel-metrics.d.ts.map +1 -0
  245. package/dist/knowledge/flywheel-metrics.js +47 -0
  246. package/dist/knowledge/flywheel-metrics.js.map +1 -0
  247. package/dist/knowledge/import.d.ts.map +1 -1
  248. package/dist/knowledge/import.js +4 -14
  249. package/dist/knowledge/import.js.map +1 -1
  250. package/dist/knowledge/index-generator.d.ts.map +1 -1
  251. package/dist/knowledge/index-generator.js +17 -14
  252. package/dist/knowledge/index-generator.js.map +1 -1
  253. package/dist/knowledge/ingest.d.ts.map +1 -1
  254. package/dist/knowledge/ingest.js +9 -4
  255. package/dist/knowledge/ingest.js.map +1 -1
  256. package/dist/knowledge/migration.d.ts.map +1 -1
  257. package/dist/knowledge/migration.js +11 -8
  258. package/dist/knowledge/migration.js.map +1 -1
  259. package/dist/knowledge/reference-tracker.d.ts.map +1 -1
  260. package/dist/knowledge/reference-tracker.js +12 -9
  261. package/dist/knowledge/reference-tracker.js.map +1 -1
  262. package/dist/knowledge/store.d.ts +15 -0
  263. package/dist/knowledge/store.d.ts.map +1 -1
  264. package/dist/knowledge/store.js +59 -10
  265. package/dist/knowledge/store.js.map +1 -1
  266. package/dist/monitoring/context-tracker.d.ts.map +1 -1
  267. package/dist/monitoring/context-tracker.js +11 -20
  268. package/dist/monitoring/context-tracker.js.map +1 -1
  269. package/dist/monitoring/index.d.ts +1 -1
  270. package/dist/monitoring/index.d.ts.map +1 -1
  271. package/dist/monitoring/index.js +1 -2
  272. package/dist/monitoring/index.js.map +1 -1
  273. package/dist/monitoring/trace-analyzer.d.ts +15 -0
  274. package/dist/monitoring/trace-analyzer.d.ts.map +1 -1
  275. package/dist/monitoring/trace-analyzer.js +19 -2
  276. package/dist/monitoring/trace-analyzer.js.map +1 -1
  277. package/dist/monitoring/traces.d.ts +21 -2
  278. package/dist/monitoring/traces.d.ts.map +1 -1
  279. package/dist/monitoring/traces.js +38 -29
  280. package/dist/monitoring/traces.js.map +1 -1
  281. package/dist/release/index.d.ts +2 -2
  282. package/dist/release/index.d.ts.map +1 -1
  283. package/dist/release/index.js +1 -5
  284. package/dist/release/index.js.map +1 -1
  285. package/dist/sdd/index-generator.js +10 -11
  286. package/dist/sdd/index-generator.js.map +1 -1
  287. package/dist/test-setup/project-fixture.d.ts +48 -0
  288. package/dist/test-setup/project-fixture.d.ts.map +1 -0
  289. package/dist/test-setup/project-fixture.js +106 -0
  290. package/dist/test-setup/project-fixture.js.map +1 -0
  291. package/dist/types/constraint.d.ts +0 -18
  292. package/dist/types/constraint.d.ts.map +1 -1
  293. package/dist/types/constraint.js.map +1 -1
  294. package/dist/types/passes-gate.d.ts +10 -25
  295. package/dist/types/passes-gate.d.ts.map +1 -1
  296. package/dist/types/project-config.d.ts +1 -1
  297. package/dist/types/session.d.ts +6 -5
  298. package/dist/types/session.d.ts.map +1 -1
  299. package/dist/utils/exec.d.ts +10 -0
  300. package/dist/utils/exec.d.ts.map +1 -1
  301. package/dist/utils/exec.js +12 -0
  302. package/dist/utils/exec.js.map +1 -1
  303. package/dist/utils/file-walk.d.ts +9 -1
  304. package/dist/utils/file-walk.d.ts.map +1 -1
  305. package/dist/utils/file-walk.js +10 -1
  306. package/dist/utils/file-walk.js.map +1 -1
  307. package/dist/utils/frontmatter.d.ts +73 -0
  308. package/dist/utils/frontmatter.d.ts.map +1 -0
  309. package/dist/utils/frontmatter.js +141 -0
  310. package/dist/utils/frontmatter.js.map +1 -0
  311. package/dist/utils/jsonl.d.ts +92 -0
  312. package/dist/utils/jsonl.d.ts.map +1 -0
  313. package/dist/utils/jsonl.js +166 -0
  314. package/dist/utils/jsonl.js.map +1 -0
  315. package/dist/utils/package-version.d.ts +16 -0
  316. package/dist/utils/package-version.d.ts.map +1 -0
  317. package/dist/utils/package-version.js +63 -0
  318. package/dist/utils/package-version.js.map +1 -0
  319. package/package.json +1 -1
  320. package/src/CONTEXT.md +7 -2
  321. package/src/__tests__/acceptance-gate.test.ts +3 -1
  322. package/src/__tests__/checker-extra.test.ts +7 -1218
  323. package/src/__tests__/context-files-resolution.test.ts +116 -0
  324. package/src/__tests__/effective-constraints.test.ts +10 -13
  325. package/src/__tests__/exec.test.ts +35 -1
  326. package/src/__tests__/layering.test.ts +152 -0
  327. package/src/__tests__/passes-gate-extra.test.ts +0 -145
  328. package/src/__tests__/passes-gate.test.ts +142 -106
  329. package/src/__tests__/skip-semantics.test.ts +5 -9
  330. package/src/cli/__tests__/command-contract.test.ts +170 -0
  331. package/src/cli/command-contract.ts +108 -0
  332. package/src/cli/commands/CONTEXT.md +22 -7
  333. package/src/cli/commands/__tests__/acceptance.test.ts +40 -33
  334. package/src/cli/commands/__tests__/analyze-sessions.test.ts +35 -29
  335. package/src/cli/commands/__tests__/bin-exit-mapping.test.ts +101 -0
  336. package/src/cli/commands/__tests__/check-drift.test.ts +27 -34
  337. package/src/cli/commands/__tests__/check-skip-output.test.ts +21 -24
  338. package/src/cli/commands/__tests__/check.test.ts +378 -382
  339. package/src/cli/commands/__tests__/command.test.ts +85 -70
  340. package/src/cli/commands/__tests__/constraints-report.test.ts +100 -71
  341. package/src/cli/commands/__tests__/constraints-retire.test.ts +152 -95
  342. package/src/cli/commands/__tests__/contract.test.ts +27 -34
  343. package/src/cli/commands/__tests__/failure.test.ts +31 -34
  344. package/src/cli/commands/__tests__/init-injection.test.ts +58 -28
  345. package/src/cli/commands/__tests__/init.test.ts +77 -48
  346. package/src/cli/commands/__tests__/knowledge-flywheel-consistency.test.ts +108 -0
  347. package/src/cli/commands/__tests__/knowledge.test.ts +69 -43
  348. package/src/cli/commands/__tests__/passes-gate.test.ts +43 -45
  349. package/src/cli/commands/__tests__/performance.test.ts +22 -23
  350. package/src/cli/commands/__tests__/posteval-plan.test.ts +39 -38
  351. package/src/cli/commands/__tests__/project-path-anchoring.test.ts +157 -0
  352. package/src/cli/commands/__tests__/project-path-convention.test.ts +169 -0
  353. package/src/cli/commands/__tests__/registry.test.ts +69 -35
  354. package/src/cli/commands/__tests__/release.test.ts +209 -80
  355. package/src/cli/commands/__tests__/report-extra.test.ts +16 -16
  356. package/src/cli/commands/__tests__/report.test.ts +25 -25
  357. package/src/cli/commands/__tests__/review.test.ts +33 -31
  358. package/src/cli/commands/__tests__/security.test.ts +23 -27
  359. package/src/cli/commands/__tests__/spec-baseline-check.test.ts +20 -20
  360. package/src/cli/commands/__tests__/spec.test.ts +41 -29
  361. package/src/cli/commands/__tests__/status-extra.test.ts +18 -18
  362. package/src/cli/commands/__tests__/status-skipped-lines.test.ts +72 -0
  363. package/src/cli/commands/__tests__/status.test.ts +34 -34
  364. package/src/cli/commands/__tests__/sync-docs-agents.test.ts +77 -78
  365. package/src/cli/commands/__tests__/sync-docs.test.ts +72 -74
  366. package/src/cli/commands/__tests__/update-user-model.test.ts +62 -33
  367. package/src/cli/commands/__tests__/validate.test.ts +27 -29
  368. package/src/cli/commands/acceptance.ts +35 -24
  369. package/src/cli/commands/analyze-sessions.ts +25 -24
  370. package/src/cli/commands/check.ts +88 -65
  371. package/src/cli/commands/command.ts +30 -17
  372. package/src/cli/commands/constraints-report.ts +37 -34
  373. package/src/cli/commands/constraints-retire.ts +58 -55
  374. package/src/cli/commands/constraints.ts +10 -27
  375. package/src/cli/commands/contract.ts +51 -42
  376. package/src/cli/commands/definitions.ts +36 -222
  377. package/src/cli/commands/failure.ts +33 -26
  378. package/src/cli/commands/init.ts +166 -220
  379. package/src/cli/commands/knowledge.ts +225 -203
  380. package/src/cli/commands/passes-gate.ts +58 -68
  381. package/src/cli/commands/performance.ts +23 -18
  382. package/src/cli/commands/posteval-plan.ts +29 -15
  383. package/src/cli/commands/release.ts +70 -59
  384. package/src/cli/commands/report.ts +9 -5
  385. package/src/cli/commands/review.ts +39 -29
  386. package/src/cli/commands/sdd.ts +7 -5
  387. package/src/cli/commands/security.ts +35 -26
  388. package/src/cli/commands/spec-baseline-check.ts +16 -11
  389. package/src/cli/commands/spec.ts +66 -53
  390. package/src/cli/commands/status.ts +59 -53
  391. package/src/cli/commands/sync-docs/agents-syncer.ts +5 -2
  392. package/src/cli/commands/sync-docs/context-syncer.ts +3 -2
  393. package/src/cli/commands/sync-docs/index.ts +73 -100
  394. package/src/cli/commands/sync-docs/project-reader.ts +17 -18
  395. package/src/cli/commands/update-user-model.ts +33 -45
  396. package/src/cli/commands/validate.ts +31 -25
  397. package/src/cli/session-mining/__tests__/transcript.test.ts +118 -0
  398. package/src/cli/session-mining/transcript.ts +26 -8
  399. package/src/context/session-manager.ts +19 -33
  400. package/src/core/CONTEXT.md +9 -6
  401. package/src/core/constraints/__tests__/capabilities-reconcile.test.ts +200 -0
  402. package/src/core/constraints/__tests__/capability-counts.test.ts +94 -0
  403. package/src/core/constraints/__tests__/git-evidence.test.ts +208 -0
  404. package/src/core/constraints/__tests__/injection-drift.test.ts +3 -10
  405. package/src/core/constraints/__tests__/injection-writer.test.ts +231 -0
  406. package/src/core/constraints/__tests__/trace-injection.test.ts +71 -0
  407. package/src/core/constraints/__tests__/usage-report.test.ts +112 -0
  408. package/src/core/constraints/agent-prompt-renderer.ts +2 -6
  409. package/src/core/constraints/capabilities-parser.ts +53 -35
  410. package/src/core/constraints/capabilities-reconcile.ts +166 -0
  411. package/src/core/constraints/checker.ts +75 -130
  412. package/src/core/constraints/checkers/__tests__/capability-sync.test.ts +214 -0
  413. package/src/core/constraints/checkers/__tests__/context-doc-sync.test.ts +77 -0
  414. package/src/core/constraints/checkers/__tests__/docs-freshness.test.ts +117 -0
  415. package/src/{__tests__ → core/constraints/checkers/__tests__}/governance-presence.test.ts +83 -9
  416. package/src/core/constraints/checkers/__tests__/iron-flags.test.ts +50 -0
  417. package/src/core/constraints/checkers/__tests__/no-bypass-checkpoint.test.ts +76 -0
  418. package/src/{__tests__ → core/constraints/checkers/__tests__}/no-hardcoded-credentials.test.ts +4 -3
  419. package/src/core/constraints/checkers/__tests__/no-test-simplification.test.ts +58 -0
  420. package/src/core/constraints/checkers/__tests__/three-state-consistency.test.ts +145 -0
  421. package/src/core/constraints/checkers/capability-sync.ts +29 -88
  422. package/src/core/constraints/checkers/context-doc-sync.ts +15 -25
  423. package/src/core/constraints/checkers/docs-freshness.ts +40 -69
  424. package/src/core/constraints/checkers/governance-presence.ts +7 -25
  425. package/src/core/constraints/checkers/no-test-simplification.ts +4 -3
  426. package/src/core/constraints/checkers/types.ts +2 -2
  427. package/src/core/constraints/context-builder.ts +39 -91
  428. package/src/core/constraints/doc-freshness/runner.ts +2 -1
  429. package/src/core/constraints/git-evidence.ts +120 -0
  430. package/src/core/constraints/injection-drift.ts +13 -60
  431. package/src/core/constraints/injection-writer.ts +212 -0
  432. package/src/core/constraints/usage-report.ts +36 -17
  433. package/src/core/index.ts +4 -2
  434. package/src/core/project-config-loader.ts +49 -9
  435. package/src/core/validators/__tests__/passes-gate.test.ts +2 -56
  436. package/src/core/validators/__tests__/test-output.test.ts +372 -0
  437. package/src/core/validators/passes-gate.ts +86 -248
  438. package/src/core/validators/test-output.ts +128 -0
  439. package/src/failure/__tests__/recorder.test.ts +168 -0
  440. package/src/failure/recorder.ts +12 -10
  441. package/src/gates/CONTEXT.md +6 -3
  442. package/src/gates/__tests__/acceptance-types-single-source.test.ts +68 -0
  443. package/src/gates/__tests__/acceptance.test.ts +4 -86
  444. package/src/gates/__tests__/effective-gates.test.ts +3 -8
  445. package/src/gates/__tests__/test-verdict-consistency.test.ts +140 -0
  446. package/src/gates/acceptance.ts +19 -84
  447. package/src/gates/checker-gate.ts +8 -7
  448. package/src/gates/command.ts +10 -16
  449. package/src/gates/contract.ts +22 -61
  450. package/src/gates/performance.ts +10 -22
  451. package/src/gates/review.ts +21 -41
  452. package/src/gates/security.ts +10 -22
  453. package/src/gates/types.ts +70 -5
  454. package/src/hooks/CONTEXT.md +1 -1
  455. package/src/hooks/bootstrap.ts +5 -2
  456. package/src/index.ts +0 -4
  457. package/src/knowledge/CONTEXT.md +8 -3
  458. package/src/knowledge/__tests__/audit-labels.test.ts +44 -0
  459. package/src/knowledge/__tests__/flywheel-metrics.test.ts +135 -0
  460. package/src/knowledge/__tests__/import.test.ts +44 -0
  461. package/src/knowledge/__tests__/index-generator.test.ts +53 -0
  462. package/src/knowledge/__tests__/ingest.test.ts +85 -0
  463. package/src/knowledge/__tests__/migration.test.ts +47 -0
  464. package/src/knowledge/__tests__/reference-tracker.test.ts +16 -0
  465. package/src/knowledge/__tests__/store.test.ts +194 -0
  466. package/src/knowledge/audit.ts +67 -71
  467. package/src/knowledge/flywheel-metrics.ts +71 -0
  468. package/src/knowledge/import.ts +4 -18
  469. package/src/knowledge/index-generator.ts +19 -14
  470. package/src/knowledge/ingest.ts +9 -4
  471. package/src/knowledge/migration.ts +11 -8
  472. package/src/knowledge/reference-tracker.ts +12 -8
  473. package/src/knowledge/store.ts +64 -11
  474. package/src/monitoring/CONTEXT.md +8 -3
  475. package/src/monitoring/__tests__/skipped-lines-report.test.ts +178 -0
  476. package/src/monitoring/__tests__/traces.test.ts +57 -0
  477. package/src/monitoring/context-tracker.ts +11 -23
  478. package/src/monitoring/index.ts +1 -1
  479. package/src/monitoring/trace-analyzer.ts +21 -2
  480. package/src/monitoring/traces.ts +39 -36
  481. package/src/release/CONTEXT.md +1 -0
  482. package/src/release/__tests__/barrel.test.ts +60 -0
  483. package/src/release/index.ts +2 -13
  484. package/src/sdd/__tests__/index-generator.test.ts +71 -0
  485. package/src/sdd/index-generator.ts +9 -9
  486. package/src/test-setup/__tests__/project-fixture.test.ts +178 -0
  487. package/src/test-setup/project-fixture.ts +87 -0
  488. package/src/tools/CONTEXT.md +6 -0
  489. package/src/tools/__tests__/registry.test.ts +221 -0
  490. package/src/types/__tests__/types.test.ts +4 -5
  491. package/src/types/constraint.ts +0 -25
  492. package/src/types/passes-gate.ts +10 -30
  493. package/src/types/project-config.ts +1 -1
  494. package/src/types/session.ts +5 -5
  495. package/src/utils/__tests__/frontmatter.test.ts +206 -0
  496. package/src/utils/__tests__/jsonl-skip-disposition.test.ts +175 -0
  497. package/src/utils/__tests__/jsonl.test.ts +187 -0
  498. package/src/utils/__tests__/package-version.test.ts +97 -0
  499. package/src/utils/exec.ts +15 -0
  500. package/src/utils/file-walk.ts +11 -2
  501. package/src/utils/frontmatter.ts +121 -0
  502. package/src/utils/jsonl.ts +167 -0
  503. package/src/utils/package-version.ts +27 -0
  504. package/dist/cli/commands/doc-freshness-check.d.ts +0 -53
  505. package/dist/cli/commands/doc-freshness-check.d.ts.map +0 -1
  506. package/dist/cli/commands/doc-freshness-check.js +0 -352
  507. package/dist/cli/commands/doc-freshness-check.js.map +0 -1
  508. package/dist/types/index.d.ts +0 -14
  509. package/dist/types/index.d.ts.map +0 -1
  510. package/dist/types/index.js +0 -39
  511. package/dist/types/index.js.map +0 -1
  512. package/src/__tests__/long-running.test.ts +0 -17
  513. package/src/__tests__/passes-gate-extension.test.ts +0 -151
  514. package/src/cli/commands/__tests__/doc-freshness-check.test.ts +0 -168
  515. package/src/cli/commands/doc-freshness-check.ts +0 -386
  516. package/src/spec/CONTEXT.md +0 -11
  517. package/src/types/index.ts +0 -47
@@ -1,63 +1,31 @@
1
1
  /**
2
- * check 命令测试
2
+ * check 命令测试(#87 重写:真 git fixture 驱动)
3
+ *
4
+ * 此前整个 checker 与 child_process 都被 mock,被测对象已不在 interface 之内
5
+ * (断言的是替身的返回值)。现在每个用例在临时 git 仓库里摆好真证据
6
+ * (staged 变更 / traces.log / .harness/config.yml),跑真 context-builder +
7
+ * 真 ConstraintChecker,断言 CLI 输出形状与 CommandResult。
8
+ * 取证次数经 createGitEvidence 的计数 runner 观测——执行仍走真 git,不 mock 子进程。
9
+ *
10
+ * trace 记录器由命令侧组合根接线(harness#88):套件把全局收集器重定向到临时文件,
11
+ * 既不断言宿主仓写入,也不污染它。
3
12
  */
4
13
 
5
- import { check, listLaws } from '../check';
6
14
  import * as fs from 'fs';
7
- import { constraintChecker } from '../../../core/constraints/checker';
8
- import { getMergedConstraintsConfig } from '../../../core/effective-constraints';
9
- import { execAsync } from '../../../utils/exec';
10
-
11
- // Mock fs
12
- jest.mock('fs', () => ({
13
- existsSync: jest.fn(),
14
- readFileSync: jest.fn(),
15
- mkdirSync: jest.fn(),
16
- writeFileSync: jest.fn(),
17
- readdirSync: jest.fn(),
18
- }));
19
-
20
- // Mock execAsync
21
- jest.mock('../../../utils/exec', () => ({
22
- execAsync: jest.fn(),
23
- }));
24
-
25
- // Mock child_process.execSync (used by isNewDirectory + getChangedFiles in check.ts)
26
- jest.mock('child_process', () => ({
27
- exec: jest.fn(),
28
- execSync: jest.fn((cmd: string) => {
29
- if (cmd.includes('git ls-tree')) {
30
- // 工单 18 后为批量命令 `git ls-tree -r --name-only HEAD`,返回文件列表
31
- return Buffer.from('src/foo.ts\nsrc/module.ts\nsrc/__tests__/module.test.ts\n');
32
- }
33
- if (cmd.includes('git diff')) {
34
- return Buffer.from('src/test.ts\nsrc/legacy.js');
35
- }
36
- return Buffer.from('');
37
- }),
38
- }));
39
-
40
- // Mock constraintChecker
41
- jest.mock('../../../core/constraints/checker', () => ({
42
- constraintChecker: {
43
- setTraceRecorder: jest.fn(),
44
- checkConstraints: jest.fn(),
45
- },
46
- }));
47
-
48
- // Mock effective-constraints(ADR-0001:check 经 getMergedConstraintsConfig 走生效集链路)
49
- jest.mock('../../../core/effective-constraints', () => ({
50
- getMergedConstraintsConfig: jest.fn().mockReturnValue({
51
- ironLaws: {},
52
- guidelines: {},
53
- prompts: {},
54
- custom: [],
55
- disabled: [],
56
- unknownIds: [],
57
- }),
58
- }));
15
+ import * as os from 'os';
16
+ import * as path from 'path';
17
+ import { execFileSync } from 'child_process';
59
18
 
60
- // Mock chalk
19
+ import { check, listLaws } from '../check';
20
+ import { captureIO, type CapturingIO } from '../../command-contract';
21
+ import { configureTraceCollector } from '../../../monitoring/traces';
22
+ import {
23
+ createGitEvidence,
24
+ realGitCommandRunner,
25
+ type GitEvidence,
26
+ } from '../../../core/constraints/git-evidence';
27
+
28
+ // chalk:格式化替身,断言只看纯文本形状(不影响判定)
61
29
  jest.mock('chalk', () => ({
62
30
  blue: jest.fn((str: string) => str),
63
31
  yellow: jest.fn((str: string) => str),
@@ -66,416 +34,444 @@ jest.mock('chalk', () => ({
66
34
  red: jest.fn((str: string) => str),
67
35
  }));
68
36
 
69
- const mockFs = fs as jest.Mocked<typeof fs> & { readdirSync: jest.Mock };
70
- const mockChecker = constraintChecker as jest.Mocked<typeof constraintChecker>;
71
- const mockGetMergedConfig = getMergedConstraintsConfig as jest.MockedFunction<typeof getMergedConstraintsConfig>;
72
- const mockExecAsync = execAsync as jest.MockedFunction<typeof execAsync>;
37
+ /** fixture 内跑 git(argv 数组,不经 shell) */
38
+ function git(dir: string, ...args: string[]): void {
39
+ execFileSync('git', args, { cwd: dir, stdio: 'pipe' });
40
+ }
41
+
42
+ /** 写文件(自动建父目录) */
43
+ function write(dir: string, rel: string, content: string): void {
44
+ const target = path.join(dir, rel);
45
+ fs.mkdirSync(path.dirname(target), { recursive: true });
46
+ fs.writeFileSync(target, content);
47
+ }
48
+
49
+ /** 真 git 仓库基线:README(需求证据)+ src 源码根,已提交 */
50
+ function gitRepo(): string {
51
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-check-cmd-'));
52
+ git(dir, 'init', '-q');
53
+ git(dir, 'config', 'user.email', 'fixture@example.com');
54
+ git(dir, 'config', 'user.name', 'Fixture');
55
+ write(dir, 'README.md', '# fixture\n');
56
+ write(dir, 'src/existing.ts', 'export const a = 1;\n');
57
+ write(dir, 'src/nested/deep.ts', 'export const d = 1;\n');
58
+ git(dir, 'add', '.');
59
+ git(dir, 'commit', '-q', '-m', 'baseline');
60
+ return dir;
61
+ }
62
+
63
+ /** 改一个已跟踪文件并 stage */
64
+ function stageChange(dir: string, rel: string, content: string): void {
65
+ write(dir, rel, content);
66
+ git(dir, 'add', '--', rel);
67
+ }
68
+
69
+ /** 真验证证据:traces.log 里有一条 pass(未跟踪,不进 diff) */
70
+ function passTraces(dir: string, lines = 1): void {
71
+ write(
72
+ dir,
73
+ '.harness/logs/traces.log',
74
+ Array(lines).fill('{"constraintId":"fixture","result":"pass"}').join('\n') + '\n'
75
+ );
76
+ }
77
+
78
+ /** 计数 git 证据:既记录证据方法请求,也记录实际 spawn 的 git 命令(执行走真 adapter) */
79
+ function recordingEvidence(projectPath: string): {
80
+ evidence: GitEvidence;
81
+ requests: string[];
82
+ commands: string[];
83
+ } {
84
+ const requests: string[] = [];
85
+ const commands: string[] = [];
86
+ const shared = createGitEvidence(projectPath, (command, cwd) => {
87
+ commands.push(command);
88
+ return realGitCommandRunner(command, cwd);
89
+ });
90
+ const evidence: GitEvidence = {
91
+ projectPath,
92
+ stagedDiff: () => {
93
+ requests.push('stagedDiff');
94
+ return shared.stagedDiff();
95
+ },
96
+ changedFileNames: (staged: boolean) => {
97
+ requests.push(`changedFileNames:${staged}`);
98
+ return shared.changedFileNames(staged);
99
+ },
100
+ headDirs: () => {
101
+ requests.push('headDirs');
102
+ return shared.headDirs();
103
+ },
104
+ };
105
+ return { evidence, requests, commands };
106
+ }
107
+
108
+ /** test_creation fixture:提交两行测试,再 stage 删除其中一行 */
109
+ function stagedTestDeletion(): string {
110
+ const dir = gitRepo();
111
+ write(dir, 'src/__tests__/existing.test.ts', "test('a', () => {}\ntest('b', () => {}\n");
112
+ git(dir, 'add', 'src/__tests__/existing.test.ts');
113
+ git(dir, 'commit', '-q', '-m', 'tests');
114
+ stageChange(dir, 'src/__tests__/existing.test.ts', "test('a', () => {}\n");
115
+ passTraces(dir);
116
+ return dir;
117
+ }
118
+
119
+ describe('check command(真 git fixture)', () => {
120
+ let io: CapturingIO;
121
+ let traceDir: string;
122
+ let traceFile: string;
123
+ let traceSeq = 0;
124
+
125
+ beforeAll(() => {
126
+ traceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-check-traces-'));
127
+ });
73
128
 
74
- describe('check command', () => {
75
- let consoleSpy: jest.SpyInstance;
129
+ afterAll(() => {
130
+ configureTraceCollector({});
131
+ try {
132
+ fs.rmSync(traceDir, { recursive: true, force: true });
133
+ } catch {
134
+ // ignore
135
+ }
136
+ });
76
137
 
77
138
  beforeEach(() => {
78
- jest.clearAllMocks();
79
- // clearAllMocks 不清实现,显式恢复默认 merged config,避免用例间泄漏
80
- mockGetMergedConfig.mockReturnValue({
81
- ironLaws: {},
82
- guidelines: {},
83
- prompts: {},
84
- custom: [],
85
- disabled: [],
86
- unknownIds: [],
87
- });
88
- consoleSpy = jest.spyOn(console, 'log').mockImplementation();
89
- process.exitCode = 0;
139
+ io = captureIO();
140
+ // 命令侧接线的是全局收集器(默认 cwd 相对写入):重定向到套件临时文件,宿主仓 .harness/ 不受影响
141
+ traceFile = path.join(traceDir, `trace-${++traceSeq}.log`);
142
+ configureTraceCollector({ traceFile });
90
143
  });
91
144
 
92
- afterEach(() => {
93
- consoleSpy.mockRestore();
94
- process.exitCode = 0;
95
- });
145
+ describe('判定与输出形状', () => {
146
+ it('真证据全部满足时逐层通过,未接线证据单独列示', async () => {
147
+ const dir = gitRepo();
148
+ stageChange(dir, 'src/existing.ts', 'export const a = 2;\n');
149
+ passTraces(dir);
96
150
 
97
- describe('check', () => {
98
- it('应该通过所有约束检查', async () => {
99
- mockChecker.checkConstraints.mockResolvedValue({
100
- passed: true,
101
- 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' } }],
102
- guidelines: [],
103
- warningCount: 0,
104
- });
151
+ const result = await check(
152
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'code_implementation' },
153
+ io
154
+ );
105
155
 
106
- await check({ preset: 'default', staged: false });
107
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('约束检查通过'));
156
+ // 3 code_implementation 铁律:incremental_progress hasSingleTask 未接线 → skip
157
+ expect(io.outText()).toContain('✅ 铁律: 全部通过 (2 条)');
158
+ expect(io.outText()).toContain('✅ 指导原则: 2/2 通过');
159
+ expect(io.outText()).toContain('⏭️ 跳过评估: 1 条(约定未采用或证据未接线,不计通过/失败)');
160
+ expect(io.outText()).toContain('- incremental_progress');
161
+ expect(io.outText()).toContain('✅ 约束检查通过');
162
+ expect(result).toEqual({ kind: 'ok' });
108
163
  });
109
164
 
110
- it('应该显示铁律违规', async () => {
111
- mockChecker.checkConstraints.mockResolvedValue({
112
- passed: false,
113
- 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' } }],
114
- guidelines: [],
115
- warningCount: 0,
116
- });
165
+ it('无验证证据 → 真铁律拦截,经异常外溢为 fail', async () => {
166
+ const dir = gitRepo();
167
+ stageChange(dir, 'src/existing.ts', 'export const a = 2;\n');
168
+ write(dir, '.harness/logs/traces.log', '{"constraintId":"fixture","result":"fail"}\n');
117
169
 
118
- const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
170
+ const result = await check(
171
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'code_implementation' },
172
+ io
173
+ );
119
174
 
120
- await check({ preset: 'default', staged: false });
121
- expect(mockExit).toHaveBeenCalledWith(1);
122
- mockExit.mockRestore();
175
+ expect(io.outText()).toContain('禁止无验证声明完成,必须运行验证命令');
176
+ expect(result).toEqual({
177
+ kind: 'fail',
178
+ reason: 'check error: 禁止无验证声明完成,必须运行验证命令',
179
+ });
123
180
  });
124
181
 
125
- it('应该显示指导原则警告', async () => {
126
- mockChecker.checkConstraints.mockResolvedValue({
127
- passed: false,
128
- ironLaws: [],
129
- 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' } }],
130
- warningCount: 1,
131
- });
182
+ it('staged 删除测试行 → 真 git diff 驱动 no_test_simplification 拦截', async () => {
183
+ const dir = stagedTestDeletion();
184
+ const { evidence, commands } = recordingEvidence(dir);
132
185
 
133
- await check({ preset: 'default', staged: false });
134
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('指导原则警告'));
135
- });
186
+ const result = await check(
187
+ { preset: 'standard', staged: true, projectPath: dir, evidence },
188
+ io
189
+ );
136
190
 
137
- it('应该加载自定义约束', async () => {
138
- mockGetMergedConfig.mockReturnValue({
139
- ironLaws: {},
140
- guidelines: {},
141
- prompts: {},
142
- custom: ['custom'],
143
- disabled: ['disabled_constraint'],
144
- unknownIds: [],
145
- });
191
+ // 触发条件由真 diff 推断出 test_creation,checker 经 env.stagedDiff() 读到真 diff
192
+ expect(io.outText()).toContain('触发条件: test_creation, code_implementation');
193
+ expect(commands).toContain('git diff --cached');
194
+ expect(io.outText()).toContain('禁止简化测试绕过困难');
195
+ expect(result.kind).toBe('fail');
196
+ });
146
197
 
147
- mockChecker.checkConstraints.mockResolvedValue({
148
- passed: true,
149
- ironLaws: [],
150
- guidelines: [],
151
- warningCount: 0,
152
- });
198
+ it('staged 硬编码凭证 → 指导原则警告但不阻断', async () => {
199
+ const dir = gitRepo();
200
+ stageChange(
201
+ dir,
202
+ 'src/existing.ts',
203
+ 'export const a = 1;\nconst pass' + 'word = "correcthorsebattery";\n'
204
+ );
205
+ passTraces(dir);
153
206
 
154
- await check({ preset: 'default', staged: false, projectPath: '/project' });
155
- expect(mockChecker.checkConstraints).toHaveBeenCalledWith(
156
- expect.anything(),
157
- expect.objectContaining({ disabled: ['disabled_constraint'] })
207
+ const result = await check(
208
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'code_implementation' },
209
+ io
158
210
  );
159
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('已禁用约束'));
211
+
212
+ expect(io.outText()).toContain('⚠️ 指导原则警告: 1 条');
213
+ expect(io.outText()).toContain('- no_hardcoded_credentials');
214
+ expect(io.outText()).toContain('✅ 约束检查通过');
215
+ expect(result).toEqual({ kind: 'ok' });
160
216
  });
161
217
 
162
- it('应该显示通过的指导原则', async () => {
163
- mockChecker.checkConstraints.mockResolvedValue({
164
- passed: true,
165
- ironLaws: [],
166
- 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' } }],
167
- warningCount: 0,
168
- });
218
+ it('变更文件数量来自真 staged 名单', async () => {
219
+ const dir = gitRepo();
220
+ stageChange(dir, 'src/existing.ts', 'export const a = 2;\n');
221
+ stageChange(dir, 'src/nested/deep.ts', 'export const d = 2;\n');
222
+ passTraces(dir);
169
223
 
170
- await check({ preset: 'default', staged: false });
171
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('指导原则'));
224
+ await check(
225
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'code_implementation' },
226
+ io
227
+ );
228
+
229
+ expect(io.outText()).toContain('变更文件: 2 个');
172
230
  });
173
231
 
174
- it('应该显示变更文件数量', async () => {
175
- // Mock getChangedFiles 返回变更文件
176
- mockExecAsync.mockResolvedValue({ stdout: 'src/foo.ts\nsrc/bar.ts\n', stderr: '' });
177
- mockChecker.checkConstraints.mockResolvedValue({
178
- passed: true,
179
- ironLaws: [],
180
- guidelines: [],
181
- warningCount: 0,
182
- });
232
+ it('非 git 目录:无变更文件行,检查照常完成', async () => {
233
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-check-nogit-'));
234
+ write(dir, 'README.md', '# fixture\n');
183
235
 
184
- await check({ preset: 'default', staged: true });
185
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('变更文件: 2 个'));
236
+ const result = await check({ preset: 'standard', staged: true, projectPath: dir }, io);
237
+
238
+ expect(io.outText()).not.toContain('变更文件');
239
+ expect(result).toEqual({ kind: 'ok' });
186
240
  });
241
+ });
187
242
 
188
- it('应该使用 unstaged diff 当 staged 为 false', async () => {
189
- mockExecAsync.mockResolvedValue({ stdout: 'file.ts\n', stderr: '' });
190
- mockChecker.checkConstraints.mockResolvedValue({
191
- passed: true,
192
- ironLaws: [],
193
- guidelines: [],
194
- warningCount: 0,
195
- });
243
+ describe('git 证据单一来源(#87 验收)', () => {
244
+ it('两个 checker 请求同一份 staged diff → 一次 run 内只 spawn 一次', async () => {
245
+ // 新增(非删除)测试行:no_test_simplification 请求 stagedDiff 后放行,
246
+ // no_hardcoded_credentials 再请求同一份 → 只有铁律都通过时两者才同 run
247
+ const dir = gitRepo();
248
+ stageChange(dir, 'src/__tests__/added.test.ts', "test('a', () => {}\n");
249
+ passTraces(dir);
250
+ const { evidence, requests, commands } = recordingEvidence(dir);
251
+
252
+ const result = await check({ preset: 'standard', staged: true, projectPath: dir, evidence }, io);
253
+
254
+ expect(result).toEqual({ kind: 'ok' });
255
+ expect(requests.filter(r => r === 'stagedDiff').length).toBeGreaterThanOrEqual(2);
256
+ expect(commands.filter(c => c === 'git diff --cached')).toHaveLength(1);
257
+ expect(commands).toContain('git diff --cached --name-only');
258
+ expect(new Set(commands).size).toBe(commands.length);
259
+ });
196
260
 
197
- await check({ preset: 'default', staged: false });
198
- expect(mockExecAsync).toHaveBeenCalledWith('git diff --name-only');
261
+ it('--staged --cached 名单,非 staged 用工作区名单', async () => {
262
+ const stagedDir = gitRepo();
263
+ stageChange(stagedDir, 'src/existing.ts', 'export const a = 2;\n');
264
+ passTraces(stagedDir);
265
+ const staged = recordingEvidence(stagedDir);
266
+ await check(
267
+ { preset: 'standard', staged: true, projectPath: stagedDir, trigger: 'code_implementation', evidence: staged.evidence },
268
+ io
269
+ );
270
+ expect(staged.requests).toContain('changedFileNames:true');
271
+ expect(staged.commands).toContain('git diff --cached --name-only');
272
+ expect(staged.commands).not.toContain('git diff --name-only');
273
+
274
+ const worktreeDir = gitRepo();
275
+ write(worktreeDir, 'src/existing.ts', 'export const a = 3;\n');
276
+ passTraces(worktreeDir);
277
+ const worktree = recordingEvidence(worktreeDir);
278
+ await check(
279
+ { preset: 'standard', staged: false, projectPath: worktreeDir, trigger: 'code_implementation', evidence: worktree.evidence },
280
+ io
281
+ );
282
+ expect(worktree.requests).toContain('changedFileNames:false');
283
+ expect(worktree.commands).toContain('git diff --name-only');
284
+ expect(worktree.commands).not.toContain('git diff --cached --name-only');
199
285
  });
286
+ });
200
287
 
201
- it('应该使用 staged diff 当 staged 为 true', async () => {
202
- mockExecAsync.mockResolvedValue({ stdout: 'file.ts\n', stderr: '' });
203
- mockChecker.checkConstraints.mockResolvedValue({
204
- passed: true,
205
- ironLaws: [],
206
- guidelines: [],
207
- warningCount: 0,
208
- });
288
+ describe('trace 记录器组合根接线(#88 验收)', () => {
289
+ it('一次 check 落盘每条约束的 trace(core 不上行依赖 monitoring,由命令侧接线)', async () => {
290
+ const dir = gitRepo();
291
+ stageChange(dir, 'src/existing.ts', 'export const a = 2;\n');
292
+ passTraces(dir);
209
293
 
210
- await check({ preset: 'default', staged: true });
211
- expect(mockExecAsync).toHaveBeenCalledWith('git diff --cached --name-only');
212
- });
294
+ await check(
295
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'code_implementation' },
296
+ io
297
+ );
213
298
 
214
- it('应该在 git 失败时返回空文件列表', async () => {
215
- mockExecAsync.mockRejectedValue(new Error('not a git repo'));
216
- mockChecker.checkConstraints.mockResolvedValue({
217
- passed: true,
218
- ironLaws: [],
219
- guidelines: [],
220
- warningCount: 0,
221
- });
299
+ const traces = fs
300
+ .readFileSync(traceFile, 'utf-8')
301
+ .split('\n')
302
+ .filter(Boolean)
303
+ .map(l => JSON.parse(l) as { constraintId: string; result: string; projectPath: string });
222
304
 
223
- await check({ preset: 'default', staged: false });
224
- // 不应显示变更文件数量
225
- expect(consoleSpy).not.toHaveBeenCalledWith(expect.stringContaining('变更文件'));
305
+ expect(traces.map(t => t.constraintId)).toEqual(
306
+ expect.arrayContaining(['incremental_progress', 'no_implementation_without_requirement'])
307
+ );
308
+ expect(traces.every(t => ['pass', 'fail', 'skip'].includes(t.result))).toBe(true);
309
+ expect(traces.every(t => t.projectPath === dir)).toBe(true);
226
310
  });
311
+ });
227
312
 
228
- it('应该检测 module_modification 触发条件(.ts 文件)', async () => {
229
- mockExecAsync.mockResolvedValue({ stdout: 'src/foo.ts\nsrc/bar.tsx\n', stderr: '' });
230
- mockChecker.checkConstraints.mockResolvedValue({
231
- passed: true, ironLaws: [], guidelines: [], warningCount: 0,
232
- });
313
+ describe('触发条件推断(真 diff + 真 HEAD 目录集)', () => {
314
+ it('已存在目录内的代码变更 module_modification', async () => {
315
+ const dir = gitRepo();
316
+ stageChange(dir, 'src/existing.ts', 'export const a = 2;\n');
317
+ passTraces(dir);
233
318
 
234
- // Mock filesystem so detectSourceRoots finds src/ as a source root
235
- const mockProjectPath = '/mock-project';
236
- mockFs.existsSync.mockImplementation((p: any) => {
237
- if (p === `${mockProjectPath}/src`) return true;
238
- return false;
239
- });
240
- mockFs.readdirSync.mockImplementation((dir: any) => {
241
- if (dir === `${mockProjectPath}/src`) {
242
- return [{ name: 'foo.ts', isFile: () => true, isDirectory: () => false }] as any;
243
- }
244
- return [];
245
- });
319
+ await check({ preset: 'standard', staged: true, projectPath: dir }, io);
246
320
 
247
- await check({ preset: 'default', staged: false, projectPath: mockProjectPath });
248
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('module_modification'));
321
+ expect(io.outText()).toContain('触发条件: module_modification, code_implementation');
249
322
  });
250
323
 
251
- it('应该检测 module_modification 触发条件(.js 文件)', async () => {
252
- mockExecAsync.mockResolvedValue({ stdout: 'src/foo.js\nsrc/bar.jsx\n', stderr: '' });
253
- mockChecker.checkConstraints.mockResolvedValue({
254
- passed: true, ironLaws: [], guidelines: [], warningCount: 0,
255
- });
324
+ it('源码根下新目录 module_creation(HEAD 里没有该目录)', async () => {
325
+ const dir = gitRepo();
326
+ stageChange(dir, 'src/brandnew/mod.ts', 'export const n = 1;\n');
327
+ passTraces(dir);
256
328
 
257
- // Mock filesystem so detectSourceRoots finds src/ as a source root
258
- const mockProjectPath = '/mock-project';
259
- mockFs.existsSync.mockImplementation((p: any) => {
260
- if (p === `${mockProjectPath}/src`) return true;
261
- return false;
262
- });
263
- mockFs.readdirSync.mockImplementation((dir: any) => {
264
- if (dir === `${mockProjectPath}/src`) {
265
- return [{ name: 'foo.ts', isFile: () => true, isDirectory: () => false }] as any;
266
- }
267
- return [];
268
- });
329
+ await check({ preset: 'standard', staged: true, projectPath: dir }, io);
269
330
 
270
- await check({ preset: 'default', staged: false, projectPath: mockProjectPath });
271
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('module_modification'));
331
+ expect(io.outText()).toContain('触发条件: module_creation, code_implementation');
272
332
  });
273
333
 
274
- it('应该检测 file_modification 触发条件(非 src/ 目录)', async () => {
275
- mockExecAsync.mockResolvedValue({ stdout: 'docs/bar.md\n', stderr: '' });
276
- mockChecker.checkConstraints.mockResolvedValue({
277
- passed: true, ironLaws: [], guidelines: [], warningCount: 0,
278
- });
334
+ it('仅测试变更 test_creation', async () => {
335
+ const dir = gitRepo();
336
+ stageChange(dir, 'src/__tests__/existing.test.ts', "test('a', () => {}\n");
337
+ passTraces(dir);
338
+
339
+ await check({ preset: 'standard', staged: true, projectPath: dir }, io);
279
340
 
280
- await check({ preset: 'default', staged: false });
281
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('file_modification'));
341
+ expect(io.outText()).toContain('触发条件: test_creation, code_implementation');
282
342
  });
283
343
 
284
- it('应该检测 module_modification 触发条件', async () => {
285
- // src 目录下有非测试文件变更
286
- mockExecAsync.mockResolvedValue({ stdout: 'src/module.ts\nsrc/__tests__/module.test.ts\n', stderr: '' });
287
- mockChecker.checkConstraints.mockResolvedValue({
288
- passed: true,
289
- ironLaws: [],
290
- guidelines: [],
291
- warningCount: 0,
292
- });
344
+ it('源码根外的文档变更 file_modification(不附加 code_implementation)', async () => {
345
+ const dir = gitRepo();
346
+ write(dir, 'docs/design.md', '# design\n');
347
+ git(dir, 'add', 'docs/design.md');
293
348
 
294
- // Mock filesystem so detectSourceRoots finds src/ as a source root
295
- const mockProjectPath = '/mock-project';
296
- mockFs.existsSync.mockImplementation((p: any) => {
297
- if (p === `${mockProjectPath}/src`) return true;
298
- return false;
299
- });
300
- mockFs.readdirSync.mockImplementation((dir: any) => {
301
- if (dir === `${mockProjectPath}/src`) {
302
- return [{ name: 'module.ts', isFile: () => true, isDirectory: () => false }] as any;
303
- }
304
- return [];
305
- });
349
+ await check({ preset: 'standard', staged: true, projectPath: dir }, io);
306
350
 
307
- await check({ preset: 'default', staged: false, projectPath: mockProjectPath });
308
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('module_modification'));
351
+ expect(io.outText()).toContain('触发条件: file_modification');
309
352
  });
310
353
 
311
- it('应该使用指定的触发条件', async () => {
312
- mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' });
313
- mockChecker.checkConstraints.mockResolvedValue({
314
- passed: true,
315
- ironLaws: [],
316
- guidelines: [],
317
- warningCount: 0,
318
- });
319
-
320
- await check({ preset: 'default', staged: false, trigger: 'code_implementation' });
321
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('code_implementation'));
322
- });
354
+ it('显式 --trigger 优先于推断', async () => {
355
+ const dir = gitRepo();
356
+ stageChange(dir, 'src/existing.ts', 'export const a = 2;\n');
357
+ passTraces(dir);
323
358
 
324
- it('应该显示铁律违规的约束详情', async () => {
325
- mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' });
326
- mockChecker.checkConstraints.mockResolvedValue({
327
- passed: false,
328
- ironLaws: [{
329
- id: 'no_bypass_checkpoint',
330
- level: 'iron_law',
331
- satisfied: false,
332
- checkedAt: new Date(),
333
- constraint: {
334
- kind: 'check' as const,
335
- id: 'no_bypass_checkpoint',
336
- rule: 'NO BYPASSING CHECKPOINTS',
337
- message: '禁止跳过检查点验证',
338
- level: 'iron_law',
339
- trigger: 'code_implementation',
340
- enforcement: 'checkpoint-required',
341
- },
342
- }],
343
- guidelines: [],
344
- warningCount: 0,
345
- });
359
+ await check(
360
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'design_request' },
361
+ io
362
+ );
346
363
 
347
- const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
348
- await check({ preset: 'default', staged: false });
349
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('no_bypass_checkpoint'));
350
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('NO BYPASSING CHECKPOINTS'));
351
- mockExit.mockRestore();
364
+ expect(io.outText()).toContain('触发条件: design_request');
365
+ expect(io.outText()).not.toContain('module_modification');
352
366
  });
367
+ });
353
368
 
354
- it('应该显示指导原则的约束详情', async () => {
355
- mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' });
356
- mockChecker.checkConstraints.mockResolvedValue({
357
- passed: true,
358
- ironLaws: [],
359
- guidelines: [{
360
- id: 'prefer_composition',
361
- level: 'guideline',
362
- satisfied: false,
363
- checkedAt: new Date(),
364
- constraint: {
365
- kind: 'check' as const,
366
- id: 'prefer_composition',
367
- rule: 'PREFER COMPOSITION OVER INHERITANCE',
368
- message: '优先使用组合而非继承',
369
- level: 'guideline',
370
- trigger: 'code_implementation',
371
- enforcement: 'warning',
372
- },
373
- }],
374
- warningCount: 1,
375
- });
369
+ describe('生效集来自真 .harness/config.yml', () => {
370
+ it('报告自定义、禁用与未知约束 id', async () => {
371
+ const dir = gitRepo();
372
+ stageChange(dir, 'src/existing.ts', 'export const a = 2;\n');
373
+ passTraces(dir);
374
+ write(
375
+ dir,
376
+ '.harness/config.yml',
377
+ [
378
+ 'custom_constraints:',
379
+ ' fixture_rule:',
380
+ ' id: fixture_rule',
381
+ ' level: guideline',
382
+ ' trigger: manual',
383
+ ' message: fixture 自定义约束',
384
+ 'constraints:',
385
+ ' capability_sync:',
386
+ ' enabled: false',
387
+ ' no_such_rule:',
388
+ ' enabled: false',
389
+ '',
390
+ ].join('\n')
391
+ );
376
392
 
377
- await check({ preset: 'default', staged: false });
378
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('prefer_composition'));
379
- });
393
+ await check(
394
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'code_implementation' },
395
+ io
396
+ );
380
397
 
398
+ expect(io.outText()).toContain('自定义约束: 1 条');
399
+ expect(io.outText()).toContain('已禁用约束: capability_sync, no_such_rule');
400
+ expect(io.outText()).toContain('配置中存在未知约束 id(已忽略,可清理): no_such_rule');
401
+ });
381
402
  });
382
403
 
383
- describe('getSmartHint (通过 check 间接测试)', () => {
384
- beforeEach(() => {
385
- mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' });
386
- mockChecker.checkConstraints.mockResolvedValue({
387
- passed: true,
388
- ironLaws: [],
389
- guidelines: [],
390
- warningCount: 0,
391
- });
392
- });
404
+ describe('智能提示(真 traces.log 与状态文件)', () => {
405
+ it('记录数首次达到 50 时提示,并落盘已提示状态', async () => {
406
+ const dir = gitRepo();
407
+ passTraces(dir, 50);
393
408
 
394
- it('应该在 trace 不存在时无提示', async () => {
395
- mockFs.existsSync.mockReturnValue(false);
409
+ await check({ preset: 'standard', staged: true, projectPath: dir, trigger: 'manual' }, io);
396
410
 
397
- await check({ preset: 'default', staged: false });
398
- // 不应有提示分隔线
399
- const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
400
- expect(output).not.toContain('────────────────');
411
+ expect(io.outText()).toContain('记录已足够,运行 harness status 查看统计');
412
+ expect(io.outText()).toContain('────────────────');
413
+ const state = JSON.parse(
414
+ fs.readFileSync(path.join(dir, '.harness', '.state.json'), 'utf-8')
415
+ );
416
+ expect(state.shownHints).toContain('trace_50');
401
417
  });
402
418
 
403
- it('应该在 trace 数达到 50 时提示查看统计', async () => {
404
- const traces = Array(50).fill('{"result":"pass"}').join('\n');
405
- mockFs.existsSync.mockImplementation((p: any) => {
406
- if (p.includes('traces.log')) return true;
407
- if (p.includes('.state.json')) return false;
408
- return false;
409
- });
410
- mockFs.readFileSync.mockReturnValue(traces);
419
+ it('已提示过的不再重复提示', async () => {
420
+ const dir = gitRepo();
421
+ passTraces(dir, 50);
422
+ write(dir, '.harness/.state.json', JSON.stringify({ shownHints: ['trace_50'] }));
411
423
 
412
- await check({ preset: 'default', staged: false });
413
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('记录已足够'));
414
- });
424
+ await check({ preset: 'standard', staged: true, projectPath: dir, trigger: 'manual' }, io);
415
425
 
416
- it('应该不重复显示已显示的提示', async () => {
417
- const traces = Array(50).fill('{"result":"pass"}').join('\n');
418
- mockFs.existsSync.mockReturnValue(true);
419
- mockFs.readFileSync.mockImplementation((p: any) => {
420
- if (p.includes('.state.json')) {
421
- return JSON.stringify({ shownHints: ['trace_50'] });
422
- }
423
- return traces;
424
- });
425
-
426
- await check({ preset: 'default', staged: false });
427
- const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
428
- expect(output).not.toContain('记录已足够');
426
+ expect(io.outText()).not.toContain('记录已足够');
427
+ expect(io.outText()).not.toContain('────────────────');
429
428
  });
430
429
 
431
- it('应该在 trace 数少于 50 时不提示', async () => {
432
- const traces = Array(10).fill('{"result":"pass"}').join('\n');
433
- mockFs.existsSync.mockImplementation((p: any) => {
434
- if (p.includes('traces.log')) return true;
435
- if (p.includes('.state.json')) return false;
436
- return false;
437
- });
438
- mockFs.readFileSync.mockReturnValue(traces);
430
+ it('记录数不足 50 时无提示', async () => {
431
+ const dir = gitRepo();
432
+ passTraces(dir, 10);
439
433
 
440
- await check({ preset: 'default', staged: false });
441
- const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
442
- expect(output).not.toContain('────────────────');
434
+ await check({ preset: 'standard', staged: true, projectPath: dir, trigger: 'manual' }, io);
435
+
436
+ expect(io.outText()).not.toContain('────────────────');
443
437
  });
444
438
 
445
- it('应该保存状态文件当有提示时', async () => {
446
- const traces = Array(50).fill('{"result":"pass"}').join('\n');
447
- mockFs.existsSync.mockImplementation((p: any) => {
448
- if (p.includes('traces.log')) return true;
449
- if (p.includes('.state.json')) return false;
450
- return false;
451
- });
452
- mockFs.readFileSync.mockReturnValue(traces);
439
+ it('无 trace 记录时无提示', async () => {
440
+ const dir = gitRepo();
453
441
 
454
- await check({ preset: 'default', staged: false });
455
- expect(mockFs.writeFileSync).toHaveBeenCalled();
456
- expect(mockFs.mkdirSync).toHaveBeenCalled();
442
+ await check({ preset: 'standard', staged: true, projectPath: dir, trigger: 'manual' }, io);
443
+
444
+ expect(io.outText()).not.toContain('────────────────');
457
445
  });
458
446
  });
459
447
 
460
448
  describe('listLaws', () => {
461
449
  it('应该列出所有约束', () => {
462
- listLaws();
463
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('所有约束'));
450
+ const result = listLaws({}, io);
451
+
452
+ expect(io.outText()).toContain('所有约束');
453
+ expect(result).toEqual({ kind: 'ok' });
464
454
  });
465
455
 
466
456
  it('应该列出铁律', () => {
467
- listLaws();
468
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('铁律'));
457
+ const result = listLaws({}, io);
458
+
459
+ expect(io.outText()).toContain('铁律');
460
+ expect(result).toEqual({ kind: 'ok' });
469
461
  });
470
462
 
471
463
  it('应该列出指导原则', () => {
472
- listLaws();
473
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('指导原则'));
464
+ const result = listLaws({}, io);
465
+
466
+ expect(io.outText()).toContain('指导原则');
467
+ expect(result).toEqual({ kind: 'ok' });
474
468
  });
475
469
 
476
470
  it('应该列出提示', () => {
477
- listLaws();
478
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('提示'));
471
+ const result = listLaws({}, io);
472
+
473
+ expect(io.outText()).toContain('提示');
474
+ expect(result).toEqual({ kind: 'ok' });
479
475
  });
480
476
  });
481
477
  });