@dommaker/harness 1.3.0 → 1.5.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 (532) hide show
  1. package/CHANGELOG.md +20 -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 +104 -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 +27 -219
  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 +17 -30
  53. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  54. package/dist/cli/commands/knowledge.js +183 -273
  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 +9 -3
  115. package/dist/cli/commands/update-user-model.d.ts.map +1 -1
  116. package/dist/cli/commands/update-user-model.js +56 -61
  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 +68 -117
  143. package/dist/core/constraints/checker.js.map +1 -1
  144. package/dist/core/constraints/checkers/capability-sync.d.ts +20 -8
  145. package/dist/core/constraints/checkers/capability-sync.d.ts.map +1 -1
  146. package/dist/core/constraints/checkers/capability-sync.js +66 -77
  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 +49 -66
  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/index.d.ts +2 -2
  160. package/dist/core/constraints/checkers/index.d.ts.map +1 -1
  161. package/dist/core/constraints/checkers/index.js +2 -1
  162. package/dist/core/constraints/checkers/index.js.map +1 -1
  163. package/dist/core/constraints/checkers/no-test-simplification.d.ts.map +1 -1
  164. package/dist/core/constraints/checkers/no-test-simplification.js +4 -3
  165. package/dist/core/constraints/checkers/no-test-simplification.js.map +1 -1
  166. package/dist/core/constraints/checkers/types.d.ts +43 -4
  167. package/dist/core/constraints/checkers/types.d.ts.map +1 -1
  168. package/dist/core/constraints/checkers/types.js +33 -0
  169. package/dist/core/constraints/checkers/types.js.map +1 -1
  170. package/dist/core/constraints/context-builder.d.ts +15 -14
  171. package/dist/core/constraints/context-builder.d.ts.map +1 -1
  172. package/dist/core/constraints/context-builder.js +32 -96
  173. package/dist/core/constraints/context-builder.js.map +1 -1
  174. package/dist/core/constraints/doc-freshness/runner.d.ts.map +1 -1
  175. package/dist/core/constraints/doc-freshness/runner.js +2 -1
  176. package/dist/core/constraints/doc-freshness/runner.js.map +1 -1
  177. package/dist/core/constraints/git-evidence.d.ts +60 -0
  178. package/dist/core/constraints/git-evidence.d.ts.map +1 -0
  179. package/dist/core/constraints/git-evidence.js +88 -0
  180. package/dist/core/constraints/git-evidence.js.map +1 -0
  181. package/dist/core/constraints/injection-drift.d.ts +3 -20
  182. package/dist/core/constraints/injection-drift.d.ts.map +1 -1
  183. package/dist/core/constraints/injection-drift.js +10 -57
  184. package/dist/core/constraints/injection-drift.js.map +1 -1
  185. package/dist/core/constraints/injection-writer.d.ts +113 -0
  186. package/dist/core/constraints/injection-writer.d.ts.map +1 -0
  187. package/dist/core/constraints/injection-writer.js +228 -0
  188. package/dist/core/constraints/injection-writer.js.map +1 -0
  189. package/dist/core/constraints/usage-report.d.ts +22 -1
  190. package/dist/core/constraints/usage-report.d.ts.map +1 -1
  191. package/dist/core/constraints/usage-report.js +27 -20
  192. package/dist/core/constraints/usage-report.js.map +1 -1
  193. package/dist/core/index.d.ts +2 -2
  194. package/dist/core/index.d.ts.map +1 -1
  195. package/dist/core/index.js +4 -2
  196. package/dist/core/index.js.map +1 -1
  197. package/dist/core/project-config-loader.d.ts +32 -1
  198. package/dist/core/project-config-loader.d.ts.map +1 -1
  199. package/dist/core/project-config-loader.js +39 -9
  200. package/dist/core/project-config-loader.js.map +1 -1
  201. package/dist/core/validators/passes-gate.d.ts +15 -64
  202. package/dist/core/validators/passes-gate.d.ts.map +1 -1
  203. package/dist/core/validators/passes-gate.js +84 -222
  204. package/dist/core/validators/passes-gate.js.map +1 -1
  205. package/dist/core/validators/test-output.d.ts +58 -0
  206. package/dist/core/validators/test-output.d.ts.map +1 -0
  207. package/dist/core/validators/test-output.js +109 -0
  208. package/dist/core/validators/test-output.js.map +1 -0
  209. package/dist/failure/recorder.d.ts +6 -0
  210. package/dist/failure/recorder.d.ts.map +1 -1
  211. package/dist/failure/recorder.js +12 -8
  212. package/dist/failure/recorder.js.map +1 -1
  213. package/dist/gates/acceptance.d.ts +1 -48
  214. package/dist/gates/acceptance.d.ts.map +1 -1
  215. package/dist/gates/acceptance.js +6 -38
  216. package/dist/gates/acceptance.js.map +1 -1
  217. package/dist/gates/checker-gate.d.ts +2 -1
  218. package/dist/gates/checker-gate.d.ts.map +1 -1
  219. package/dist/gates/checker-gate.js +11 -13
  220. package/dist/gates/checker-gate.js.map +1 -1
  221. package/dist/gates/command.d.ts.map +1 -1
  222. package/dist/gates/command.js +10 -22
  223. package/dist/gates/command.js.map +1 -1
  224. package/dist/gates/contract.d.ts.map +1 -1
  225. package/dist/gates/contract.js +22 -61
  226. package/dist/gates/contract.js.map +1 -1
  227. package/dist/gates/performance.d.ts.map +1 -1
  228. package/dist/gates/performance.js +13 -31
  229. package/dist/gates/performance.js.map +1 -1
  230. package/dist/gates/review.d.ts.map +1 -1
  231. package/dist/gates/review.js +21 -53
  232. package/dist/gates/review.js.map +1 -1
  233. package/dist/gates/security.d.ts.map +1 -1
  234. package/dist/gates/security.js +14 -32
  235. package/dist/gates/security.js.map +1 -1
  236. package/dist/gates/types.d.ts +28 -5
  237. package/dist/gates/types.d.ts.map +1 -1
  238. package/dist/gates/types.js +38 -1
  239. package/dist/gates/types.js.map +1 -1
  240. package/dist/hooks/bootstrap.d.ts +2 -0
  241. package/dist/hooks/bootstrap.d.ts.map +1 -1
  242. package/dist/hooks/bootstrap.js +5 -2
  243. package/dist/hooks/bootstrap.js.map +1 -1
  244. package/dist/index.d.ts +1 -2
  245. package/dist/index.d.ts.map +1 -1
  246. package/dist/index.js.map +1 -1
  247. package/dist/knowledge/audit.d.ts +7 -2
  248. package/dist/knowledge/audit.d.ts.map +1 -1
  249. package/dist/knowledge/audit.js +59 -79
  250. package/dist/knowledge/audit.js.map +1 -1
  251. package/dist/knowledge/flywheel-metrics.d.ts +39 -0
  252. package/dist/knowledge/flywheel-metrics.d.ts.map +1 -0
  253. package/dist/knowledge/flywheel-metrics.js +47 -0
  254. package/dist/knowledge/flywheel-metrics.js.map +1 -0
  255. package/dist/knowledge/import.d.ts.map +1 -1
  256. package/dist/knowledge/import.js +4 -14
  257. package/dist/knowledge/import.js.map +1 -1
  258. package/dist/knowledge/index-generator.d.ts.map +1 -1
  259. package/dist/knowledge/index-generator.js +17 -14
  260. package/dist/knowledge/index-generator.js.map +1 -1
  261. package/dist/knowledge/ingest.d.ts.map +1 -1
  262. package/dist/knowledge/ingest.js +9 -4
  263. package/dist/knowledge/ingest.js.map +1 -1
  264. package/dist/knowledge/migration.d.ts.map +1 -1
  265. package/dist/knowledge/migration.js +11 -8
  266. package/dist/knowledge/migration.js.map +1 -1
  267. package/dist/knowledge/reference-tracker.d.ts.map +1 -1
  268. package/dist/knowledge/reference-tracker.js +12 -9
  269. package/dist/knowledge/reference-tracker.js.map +1 -1
  270. package/dist/knowledge/store.d.ts +15 -0
  271. package/dist/knowledge/store.d.ts.map +1 -1
  272. package/dist/knowledge/store.js +59 -10
  273. package/dist/knowledge/store.js.map +1 -1
  274. package/dist/monitoring/context-tracker.d.ts.map +1 -1
  275. package/dist/monitoring/context-tracker.js +11 -20
  276. package/dist/monitoring/context-tracker.js.map +1 -1
  277. package/dist/monitoring/index.d.ts +1 -1
  278. package/dist/monitoring/index.d.ts.map +1 -1
  279. package/dist/monitoring/index.js +1 -2
  280. package/dist/monitoring/index.js.map +1 -1
  281. package/dist/monitoring/trace-analyzer.d.ts +15 -0
  282. package/dist/monitoring/trace-analyzer.d.ts.map +1 -1
  283. package/dist/monitoring/trace-analyzer.js +19 -2
  284. package/dist/monitoring/trace-analyzer.js.map +1 -1
  285. package/dist/monitoring/traces.d.ts +21 -2
  286. package/dist/monitoring/traces.d.ts.map +1 -1
  287. package/dist/monitoring/traces.js +38 -29
  288. package/dist/monitoring/traces.js.map +1 -1
  289. package/dist/release/index.d.ts +2 -2
  290. package/dist/release/index.d.ts.map +1 -1
  291. package/dist/release/index.js +1 -5
  292. package/dist/release/index.js.map +1 -1
  293. package/dist/sdd/index-generator.js +10 -11
  294. package/dist/sdd/index-generator.js.map +1 -1
  295. package/dist/test-setup/project-fixture.d.ts +48 -0
  296. package/dist/test-setup/project-fixture.d.ts.map +1 -0
  297. package/dist/test-setup/project-fixture.js +106 -0
  298. package/dist/test-setup/project-fixture.js.map +1 -0
  299. package/dist/types/constraint.d.ts +8 -18
  300. package/dist/types/constraint.d.ts.map +1 -1
  301. package/dist/types/constraint.js +6 -1
  302. package/dist/types/constraint.js.map +1 -1
  303. package/dist/types/passes-gate.d.ts +10 -25
  304. package/dist/types/passes-gate.d.ts.map +1 -1
  305. package/dist/types/project-config.d.ts +1 -1
  306. package/dist/types/session.d.ts +6 -5
  307. package/dist/types/session.d.ts.map +1 -1
  308. package/dist/types/trace.d.ts +9 -0
  309. package/dist/types/trace.d.ts.map +1 -1
  310. package/dist/utils/exec.d.ts +10 -0
  311. package/dist/utils/exec.d.ts.map +1 -1
  312. package/dist/utils/exec.js +12 -0
  313. package/dist/utils/exec.js.map +1 -1
  314. package/dist/utils/file-walk.d.ts +9 -1
  315. package/dist/utils/file-walk.d.ts.map +1 -1
  316. package/dist/utils/file-walk.js +10 -1
  317. package/dist/utils/file-walk.js.map +1 -1
  318. package/dist/utils/frontmatter.d.ts +73 -0
  319. package/dist/utils/frontmatter.d.ts.map +1 -0
  320. package/dist/utils/frontmatter.js +141 -0
  321. package/dist/utils/frontmatter.js.map +1 -0
  322. package/dist/utils/jsonl.d.ts +92 -0
  323. package/dist/utils/jsonl.d.ts.map +1 -0
  324. package/dist/utils/jsonl.js +166 -0
  325. package/dist/utils/jsonl.js.map +1 -0
  326. package/dist/utils/package-version.d.ts +16 -0
  327. package/dist/utils/package-version.d.ts.map +1 -0
  328. package/dist/utils/package-version.js +63 -0
  329. package/dist/utils/package-version.js.map +1 -0
  330. package/package.json +1 -1
  331. package/src/CONTEXT.md +8 -2
  332. package/src/__tests__/acceptance-gate.test.ts +3 -1
  333. package/src/__tests__/checker-extra.test.ts +7 -1218
  334. package/src/__tests__/context-files-resolution.test.ts +116 -0
  335. package/src/__tests__/effective-constraints.test.ts +10 -13
  336. package/src/__tests__/exec.test.ts +35 -1
  337. package/src/__tests__/layering.test.ts +152 -0
  338. package/src/__tests__/passes-gate-extra.test.ts +0 -145
  339. package/src/__tests__/passes-gate.test.ts +130 -101
  340. package/src/__tests__/skip-semantics.test.ts +5 -9
  341. package/src/cli/__tests__/command-contract.test.ts +170 -0
  342. package/src/cli/command-contract.ts +108 -0
  343. package/src/cli/commands/CONTEXT.md +23 -8
  344. package/src/cli/commands/__tests__/acceptance.test.ts +40 -33
  345. package/src/cli/commands/__tests__/analyze-sessions.test.ts +35 -29
  346. package/src/cli/commands/__tests__/bin-exit-mapping.test.ts +107 -0
  347. package/src/cli/commands/__tests__/check-drift.test.ts +27 -34
  348. package/src/cli/commands/__tests__/check-skip-output.test.ts +21 -24
  349. package/src/cli/commands/__tests__/check.test.ts +448 -378
  350. package/src/cli/commands/__tests__/command.test.ts +85 -70
  351. package/src/cli/commands/__tests__/constraints-report.test.ts +100 -71
  352. package/src/cli/commands/__tests__/constraints-retire.test.ts +152 -95
  353. package/src/cli/commands/__tests__/contract.test.ts +27 -34
  354. package/src/cli/commands/__tests__/failure.test.ts +31 -34
  355. package/src/cli/commands/__tests__/init-injection.test.ts +58 -28
  356. package/src/cli/commands/__tests__/init.test.ts +77 -48
  357. package/src/cli/commands/__tests__/knowledge-flywheel-consistency.test.ts +108 -0
  358. package/src/cli/commands/__tests__/knowledge.test.ts +69 -43
  359. package/src/cli/commands/__tests__/passes-gate.test.ts +43 -45
  360. package/src/cli/commands/__tests__/performance.test.ts +22 -23
  361. package/src/cli/commands/__tests__/posteval-plan.test.ts +39 -38
  362. package/src/cli/commands/__tests__/project-path-anchoring.test.ts +157 -0
  363. package/src/cli/commands/__tests__/project-path-convention.test.ts +231 -0
  364. package/src/cli/commands/__tests__/registry.test.ts +75 -35
  365. package/src/cli/commands/__tests__/release.test.ts +209 -80
  366. package/src/cli/commands/__tests__/report-extra.test.ts +16 -16
  367. package/src/cli/commands/__tests__/report.test.ts +25 -25
  368. package/src/cli/commands/__tests__/review.test.ts +33 -31
  369. package/src/cli/commands/__tests__/security.test.ts +23 -27
  370. package/src/cli/commands/__tests__/spec-baseline-check.test.ts +20 -20
  371. package/src/cli/commands/__tests__/spec.test.ts +41 -29
  372. package/src/cli/commands/__tests__/status-extra.test.ts +18 -18
  373. package/src/cli/commands/__tests__/status-skipped-lines.test.ts +72 -0
  374. package/src/cli/commands/__tests__/status.test.ts +34 -34
  375. package/src/cli/commands/__tests__/sync-docs-agents.test.ts +77 -78
  376. package/src/cli/commands/__tests__/sync-docs.test.ts +72 -74
  377. package/src/cli/commands/__tests__/update-user-model.test.ts +97 -34
  378. package/src/cli/commands/__tests__/validate.test.ts +27 -29
  379. package/src/cli/commands/acceptance.ts +35 -24
  380. package/src/cli/commands/analyze-sessions.ts +25 -24
  381. package/src/cli/commands/check.ts +127 -66
  382. package/src/cli/commands/command.ts +30 -17
  383. package/src/cli/commands/constraints-report.ts +37 -34
  384. package/src/cli/commands/constraints-retire.ts +58 -55
  385. package/src/cli/commands/constraints.ts +10 -27
  386. package/src/cli/commands/contract.ts +51 -42
  387. package/src/cli/commands/definitions.ts +34 -227
  388. package/src/cli/commands/failure.ts +33 -26
  389. package/src/cli/commands/init.ts +166 -220
  390. package/src/cli/commands/knowledge.ts +192 -289
  391. package/src/cli/commands/passes-gate.ts +58 -68
  392. package/src/cli/commands/performance.ts +23 -18
  393. package/src/cli/commands/posteval-plan.ts +29 -15
  394. package/src/cli/commands/release.ts +70 -59
  395. package/src/cli/commands/report.ts +9 -5
  396. package/src/cli/commands/review.ts +39 -29
  397. package/src/cli/commands/sdd.ts +7 -5
  398. package/src/cli/commands/security.ts +35 -26
  399. package/src/cli/commands/spec-baseline-check.ts +16 -11
  400. package/src/cli/commands/spec.ts +66 -53
  401. package/src/cli/commands/status.ts +59 -53
  402. package/src/cli/commands/sync-docs/agents-syncer.ts +5 -2
  403. package/src/cli/commands/sync-docs/context-syncer.ts +3 -2
  404. package/src/cli/commands/sync-docs/index.ts +73 -100
  405. package/src/cli/commands/sync-docs/project-reader.ts +17 -18
  406. package/src/cli/commands/update-user-model.ts +64 -61
  407. package/src/cli/commands/validate.ts +31 -25
  408. package/src/cli/session-mining/__tests__/transcript.test.ts +118 -0
  409. package/src/cli/session-mining/transcript.ts +26 -8
  410. package/src/context/session-manager.ts +19 -33
  411. package/src/core/CONTEXT.md +10 -6
  412. package/src/core/constraints/__tests__/capabilities-reconcile.test.ts +200 -0
  413. package/src/core/constraints/__tests__/capability-counts.test.ts +94 -0
  414. package/src/core/constraints/__tests__/git-evidence.test.ts +208 -0
  415. package/src/core/constraints/__tests__/injection-drift.test.ts +3 -10
  416. package/src/core/constraints/__tests__/injection-writer.test.ts +231 -0
  417. package/src/core/constraints/__tests__/trace-injection.test.ts +71 -0
  418. package/src/core/constraints/__tests__/usage-report.test.ts +112 -0
  419. package/src/core/constraints/agent-prompt-renderer.ts +2 -6
  420. package/src/core/constraints/capabilities-parser.ts +53 -35
  421. package/src/core/constraints/capabilities-reconcile.ts +166 -0
  422. package/src/core/constraints/checker.ts +82 -133
  423. package/src/core/constraints/checkers/__tests__/capability-sync.test.ts +273 -0
  424. package/src/core/constraints/checkers/__tests__/check-outcome.test.ts +176 -0
  425. package/src/core/constraints/checkers/__tests__/context-doc-sync.test.ts +77 -0
  426. package/src/core/constraints/checkers/__tests__/docs-freshness.test.ts +126 -0
  427. package/src/{__tests__ → core/constraints/checkers/__tests__}/governance-presence.test.ts +83 -9
  428. package/src/core/constraints/checkers/__tests__/iron-flags.test.ts +50 -0
  429. package/src/core/constraints/checkers/__tests__/no-bypass-checkpoint.test.ts +76 -0
  430. package/src/{__tests__ → core/constraints/checkers/__tests__}/no-hardcoded-credentials.test.ts +4 -3
  431. package/src/core/constraints/checkers/__tests__/no-test-simplification.test.ts +58 -0
  432. package/src/core/constraints/checkers/__tests__/three-state-consistency.test.ts +153 -0
  433. package/src/core/constraints/checkers/capability-sync.ts +75 -96
  434. package/src/core/constraints/checkers/context-doc-sync.ts +15 -25
  435. package/src/core/constraints/checkers/docs-freshness.ts +54 -70
  436. package/src/core/constraints/checkers/governance-presence.ts +7 -25
  437. package/src/core/constraints/checkers/index.ts +9 -2
  438. package/src/core/constraints/checkers/no-test-simplification.ts +4 -3
  439. package/src/core/constraints/checkers/types.ts +60 -4
  440. package/src/core/constraints/context-builder.ts +39 -91
  441. package/src/core/constraints/doc-freshness/runner.ts +2 -1
  442. package/src/core/constraints/git-evidence.ts +120 -0
  443. package/src/core/constraints/injection-drift.ts +13 -60
  444. package/src/core/constraints/injection-writer.ts +212 -0
  445. package/src/core/constraints/usage-report.ts +36 -17
  446. package/src/core/index.ts +4 -2
  447. package/src/core/project-config-loader.ts +49 -9
  448. package/src/core/validators/__tests__/passes-gate.test.ts +2 -56
  449. package/src/core/validators/__tests__/test-output.test.ts +372 -0
  450. package/src/core/validators/passes-gate.ts +86 -248
  451. package/src/core/validators/test-output.ts +128 -0
  452. package/src/failure/__tests__/recorder.test.ts +168 -0
  453. package/src/failure/recorder.ts +12 -10
  454. package/src/gates/CONTEXT.md +7 -4
  455. package/src/gates/__tests__/acceptance-types-single-source.test.ts +68 -0
  456. package/src/gates/__tests__/acceptance.test.ts +4 -86
  457. package/src/gates/__tests__/checker-gate.test.ts +24 -0
  458. package/src/gates/__tests__/effective-gates.test.ts +3 -8
  459. package/src/gates/__tests__/test-verdict-consistency.test.ts +140 -0
  460. package/src/gates/acceptance.ts +19 -84
  461. package/src/gates/checker-gate.ts +12 -15
  462. package/src/gates/command.ts +10 -16
  463. package/src/gates/contract.ts +22 -61
  464. package/src/gates/performance.ts +10 -22
  465. package/src/gates/review.ts +21 -41
  466. package/src/gates/security.ts +10 -22
  467. package/src/gates/types.ts +70 -5
  468. package/src/hooks/CONTEXT.md +1 -1
  469. package/src/hooks/bootstrap.ts +5 -2
  470. package/src/index.ts +0 -4
  471. package/src/knowledge/CONTEXT.md +8 -3
  472. package/src/knowledge/__tests__/audit-labels.test.ts +44 -0
  473. package/src/knowledge/__tests__/flywheel-metrics.test.ts +135 -0
  474. package/src/knowledge/__tests__/import.test.ts +44 -0
  475. package/src/knowledge/__tests__/index-generator.test.ts +53 -0
  476. package/src/knowledge/__tests__/ingest.test.ts +85 -0
  477. package/src/knowledge/__tests__/migration.test.ts +47 -0
  478. package/src/knowledge/__tests__/reference-tracker.test.ts +16 -0
  479. package/src/knowledge/__tests__/store.test.ts +194 -0
  480. package/src/knowledge/audit.ts +67 -71
  481. package/src/knowledge/flywheel-metrics.ts +71 -0
  482. package/src/knowledge/import.ts +4 -18
  483. package/src/knowledge/index-generator.ts +19 -14
  484. package/src/knowledge/ingest.ts +9 -4
  485. package/src/knowledge/migration.ts +11 -8
  486. package/src/knowledge/reference-tracker.ts +12 -8
  487. package/src/knowledge/store.ts +64 -11
  488. package/src/monitoring/CONTEXT.md +8 -3
  489. package/src/monitoring/__tests__/skipped-lines-report.test.ts +178 -0
  490. package/src/monitoring/__tests__/traces.test.ts +57 -0
  491. package/src/monitoring/context-tracker.ts +11 -23
  492. package/src/monitoring/index.ts +1 -1
  493. package/src/monitoring/trace-analyzer.ts +21 -2
  494. package/src/monitoring/traces.ts +39 -36
  495. package/src/release/CONTEXT.md +1 -0
  496. package/src/release/__tests__/barrel.test.ts +60 -0
  497. package/src/release/index.ts +2 -13
  498. package/src/sdd/__tests__/index-generator.test.ts +71 -0
  499. package/src/sdd/index-generator.ts +9 -9
  500. package/src/test-setup/__tests__/project-fixture.test.ts +178 -0
  501. package/src/test-setup/project-fixture.ts +87 -0
  502. package/src/tools/CONTEXT.md +6 -0
  503. package/src/tools/__tests__/registry.test.ts +221 -0
  504. package/src/types/__tests__/types.test.ts +4 -5
  505. package/src/types/constraint.ts +16 -27
  506. package/src/types/passes-gate.ts +10 -30
  507. package/src/types/project-config.ts +1 -1
  508. package/src/types/session.ts +5 -5
  509. package/src/types/trace.ts +10 -0
  510. package/src/utils/__tests__/frontmatter.test.ts +206 -0
  511. package/src/utils/__tests__/jsonl-skip-disposition.test.ts +175 -0
  512. package/src/utils/__tests__/jsonl.test.ts +187 -0
  513. package/src/utils/__tests__/package-version.test.ts +97 -0
  514. package/src/utils/exec.ts +15 -0
  515. package/src/utils/file-walk.ts +11 -2
  516. package/src/utils/frontmatter.ts +121 -0
  517. package/src/utils/jsonl.ts +167 -0
  518. package/src/utils/package-version.ts +27 -0
  519. package/dist/cli/commands/doc-freshness-check.d.ts +0 -53
  520. package/dist/cli/commands/doc-freshness-check.d.ts.map +0 -1
  521. package/dist/cli/commands/doc-freshness-check.js +0 -352
  522. package/dist/cli/commands/doc-freshness-check.js.map +0 -1
  523. package/dist/types/index.d.ts +0 -14
  524. package/dist/types/index.d.ts.map +0 -1
  525. package/dist/types/index.js +0 -39
  526. package/dist/types/index.js.map +0 -1
  527. package/src/__tests__/long-running.test.ts +0 -17
  528. package/src/__tests__/passes-gate-extension.test.ts +0 -151
  529. package/src/cli/commands/__tests__/doc-freshness-check.test.ts +0 -168
  530. package/src/cli/commands/doc-freshness-check.ts +0 -386
  531. package/src/spec/CONTEXT.md +0 -11
  532. 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
