@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
@@ -10,32 +10,19 @@
10
10
  */
11
11
 
12
12
  import * as fs from 'fs';
13
- import * as os from 'os';
13
+ import { captureIO, type CapturingIO } from '../../command-contract';
14
14
  import * as path from 'path';
15
15
  import { PassThrough, Writable } from 'stream';
16
16
  import * as yaml from 'js-yaml';
17
- import type { ExecutionTrace } from '../../../types/trace';
18
17
  import { getConstraint } from '../../../core/constraints/definitions';
19
18
  import { getEffectiveConstraints } from '../../../core/effective-constraints';
20
19
  import { renderConstraintsSection } from '../../../core/constraints/injection-renderer';
21
20
  import { FileKnowledgeStore } from '../../../knowledge/store';
22
21
  import { retireConstraint, constraintsRetire, runRetireInteractive, printRetireResult } from '../constraints-retire';
22
+ import { createProjectFixture, writeProjectTraces } from '../../../test-setup/project-fixture';
23
23
 
24
24
  const FIXED_NOW = new Date('2026-08-08T12:00:00.000Z');
25
25
 
26
- function makeTmpProject(): string {
27
- return fs.mkdtempSync(path.join(os.tmpdir(), 'harness-retire-test-'));
28
- }
29
-
30
- function writeTraces(root: string, traces: Partial<ExecutionTrace>[]): void {
31
- const dir = path.join(root, '.harness', 'logs');
32
- fs.mkdirSync(dir, { recursive: true });
33
- const lines = traces.map(t =>
34
- JSON.stringify({ level: 'guideline', timestamp: 1700000000000, result: 'pass', ...t })
35
- );
36
- fs.writeFileSync(path.join(dir, 'traces.log'), lines.join('\n') + '\n', 'utf-8');
37
- }
38
-
39
26
  function readConfig(root: string): any {
40
27
  return yaml.load(fs.readFileSync(path.join(root, '.harness', 'config.yml'), 'utf-8'));
41
28
  }
@@ -58,20 +45,18 @@ function readCustom(root: string): any {
58
45
  return yaml.load(fs.readFileSync(path.join(root, '.harness', 'custom-constraints.yml'), 'utf-8'));
59
46
  }
60
47
 
61
- let logSpy: jest.SpyInstance;
62
-
63
48
  beforeEach(() => {
64
- logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
65
49
  });
66
50
 