- }));
15
+ import * as os from 'os';
16
+ import * as path from 'path';
17
+ import { execFileSync } from 'child_process';
24
18
 
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
- }));
59
-
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,518 @@ 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
+ // 'pass'+'word' 拆串是躲本仓 no_hardcoded_credentials 静态扫描:测试源文件里出现完整
204
+ // `password = "..."` 字面量会撞自己的 pre-commit 门禁;运行时拼接值不变,断言不受影响
205
+ 'export const a = 1;\nconst pass' + 'word = "correcthorsebattery";\n'
206
+ );
207
+ passTraces(dir);
153
208
 
154
- await check({ preset: 'default', staged: false, projectPath: '/project' });
155
- expect(mockChecker.checkConstraints).toHaveBeenCalledWith(
156
- expect.anything(),
157
- expect.objectContaining({ disabled: ['disabled_constraint'] })
209
+ const result = await check(
210
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'code_implementation' },
211
+ io
158
212
  );
159
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('已禁用约束'));
213
+
214
+ expect(io.outText()).toContain('⚠️ 指导原则警告: 1 条');
215
+ expect(io.outText()).toContain('- no_hardcoded_credentials');
216
+ expect(io.outText()).toContain('✅ 约束检查通过');
217
+ expect(result).toEqual({ kind: 'ok' });
160
218
  });
161
219
 
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
- });
220
+ it('变更文件数量来自真 staged 名单', async () => {
221
+ const dir = gitRepo();
222
+ stageChange(dir, 'src/existing.ts', 'export const a = 2;\n');
223
+ stageChange(dir, 'src/nested/deep.ts', 'export const d = 2;\n');
224
+ passTraces(dir);
225
+
226
+ await check(
227
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'code_implementation' },
228
+ io
229
+ );
169
230
 
170
- await check({ preset: 'default', staged: false });
171
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('指导原则'));
231
+ expect(io.outText()).toContain('变更文件: 2 ');
172
232
  });
173
233
 
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
- });
234
+ it('非 git 目录:无变更文件行,检查照常完成', async () => {
235
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-check-nogit-'));
236
+ write(dir, 'README.md', '# fixture\n');
237
+
238
+ const result = await check({ preset: 'standard', staged: true, projectPath: dir }, io);
183
239
 
184
- await check({ preset: 'default', staged: true });
185
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('变更文件: 2 '));
240
+ expect(io.outText()).not.toContain('变更文件');
241
+ expect(result).toEqual({ kind: 'ok' });
186
242
  });
243
+ });
187
244
 
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
- });
245
+ describe('判定证据外显(harness#119)', () => {
246
+ /** CAPABILITIES.md git 仓库:登记列表外的源文件即为仓库级漂移 */
247
+ function capRepo(registered: string[]): string {
248
+ const dir = gitRepo();
249
+ const rows = registered
250
+ .map((rel) => `| ${path.basename(rel, '.ts')} | ${rel} | fixture |\n`)
251
+ .join('');
252
+ write(dir, 'CAPABILITIES.md', `# C\n\n| 模块 | 文件 | 说明 |\n|------|------|------|\n${rows}`);
253
+ git(dir, 'add', 'CAPABILITIES.md');
254
+ git(dir, 'commit', '-q', '-m', 'capabilities');
255
+ return dir;
256
+ }
196
257
 
197
- await check({ preset: 'default', staged: false });
198
- expect(mockExecAsync).toHaveBeenCalledWith('git diff --name-only');
199
- });
258
+ /** 从本用例 trace 文件里取某条约束的记录 */
259
+ function traceOf(constraintId: string) {
260
+ return fs
261
+ .readFileSync(traceFile, 'utf-8')
262
+ .split('\n')
263
+ .filter(Boolean)
264
+ .map((line) => JSON.parse(line) as { constraintId: string; result: string; evidence?: string[] })
265
+ .find((l) => l.constraintId === constraintId);
266
+ }
200
267
 
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
- });
268
+ it('变更文件未登记 警告随附证据点名该文件', async () => {
269
+ const dir = capRepo(['src/existing.ts', 'src/nested/deep.ts']);
270
+ stageChange(dir, 'src/nested/extra.ts', 'export const e = 1;\n');
271
+ passTraces(dir);
209
272
 
210
- await check({ preset: 'default', staged: true });
211
- expect(mockExecAsync).toHaveBeenCalledWith('git diff --cached --name-only');
273
+ const result = await check(
274
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'module_modification' },
275
+ io
276
+ );
277
+
278
+ expect(io.outText()).toContain('- capability_sync');
279
+ expect(io.outText()).toContain('src/nested/extra.ts');
280
+ expect(result).toEqual({ kind: 'ok' });
212
281
  });