67
- afterEach(() => {
68
- logSpy.mockRestore();
51
+ let io: CapturingIO;
52
+ beforeEach(() => {
53
+ io = captureIO();
69
54
  });
70
55
 
71
56
  describe('retireConstraint 执行逻辑', () => {
72
57
  it('config.yml 写入形态:enabled:false + retired 元数据(at/reason/stats)', () => {
73
- const root = makeTmpProject();
74
- writeTraces(root, [
58
+ const root = createProjectFixture({ name: 'harness-retire-test' });
59
+ writeProjectTraces(root, [
75
60
  { constraintId: 'no_hardcoded_credentials', result: 'pass' },
76
61
  { constraintId: 'no_hardcoded_credentials', result: 'pass', timestamp: 1700000001000 },
77
62
  { constraintId: 'no_hardcoded_credentials', result: 'fail', timestamp: 1700000002000 },
@@ -97,7 +82,7 @@ describe('retireConstraint 执行逻辑', () => {
97
82
  });
98
83
 
99
84
  it('config.yml 不存在时创建;已有其他配置时保留', () => {
100
- const root = makeTmpProject();
85
+ const root = createProjectFixture({ name: 'harness-retire-test' });
101
86
  fs.mkdirSync(path.join(root, '.harness'), { recursive: true });
102
87
  fs.writeFileSync(path.join(root, '.harness', 'config.yml'), 'preset: strict\nscenes:\n - llm-app\n', 'utf-8');
103
88
 
@@ -112,8 +97,10 @@ describe('retireConstraint 执行逻辑', () => {
112
97
  });
113
98
 
114
99
  it('KnowledgeStore 写入退役记录:规则原文 + 原因 + 统计 + signal 模式', () => {
115
- const root = makeTmpProject();
116
- writeTraces(root, [{ constraintId: 'no_bypass_checkpoint', result: 'fail' }]);
100
+ const root = createProjectFixture({
101
+ name: 'harness-retire-test',
102
+ traces: [{ constraintId: 'no_bypass_checkpoint', result: 'fail' }],
103
+ });
117
104
 
118
105
  const result = retireConstraint(root, 'no_bypass_checkpoint', { reason: '流程已内置门禁', now: FIXED_NOW });
119
106
  expect(result.status).toBe('retired');
@@ -137,7 +124,7 @@ describe('retireConstraint 执行逻辑', () => {
137
124
  });
138
125
 
139
126
  it('CLAUDE.md 含标记段时同步重渲染(退役条目消失、标记保留)', () => {
140
- const root = makeTmpProject();
127
+ const root = createProjectFixture({ name: 'harness-retire-test' });
141
128
  // 先用当前生效集渲染一个合法注入段
142
129
  const before = renderConstraintsSection(getEffectiveConstraints(root), '0.0.0-test');
143
130
  expect(before).toContain('no_bypass_checkpoint');
@@ -153,14 +140,14 @@ describe('retireConstraint 执行逻辑', () => {
153
140
  expect(after).not.toContain('**no_bypass_checkpoint**');
154
141
  expect(after).toContain('其他内容');
155
142
 
156
- printRetireResult(result);
157
- const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
143
+ printRetireResult(result, io);
144
+ const output = io.outText();
158
145
  expect(output).toContain('已同步 CLAUDE.md 注入段');
159
146
  expect(output).toContain('恢复方法');
160
147
  });
161
148
 
162
149
  it('CLAUDE.md 无标记段或不存在时不创建、不同步', () => {
163
- const root = makeTmpProject();
150
+ const root = createProjectFixture({ name: 'harness-retire-test' });
164
151
  fs.writeFileSync(path.join(root, 'CLAUDE.md'), '# 用户自写\n', 'utf-8');
165
152
  const result = retireConstraint(root, 'capability_sync', { now: FIXED_NOW });
166
153
  expect(result.status).toBe('retired');
@@ -169,7 +156,7 @@ describe('retireConstraint 执行逻辑', () => {
169
156
  });
170
157
 
171
158
  it('新模型仓:AGENTS.md PRESERVE:governance 内含标记段时同步重渲染(无 CLAUDE.md)', () => {
172
- const root = makeTmpProject();
159
+ const root = createProjectFixture({ name: 'harness-retire-test' });
173
160
  const before = renderConstraintsSection(getEffectiveConstraints(root), '0.0.0-test');
174
161
  expect(before).toContain('no_bypass_checkpoint');
175
162
  fs.writeFileSync(
@@ -194,7 +181,7 @@ describe('retireConstraint 执行逻辑', () => {
194
181
  });
195
182
 
196
183
  it('两文件均有标记段时旧模型仓豁免:只同步 CLAUDE.md,AGENTS.md 不动', () => {
197
- const root = makeTmpProject();
184
+ const root = createProjectFixture({ name: 'harness-retire-test' });
198
185
  const before = renderConstraintsSection(getEffectiveConstraints(root), '0.0.0-test');
199
186
  fs.writeFileSync(path.join(root, 'CLAUDE.md'), `# 项目\n\n## Governance Rules\n${before}`, 'utf-8');
200
187
  const agentsMd = `# AGENTS.md\n\n<!-- PRESERVE:governance -->\n## Governance Rules\n${before}<!-- /PRESERVE:governance -->\n`;
@@ -210,7 +197,7 @@ describe('retireConstraint 执行逻辑', () => {
210
197
  });
211
198
 
212
199
  it('重复 retire:already_retired,不覆盖原 retired 元数据', () => {
213
- const root = makeTmpProject();
200
+ const root = createProjectFixture({ name: 'harness-retire-test' });
214
201
  const first = retireConstraint(root, 'capability_sync', { reason: '第一次', now: FIXED_NOW });
215
202
  expect(first.status).toBe('retired');
216
203
 
@@ -224,28 +211,28 @@ describe('retireConstraint 执行逻辑', () => {
224
211
  });
225
212
 
226
213
  it('未知 id:unknown_id,不落盘任何文件', () => {
227
- const root = makeTmpProject();
214
+ const root = createProjectFixture({ name: 'harness-retire-test' });
228
215
  const result = retireConstraint(root, 'not_a_constraint', { now: FIXED_NOW });
229
216
  expect(result.status).toBe('unknown_id');
230
217
  expect(fs.existsSync(path.join(root, '.harness'))).toBe(false);
231
218
  });
232
219
 
233
220
  it('check 层 iron:isIronLaw 标记为 true(供交互模式二次确认)', () => {
234
- const root = makeTmpProject();
221
+ const root = createProjectFixture({ name: 'harness-retire-test' });
235
222
  const result = retireConstraint(root, 'docs_freshness', { now: FIXED_NOW });
236
223
  expect(result.status).toBe('retired');
237
224
  expect(result.isIronLaw).toBe(true);
238
225
  });
239
226
 
240
227
  it('退役后生效集不再包含该约束', () => {
241
- const root = makeTmpProject();
228
+ const root = createProjectFixture({ name: 'harness-retire-test' });
242
229
  expect(getEffectiveConstraints(root).some(c => c.id === 'capability_sync')).toBe(true);
243
230
  retireConstraint(root, 'capability_sync', { now: FIXED_NOW });
244
231
  expect(getEffectiveConstraints(root).some(c => c.id === 'capability_sync')).toBe(false);
245
232
  });
246
233
 
247
234
  it('custom 约束退役:落 custom-constraints.yml retired 段(不写 config.yml),生效集与注入段移除', () => {
248
- const root = makeTmpProject();
235
+ const root = createProjectFixture({ name: 'harness-retire-test' });
249
236
  writeCustom(root, CUSTOM_YML);
250
237
  const before = renderConstraintsSection(getEffectiveConstraints(root), '0.0.0-test');
251
238
  expect(before).toContain('**my_custom_rule**');
@@ -271,13 +258,13 @@ describe('retireConstraint 执行逻辑', () => {
271
258
  expect(after).not.toContain('**my_custom_rule**');
272
259
  expect(after).toContain('其他内容');
273
260
  // 打印的恢复提示指向 yml
274
- printRetireResult(result);
275
- const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
261
+ printRetireResult(result, io);
262
+ const output = io.outText();
276
263
  expect(output).toContain('custom_constraints.my_custom_rule.retired');
277
264
  });
278
265
 
279
266
  it('custom 重复退役:yml 已带 retired → already_retired,不覆盖原元数据', () => {
280
- const root = makeTmpProject();
267
+ const root = createProjectFixture({ name: 'harness-retire-test' });
281
268
  writeCustom(root, CUSTOM_YML);
282
269
  const first = retireConstraint(root, 'my_custom_rule', { reason: '第一次', now: FIXED_NOW });
283
270
  expect(first.status).toBe('retired');
@@ -292,7 +279,7 @@ describe('retireConstraint 执行逻辑', () => {
292
279
  });
293
280
 
294
281
  it('custom 历史落点(config.yml enabled:false)仍判定 already_retired', () => {
295
- const root = makeTmpProject();
282
+ const root = createProjectFixture({ name: 'harness-retire-test' });
296
283
  writeCustom(root, CUSTOM_YML);
297
284
  fs.writeFileSync(path.join(root, '.harness', 'config.yml'), 'constraints:\n my_custom_rule:\n enabled: false\n', 'utf-8');
298
285
  const result = retireConstraint(root, 'my_custom_rule', { now: FIXED_NOW });
@@ -300,7 +287,7 @@ describe('retireConstraint 执行逻辑', () => {
300
287
  });
301
288
 
302
289
  it('custom 退役 KnowledgeStore 记录包含 promptInjection(规则原文完整)', () => {
303
- const root = makeTmpProject();
290
+ const root = createProjectFixture({ name: 'harness-retire-test' });
304
291
  writeCustom(root, CUSTOM_YML);
305
292
  const result = retireConstraint(root, 'my_custom_rule', { reason: '由新机制覆盖', now: FIXED_NOW });
306
293
  expect(result.status).toBe('retired');
@@ -313,7 +300,7 @@ describe('retireConstraint 执行逻辑', () => {
313
300
  });
314
301
 
315
302
  it('custom 恢复:删除 yml retired 段后回到生效集', () => {
316
- const root = makeTmpProject();
303
+ const root = createProjectFixture({ name: 'harness-retire-test' });
317
304
  writeCustom(root, CUSTOM_YML);
318
305
  retireConstraint(root, 'my_custom_rule', { now: FIXED_NOW });
319
306
  expect(getEffectiveConstraints(root).some(c => c.id === 'my_custom_rule')).toBe(false);
@@ -327,39 +314,25 @@ describe('retireConstraint 执行逻辑', () => {
327
314
  });
328
315
 
329
316
  describe('constraintsRetire 非交互直达', () => {
330
- let originalExitCode: typeof process.exitCode;
331
-
332
- beforeEach(() => {
333
- originalExitCode = process.exitCode;
334
- process.exitCode = 0;
335
- });
336
-
337
- afterEach(() => {
338
- process.exitCode = originalExitCode;
339
- });
340
-
341
317
  it('无 --yes 直达:报错 + 非零退出码 + 不落盘任何文件(#24 人确认闸门)', async () => {
342
- const root = makeTmpProject();
343
- const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
344
- try {
345
- await constraintsRetire('docs_freshness', { projectPath: root, reason: '直接退役' });
318
+ const root = createProjectFixture({ name: 'harness-retire-test' });
319
+ {
320
+ const result = await constraintsRetire('docs_freshness', { projectPath: root, reason: '直接退役' }, io);
346
321
 
347
- const output = errSpy.mock.calls.map(c => String(c[0])).join('\n');
322
+ const output = io.errText();
348
323
  expect(output).toContain('--yes');
349
324
  expect(output).toContain('交互');
350
- expect(process.exitCode).toBe(1);
325
+ expect(result).toEqual({ kind: 'usage-error', reason: expect.stringContaining('缺少显式 --yes 人确认') });
351
326
  // 无副作用:任何文件都不落盘
352
327
  expect(fs.existsSync(path.join(root, '.harness'))).toBe(false);
353
- } finally {
354
- errSpy.mockRestore();
355
328
  }
356
329
  });
357
330
 
358
331
  it('--yes 直达 iron 退役:打印额外警示并落盘', async () => {
359
- const root = makeTmpProject();
360
- await constraintsRetire('docs_freshness', { projectPath: root, reason: '直接退役', yes: true });
332
+ const root = createProjectFixture({ name: 'harness-retire-test' });
333
+ await constraintsRetire('docs_freshness', { projectPath: root, reason: '直接退役', yes: true }, io);
361
334
 
362
- const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
335
+ const output = io.outText();
363
336
  expect(output).toContain('Iron Law');
364
337
  expect(output).toContain('已退役');
365
338
 
@@ -368,10 +341,10 @@ describe('constraintsRetire 非交互直达', () => {
368
341
  });
369
342
 
370
343
  it('--yes 直达非 iron(guideline)退役:不打印 iron 警示并落盘', async () => {
371
- const root = makeTmpProject();
372
- await constraintsRetire('no_hardcoded_credentials', { projectPath: root, reason: '直接退役', yes: true });
344
+ const root = createProjectFixture({ name: 'harness-retire-test' });
345
+ await constraintsRetire('no_hardcoded_credentials', { projectPath: root, reason: '直接退役', yes: true }, io);
373
346
 
374
- const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
347
+ const output = io.outText();
375
348
  expect(output).not.toContain('Iron Law');
376
349
  expect(output).toContain('已退役');
377
350
 
@@ -380,18 +353,31 @@ describe('constraintsRetire 非交互直达', () => {
380
353
  });
381
354
 
382
355
  it('--yes 直达未知 id:明确提示', async () => {
383
- const root = makeTmpProject();
384
- await constraintsRetire('ghost', { projectPath: root, yes: true });
385
- const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
356
+ const root = createProjectFixture({ name: 'harness-retire-test' });
357
+ await constraintsRetire('ghost', { projectPath: root, yes: true }, io);
358
+ const output = io.outText();
386
359
  expect(output).toContain('约束不存在');
387
360
  });
388
361
  });
389
362
 
390
363
  describe('runRetireInteractive 交互流程(注入 IO 流)', () => {
391
- /** 逐行 drip-feed,等待 readline 消费上一行 */
364
+ /**
365
+ * 交互正文仍走 console(RetireIO 只注入 readline 的 input/output 流,
366
+ * 且 retireConstraint 核心直接 console.warn 式打印)→ 用 console 捕获。
367
+ */
368
+ function captureLog(): { text: () => string; restore: () => void } {
369
+ let out = '';
370
+ const spy = jest.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
371
+ out += String(args[0] ?? '') + '\n';
372
+ });
373
+ return { text: () => out, restore: () => spy.mockRestore() };
374
+ }
375
+
376
+ /** 逐行 drip-feed,等待 readline 消费上一行;output 收集写入文本 */
392
377
  function makeIo(lines: string[]) {
393
378
  const input = new PassThrough();
394
- const output = new Writable({ write(_c, _e, cb) { cb(); } });
379
+ const chunks: string[] = [];
380
+ const output = new Writable({ write(c, _e, cb) { chunks.push(String(c)); cb(); } });
395
381
  let i = 0;
396
382
  const timer = setInterval(() => {
397
383
  if (i < lines.length) {
@@ -401,18 +387,20 @@ describe('runRetireInteractive 交互流程(注入 IO 流)', () => {
401
387
  clearInterval(timer);
402
388
  }
403
389
  }, 30);
404
- return { input, output, done: () => clearInterval(timer) };
390
+ return { input, output, text: () => chunks.join(''), done: () => clearInterval(timer) };
405
391
  }
406
392
 
407
393
  it('候选编号选择 + iron 二次确认拒绝 → 不落盘', async () => {
408
- const root = makeTmpProject();
394
+ const root = createProjectFixture({ name: 'harness-retire-test' });
409
395
  // 无 trace → 候选全部为零触发,1 号候选是第一条 iron(no_completion_without_verification)
410
- const io = makeIo(['1', 'n']); // 选 1 号 → iron 确认拒绝
396
+ const streams = makeIo(['1', 'n']); // 选 1 号 → iron 确认拒绝
397
+ const printed = captureLog();
411
398
 
412
- await runRetireInteractive(root, io);
413
- io.done();
399
+ await runRetireInteractive(root, streams);
400
+ streams.done();
401
+ printed.restore();
414
402
 
415
- const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
403
+ const output = printed.text() + streams.text();
416
404
  expect(output).toContain('退役候选');
417
405
  expect(output).toContain('已跳过 no_completion_without_verification');
418
406
  expect(output).toContain('无可执行项');
@@ -420,7 +408,7 @@ describe('runRetireInteractive 交互流程(注入 IO 流)', () => {
420
408
  });
421
409
 
422
410
  it('无候选 → 手动输入 id → 确认执行 → 落盘', async () => {
423
- const root = makeTmpProject();
411
+ const root = createProjectFixture({ name: 'harness-retire-test' });
424
412
  // 所有 check 约束给少量健康数据(低于一切候选阈值)
425
413
  const healthy = getEffectiveConstraints(root)
426
414
  .filter(c => c.kind === 'check')
@@ -429,13 +417,15 @@ describe('runRetireInteractive 交互流程(注入 IO 流)', () => {
429
417
  { constraintId: c.id, result: 'pass' as const, timestamp: 1700000001000 },
430
418
  { constraintId: c.id, result: 'fail' as const, timestamp: 1700000002000 },
431
419
  ]);
432
- writeTraces(root, healthy);
420
+ writeProjectTraces(root, healthy);
433
421
 
434
- const io = makeIo(['no_hardcoded_credentials', '误报太多', 'y']);
435
- await runRetireInteractive(root, io);
436
- io.done();
422
+ const streams = makeIo(['no_hardcoded_credentials', '误报太多', 'y']);
423
+ const printed = captureLog();
424
+ await runRetireInteractive(root, streams);
425
+ streams.done();
426
+ printed.restore();
437
427
 
438
- const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
428
+ const output = printed.text() + streams.text();
439
429
  expect(output).toContain('没有退役候选');
440
430
  expect(output).toContain('已退役');
441
431
 
@@ -444,8 +434,73 @@ describe('runRetireInteractive 交互流程(注入 IO 流)', () => {
444
434
  expect(config.constraints.no_hardcoded_credentials.retired.reason).toBe('误报太多');
445
435
  });
446
436
 
437
+ it('坏行 fixture:候选列表前告知损坏行数(决策依据不完整不静默,harness#100)', async () => {
438
+ const root = createProjectFixture({ name: 'harness-retire-test' });
439
+ const dir = path.join(root, '.harness', 'logs');
440
+ fs.mkdirSync(dir, { recursive: true });
441
+ fs.writeFileSync(
442
+ path.join(dir, 'traces.log'),
443
+ '{"constraintId":"a","level":"iron_law","timestamp":1700000000000,"result":"pass"}\n{bad json\n{also bad\n',
444
+ 'utf-8'
445
+ );
446
+
447
+ const streams = makeIo(['']); // 直接取消,只验证告知行
448
+ const printed = captureLog();
449
+ const notices: string[] = [];
450
+ const errSpy = jest.spyOn(console, 'error').mockImplementation((...args: unknown[]) => {
451
+ notices.push(String(args[0] ?? ''));
452
+ });
453
+ await runRetireInteractive(root, streams);
454
+ streams.done();
455
+ errSpy.mockRestore();
456
+ printed.restore();
457
+
458
+ expect(notices.join('\n')).toContain('2 行损坏');
459
+ expect(printed.text()).toContain('退役候选');
460
+ });
461
+
462
+ it('trace 无损坏时不出现坏行提示(零噪声)', async () => {
463
+ const root = createProjectFixture({
464
+ name: 'harness-retire-test',
465
+ traces: [{ constraintId: 'a', result: 'pass' }],
466
+ });
467
+
468
+ const streams = makeIo(['']);
469
+ const printed = captureLog();
470
+ const notices: string[] = [];
471
+ const errSpy = jest.spyOn(console, 'error').mockImplementation((...args: unknown[]) => {
472
+ notices.push(String(args[0] ?? ''));
473
+ });
474
+ await runRetireInteractive(root, streams);
475
+ streams.done();
476
+ errSpy.mockRestore();
477
+ printed.restore();
478
+
479
+ expect(notices.join('\n')).not.toContain('损坏');
480
+ expect(printed.text()).toContain('退役候选');
481
+ });
482
+
483
+ it('直达 --yes 路径也在落盘前告知损坏行数(harness#100:不经过交互也有告知)', async () => {
484
+ const root = createProjectFixture({ name: 'harness-retire-test' });
485
+ const dir = path.join(root, '.harness', 'logs');
486
+ fs.mkdirSync(dir, { recursive: true });
487
+ fs.writeFileSync(
488
+ path.join(dir, 'traces.log'),
489
+ '{"constraintId":"a","level":"iron_law","timestamp":1700000000000,"result":"pass"}\n{bad json\n',
490
+ 'utf-8'
491
+ );
492
+
493
+ const directIo = captureIO();
494
+ await constraintsRetire('no_hardcoded_credentials', { projectPath: root, yes: true }, directIo);
495
+
496
+ expect(directIo.errText()).toContain('1 行损坏');
497
+ // 告知不挤动结果正文:退役结论仍在 stdout
498
+ expect(directIo.outText()).toContain('已退役');
499
+ expect(readConfig(root).constraints.no_hardcoded_credentials.enabled).toBe(false);
500
+ });
501
+
447
502
  it('无候选 → 手动输入 custom id → 确认执行 → 落 custom-constraints.yml', async () => {
448
- const root = makeTmpProject();
503
+ const root = createProjectFixture({ name: 'harness-retire-test' });
449
504
  writeCustom(root, CUSTOM_YML);
450
505
  // 所有 check 约束给少量健康数据(低于一切候选阈值)
451
506
  const healthy = getEffectiveConstraints(root)
@@ -455,11 +510,11 @@ describe('runRetireInteractive 交互流程(注入 IO 流)', () => {
455
510
  { constraintId: c.id, result: 'pass' as const, timestamp: 1700000001000 },
456
511
  { constraintId: c.id, result: 'fail' as const, timestamp: 1700000002000 },
457
512
  ]);
458
- writeTraces(root, healthy);
513
+ writeProjectTraces(root, healthy);
459
514
 
460
- const io = makeIo(['my_custom_rule', '作用对象消失', 'y']);
461
- await runRetireInteractive(root, io);
462
- io.done();
515
+ const streams = makeIo(['my_custom_rule', '作用对象消失', 'y']);
516
+ await runRetireInteractive(root, streams);
517
+ streams.done();
463
518
 
464
519
  const custom = readCustom(root).custom_constraints.my_custom_rule;
465
520
  expect(custom.retired.reason).toBe('作用对象消失');
@@ -467,17 +522,19 @@ describe('runRetireInteractive 交互流程(注入 IO 流)', () => {
467
522
  });
468
523
 
469
524
  it('无候选 → 手动输入未知 id → 提示不存在并取消', async () => {
470
- const root = makeTmpProject();
525
+ const root = createProjectFixture({ name: 'harness-retire-test' });
471
526
  const healthy = getEffectiveConstraints(root)
472
527
  .filter(c => c.kind === 'check')
473
528
  .flatMap(c => [{ constraintId: c.id, result: 'pass' as const }]);
474
- writeTraces(root, healthy);
529
+ writeProjectTraces(root, healthy);
475
530
 
476
- const io = makeIo(['ghost_id']);
477
- await runRetireInteractive(root, io);
478
- io.done();
531
+ const streams = makeIo(['ghost_id']);
532
+ const printed = captureLog();
533
+ await runRetireInteractive(root, streams);
534
+ streams.done();
535
+ printed.restore();
479
536
 
480
- const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
537
+ const output = printed.text() + streams.text();
481
538
  expect(output).toContain('ghost_id');
482
539
  expect(output).toContain('约束不存在');
483
540
  expect(fs.existsSync(path.join(root, '.harness', 'config.yml'))).toBe(false);
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { contract, validateSchema } from '../contract';
6
+ import { captureIO, type CapturingIO } from '../../command-contract';
6
7
  import * as fs from 'fs/promises';
7
8
  import { ContractGate } from '../../../gates/contract';
8
9
 
@@ -34,20 +35,12 @@ const MockGate = ContractGate as jest.MockedClass<typeof ContractGate>;
34
35
  const yaml = require('js-yaml');
35
36
 
36
37
  describe('contract command', () => {
37
- let consoleSpy: jest.SpyInstance;
38
- let exitSpy: jest.SpyInstance;
38
+ let io: CapturingIO;
39
39
 
40
40
  beforeEach(() => {
41
- jest.clearAllMocks();
42
- consoleSpy = jest.spyOn(console, 'log').mockImplementation();
43
- exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
44
- process.exitCode = 0;
45
- });
46
41
 
47
- afterEach(() => {
48
- consoleSpy.mockRestore();
49
- exitSpy.mockRestore();
50
- process.exitCode = 0;
42
+ io = captureIO();
43
+ jest.clearAllMocks();
51
44
  });
52
45
 
53
46
  describe('contract', () => {
@@ -60,19 +53,19 @@ describe('contract command', () => {
60
53
  });
61
54
  MockGate.mockImplementation(() => ({ check: mockCheck }) as any);
62
55
 
63
- await contract({});
56
+ const result = await contract({}, io);
64
57
 
65
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('契约门控检查通过'));
66
- expect(exitSpy).not.toHaveBeenCalled();
58
+ expect(io.outText()).toContain('契约门控检查通过');
59
+ expect(result.kind).toBe('ok');
67
60
  });
68
61
 
69
62
  it('should exit 1 when file does not exist', async () => {
70
63
  mockFs.access.mockRejectedValue(new Error('ENOENT'));
71
64
 
72
- await contract({});
65
+ const result = await contract({}, io);
73
66
 
74
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('契约文件不存在'));
75
- expect(exitSpy).toHaveBeenCalledWith(1);
67
+ expect(io.outText()).toContain('契约文件不存在');
68
+ expect(result.kind).toBe('fail');
76
69
  });
77
70
 
78
71
  it('should print failure and exit 1 when check fails', async () => {
@@ -87,10 +80,10 @@ describe('contract command', () => {
87
80
  });
88
81
  MockGate.mockImplementation(() => ({ check: mockCheck }) as any);
89
82
 
90
- await contract({});
83
+ const result = await contract({}, io);
91
84
 
92
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('契约门控检查失败'));
93
- expect(exitSpy).toHaveBeenCalledWith(1);
85
+ expect(io.outText()).toContain('契约门控检查失败');
86
+ expect(result.kind).toBe('fail');
94
87
  });
95
88
 
96
89
  it('should handle thrown errors and exit 1', async () => {
@@ -98,10 +91,10 @@ describe('contract command', () => {
98
91
  const mockCheck = jest.fn().mockRejectedValue(new Error('gate error'));
99
92
  MockGate.mockImplementation(() => ({ check: mockCheck }) as any);
100
93
 
101
- await contract({});
94
+ const result = await contract({}, io);
102
95
 
103
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('契约门控检查出错'));
104
- expect(exitSpy).toHaveBeenCalledWith(1);
96
+ expect(io.outText()).toContain('契约门控检查出错');
97
+ expect(result.kind).toBe('fail');
105
98
  });
106
99
  });
107
100
 
@@ -114,38 +107,38 @@ describe('contract command', () => {
114
107
  paths: { '/users': {} },
115
108
  });
116
109
 
117
- await validateSchema({});
110
+ await validateSchema({}, io);
118
111
 
119
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Schema 验证通过'));
112
+ expect(io.outText()).toContain('Schema 验证通过');
120
113
  });
121
114
 
122
115
  it('should report missing fields', async () => {
123
116
  mockFs.readFile.mockResolvedValue('{}');
124
117
  yaml.load.mockReturnValue({});
125
118
 
126
- await validateSchema({});
119
+ const result = await validateSchema({}, io);
127
120
 
128
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Schema 验证失败'));
129
- expect(exitSpy).toHaveBeenCalledWith(1);
121
+ expect(io.outText()).toContain('Schema 验证失败');
122
+ expect(result.kind).toBe('fail');
130
123
  });
131
124
 
132
125
  it('should report YAML parse errors', async () => {
133
126
  mockFs.readFile.mockResolvedValue('invalid: yaml: content:');
134
127
  yaml.load.mockImplementation(() => { throw new Error('parse error'); });
135
128
 
136
- await validateSchema({});
129
+ const result = await validateSchema({}, io);
137
130
 
138
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('YAML 解析错误'));
139
- expect(exitSpy).toHaveBeenCalledWith(1);
131
+ expect(io.outText()).toContain('YAML 解析错误');
132
+ expect(result.kind).toBe('fail');
140
133
  });
141
134
 
142
135
  it('should handle file read errors', async () => {
143
136
  mockFs.readFile.mockRejectedValue(new Error('ENOENT'));
144
137
 
145
- await validateSchema({});
138
+ const result = await validateSchema({}, io);
146
139
 
147
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('验证失败'));
148
- expect(exitSpy).toHaveBeenCalledWith(1);
140
+ expect(io.outText()).toContain('验证失败');
141
+ expect(result.kind).toBe('fail');
149
142
  });
150
143
  });
151
144
  });