213
282
 
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
- });
283
+ it('仓库级漂移(与本次变更无关)→ 提示块露出,不判违规', async () => {
284
+ const dir = capRepo(['src/existing.ts']);
285
+ passTraces(dir);
222
286
 
223
- await check({ preset: 'default', staged: false });
224
- // 不应显示变更文件数量
225
- expect(consoleSpy).not.toHaveBeenCalledWith(expect.stringContaining('变更文件'));
287
+ const result = await check(
288
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'module_modification' },
289
+ io
290
+ );
291
+
292
+ const out = io.outText();
293
+ expect(out).toContain('✅ 指导原则: 1/1 通过');
294
+ expect(out).not.toContain('指导原则警告');
295
+ expect(out).toContain('💡 提示: 1 条(不判违规,供参考)');
296
+ expect(out).toContain('- capability_sync:');
297
+ expect(out).toContain('src/nested/deep.ts');
298
+ expect(out).toContain('✅ 约束检查通过');
299
+ expect(result).toEqual({ kind: 'ok' });
226
300
  });
227
301
 
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
- });
302
+ it('提示与违规都写进 trace evidence 字段(统计侧可诊断)', async () => {
303
+ const dir = capRepo(['src/existing.ts']);
304
+ passTraces(dir);
233
305
 
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
- });
306
+ await check(
307
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'module_modification' },
308
+ io
309
+ );
246
310
 
247
- await check({ preset: 'default', staged: false, projectPath: mockProjectPath });
248
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('module_modification'));
311
+ const record = traceOf('capability_sync');
312
+ expect(record?.result).toBe('pass');
313
+ expect(record?.evidence?.join('\n')).toContain('src/nested/deep.ts');
249
314
  });
315
+ });
250
316
 
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
- });
256
-
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
- });
317
+ describe('git 证据单一来源(#87 验收)', () => {
318
+ it('两个 checker 请求同一份 staged diff → 一次 run 内只 spawn 一次', async () => {
319
+ // 新增(非删除)测试行:no_test_simplification 请求 stagedDiff 后放行,
320
+ // no_hardcoded_credentials 再请求同一份 只有铁律都通过时两者才同 run
321
+ const dir = gitRepo();
322
+ stageChange(dir, 'src/__tests__/added.test.ts', "test('a', () => {}\n");
323
+ passTraces(dir);
324
+ const { evidence, requests, commands } = recordingEvidence(dir);
325
+
326
+ const result = await check({ preset: 'standard', staged: true, projectPath: dir, evidence }, io);
327
+
328
+ expect(result).toEqual({ kind: 'ok' });
329
+ expect(requests.filter(r => r === 'stagedDiff').length).toBeGreaterThanOrEqual(2);
330
+ expect(commands.filter(c => c === 'git diff --cached')).toHaveLength(1);
331
+ expect(commands).toContain('git diff --cached --name-only');
332
+ expect(new Set(commands).size).toBe(commands.length);
333
+ });
269
334
 
270
- await check({ preset: 'default', staged: false, projectPath: mockProjectPath });
271
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('module_modification'));
335
+ it('--staged --cached 名单,非 staged 用工作区名单', async () => {
336
+ const stagedDir = gitRepo();
337
+ stageChange(stagedDir, 'src/existing.ts', 'export const a = 2;\n');
338
+ passTraces(stagedDir);
339
+ const staged = recordingEvidence(stagedDir);
340
+ await check(
341
+ { preset: 'standard', staged: true, projectPath: stagedDir, trigger: 'code_implementation', evidence: staged.evidence },
342
+ io
343
+ );
344
+ expect(staged.requests).toContain('changedFileNames:true');
345
+ expect(staged.commands).toContain('git diff --cached --name-only');
346
+ expect(staged.commands).not.toContain('git diff --name-only');
347
+
348
+ const worktreeDir = gitRepo();
349
+ write(worktreeDir, 'src/existing.ts', 'export const a = 3;\n');
350
+ passTraces(worktreeDir);
351
+ const worktree = recordingEvidence(worktreeDir);
352
+ await check(
353
+ { preset: 'standard', staged: false, projectPath: worktreeDir, trigger: 'code_implementation', evidence: worktree.evidence },
354
+ io
355
+ );
356
+ expect(worktree.requests).toContain('changedFileNames:false');
357
+ expect(worktree.commands).toContain('git diff --name-only');
358
+ expect(worktree.commands).not.toContain('git diff --cached --name-only');
272
359
  });
360
+ });
273
361
 
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
- });
362
+ describe('trace 记录器组合根接线(#88 验收)', () => {
363
+ it('一次 check 落盘每条约束的 trace(core 不上行依赖 monitoring,由命令侧接线)', async () => {
364
+ const dir = gitRepo();
365
+ stageChange(dir, 'src/existing.ts', 'export const a = 2;\n');
366
+ passTraces(dir);
279
367
 
280
- await check({ preset: 'default', staged: false });
281
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('file_modification'));
368
+ await check(
369
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'code_implementation' },
370
+ io
371
+ );
372
+
373
+ const traces = fs
374
+ .readFileSync(traceFile, 'utf-8')
375
+ .split('\n')
376
+ .filter(Boolean)
377
+ .map(l => JSON.parse(l) as { constraintId: string; result: string; projectPath: string });
378
+
379
+ expect(traces.map(t => t.constraintId)).toEqual(
380
+ expect.arrayContaining(['incremental_progress', 'no_implementation_without_requirement'])
381
+ );
382
+ expect(traces.every(t => ['pass', 'fail', 'skip'].includes(t.result))).toBe(true);
383
+ expect(traces.every(t => t.projectPath === dir)).toBe(true);
282
384
  });
385
+ });
283
386
 
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
- });
387
+ describe('触发条件推断(真 diff + 真 HEAD 目录集)', () => {
388
+ it('已存在目录内的代码变更 module_modification', async () => {
389
+ const dir = gitRepo();
390
+ stageChange(dir, 'src/existing.ts', 'export const a = 2;\n');
391
+ passTraces(dir);
293
392
 
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
- });
393
+ await check({ preset: 'standard', staged: true, projectPath: dir }, io);
306
394
 
307
- await check({ preset: 'default', staged: false, projectPath: mockProjectPath });
308
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('module_modification'));
395
+ expect(io.outText()).toContain('触发条件: module_modification, code_implementation');
309
396
  });
310
397
 
311
- it('应该使用指定的触发条件', async () => {
312
- mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' });
313
- mockChecker.checkConstraints.mockResolvedValue({
314
- passed: true,
315
- ironLaws: [],
316
- guidelines: [],
317
- warningCount: 0,
318
- });
398
+ it('源码根下新目录 → module_creation(HEAD 里没有该目录)', async () => {
399
+ const dir = gitRepo();
400
+ stageChange(dir, 'src/brandnew/mod.ts', 'export const n = 1;\n');
401
+ passTraces(dir);
319
402
 
320
- await check({ preset: 'default', staged: false, trigger: 'code_implementation' });
321
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('code_implementation'));
403
+ await check({ preset: 'standard', staged: true, projectPath: dir }, io);
404
+
405
+ expect(io.outText()).toContain('触发条件: module_creation, code_implementation');
322
406
  });
323
407
 
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
- });
408
+ it('仅测试变更 → test_creation', async () => {
409
+ const dir = gitRepo();
410
+ stageChange(dir, 'src/__tests__/existing.test.ts', "test('a', () => {}\n");
411
+ passTraces(dir);
412
+
413
+ await check({ preset: 'standard', staged: true, projectPath: dir }, io);
346
414
 
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();
415
+ expect(io.outText()).toContain('触发条件: test_creation, code_implementation');
352
416
  });
353
417
 
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
- });
418
+ it('源码根外的文档变更 → file_modification(不附加 code_implementation)', async () => {
419
+ const dir = gitRepo();
420
+ write(dir, 'docs/design.md', '# design\n');
421
+ git(dir, 'add', 'docs/design.md');
422
+
423
+ await check({ preset: 'standard', staged: true, projectPath: dir }, io);
376
424
 
377
- await check({ preset: 'default', staged: false });
378
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('prefer_composition'));
425
+ expect(io.outText()).toContain('触发条件: file_modification');
379
426
  });
380
427
 
381
- });
428
+ it('显式 --trigger 优先于推断', async () => {
429
+ const dir = gitRepo();
430
+ stageChange(dir, 'src/existing.ts', 'export const a = 2;\n');
431
+ passTraces(dir);
382
432
 
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
- });
433
+ await check(
434
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'design_request' },
435
+ io
436
+ );
437
+
438
+ expect(io.outText()).toContain('触发条件: design_request');
439
+ expect(io.outText()).not.toContain('module_modification');
392
440
  });
441
+ });
442
+
443
+ describe('生效集来自真 .harness/config.yml', () => {
444
+ it('报告自定义、禁用与未知约束 id', async () => {
445
+ const dir = gitRepo();
446
+ stageChange(dir, 'src/existing.ts', 'export const a = 2;\n');
447
+ passTraces(dir);
448
+ write(
449
+ dir,
450
+ '.harness/config.yml',
451
+ [
452
+ 'custom_constraints:',
453
+ ' fixture_rule:',
454
+ ' id: fixture_rule',
455
+ ' level: guideline',
456
+ ' trigger: manual',
457
+ ' message: fixture 自定义约束',
458
+ 'constraints:',
459
+ ' capability_sync:',
460
+ ' enabled: false',
461
+ ' no_such_rule:',
462
+ ' enabled: false',
463
+ '',
464
+ ].join('\n')
465
+ );
393
466
 
394
- it('应该在 trace 不存在时无提示', async () => {
395
- mockFs.existsSync.mockReturnValue(false);
467
+ await check(
468
+ { preset: 'standard', staged: true, projectPath: dir, trigger: 'code_implementation' },
469
+ io
470
+ );
396
471
 
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('────────────────');
472
+ expect(io.outText()).toContain('自定义约束: 1 ');
473
+ expect(io.outText()).toContain('已禁用约束: capability_sync, no_such_rule');
474
+ expect(io.outText()).toContain('配置中存在未知约束 id(已忽略,可清理): no_such_rule');
401
475
  });
476
+ });
402
477
 
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);
478
+ describe('智能提示(真 traces.log 与状态文件)', () => {
479
+ it('记录数首次达到 50 时提示,并落盘已提示状态', async () => {
480
+ const dir = gitRepo();
481
+ passTraces(dir, 50);
411
482
 
412
- await check({ preset: 'default', staged: false });
413
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('记录已足够'));
483
+ await check({ preset: 'standard', staged: true, projectPath: dir, trigger: 'manual' }, io);
484
+
485
+ expect(io.outText()).toContain('记录已足够,运行 harness status 查看统计');
486
+ expect(io.outText()).toContain('────────────────');
487
+ const state = JSON.parse(
488
+ fs.readFileSync(path.join(dir, '.harness', '.state.json'), 'utf-8')
489
+ );
490
+ expect(state.shownHints).toContain('trace_50');
414
491
  });
415
492
 
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
- });
493
+ it('已提示过的不再重复提示', async () => {
494
+ const dir = gitRepo();
495
+ passTraces(dir, 50);
496
+ write(dir, '.harness/.state.json', JSON.stringify({ shownHints: ['trace_50'] }));
425
497
 
426
- await check({ preset: 'default', staged: false });
427
- const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
428
- expect(output).not.toContain('记录已足够');
498
+ await check({ preset: 'standard', staged: true, projectPath: dir, trigger: 'manual' }, io);
499
+
500
+ expect(io.outText()).not.toContain('记录已足够');
501
+ expect(io.outText()).not.toContain('────────────────');
429
502
  });
430
503
 
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);
504
+ it('记录数不足 50 时无提示', async () => {
505
+ const dir = gitRepo();
506
+ passTraces(dir, 10);
507
+
508
+ await check({ preset: 'standard', staged: true, projectPath: dir, trigger: 'manual' }, io);
439
509
 
440
- await check({ preset: 'default', staged: false });
441
- const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
442
- expect(output).not.toContain('────────────────');
510
+ expect(io.outText()).not.toContain('────────────────');
443
511
  });
444
512
 
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);
513
+ it('无 trace 记录时无提示', async () => {
514
+ const dir = gitRepo();
515
+
516
+ await check({ preset: 'standard', staged: true, projectPath: dir, trigger: 'manual' }, io);
453
517
 
454
- await check({ preset: 'default', staged: false });
455
- expect(mockFs.writeFileSync).toHaveBeenCalled();
456
- expect(mockFs.mkdirSync).toHaveBeenCalled();
518
+ expect(io.outText()).not.toContain('────────────────');
457
519
  });
458
520
  });
459
521
 
460
522
  describe('listLaws', () => {
461
523
  it('应该列出所有约束', () => {
462
- listLaws();
463
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('所有约束'));
524
+ const result = listLaws({}, io);
525
+
526
+ expect(io.outText()).toContain('所有约束');
527
+ expect(result).toEqual({ kind: 'ok' });
464
528
  });
465
529
 
466
530
  it('应该列出铁律', () => {
467
- listLaws();
468
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('铁律'));
531
+ const result = listLaws({}, io);
532
+
533
+ expect(io.outText()).toContain('铁律');
534
+ expect(result).toEqual({ kind: 'ok' });
469
535
  });
470
536
 
471
537
  it('应该列出指导原则', () => {
472
- listLaws();
473
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('指导原则'));
538
+ const result = listLaws({}, io);
539
+
540
+ expect(io.outText()).toContain('指导原则');
541
+ expect(result).toEqual({ kind: 'ok' });
474
542
  });
475
543
 
476
544
  it('应该列出提示', () => {
477
- listLaws();
478
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('提示'));
545
+ const result = listLaws({}, io);
546
+
547
+ expect(io.outText()).toContain('提示');
548
+ expect(result).toEqual({ kind: 'ok' });
479
549
  });
480
550
  });
481
551
  });