@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
@@ -3,9 +3,15 @@
3
3
  */
4
4
 
5
5
  import * as fs from 'fs';
6
+ import { captureIO, type CapturingIO } from '../../command-contract';
6
7
  import * as path from 'path';
7
8
  import { syncDocs } from '../sync-docs';
8
9
 
10
+ let io: CapturingIO;
11
+ beforeEach(() => {
12
+ io = captureIO();
13
+ });
14
+
9
15
  describe('sync-docs command', () => {
10
16
  const tempDir = path.join(process.cwd(), 'temp-test-sync-docs');
11
17
 
@@ -21,15 +27,8 @@ describe('sync-docs command', () => {
21
27
  }
22
28
  });
23
29
 
24
- let consoleSpy: jest.SpyInstance;
25
-
26
30
  beforeEach(() => {
27
31
  jest.clearAllMocks();
28
- consoleSpy = jest.spyOn(console, 'log').mockImplementation();
29
- });
30
-
31
- afterEach(() => {
32
- consoleSpy.mockRestore();
33
32
  });
34
33
 
35
34
  describe('generateModuleTable', () => {
@@ -38,11 +37,11 @@ describe('sync-docs command', () => {
38
37
  fs.mkdirSync(testDir, { recursive: true });
39
38
 
40
39
  // No src dir → no modules → nothing to report
41
- const result = await syncDocs({ projectPath: testDir });
40
+ const result = await syncDocs({ projectPath: testDir }, io);
42
41
 
43
- expect(result).toBe(true);
42
+ expect(result).toEqual({ kind: 'ok' });
44
43
  // When no modules and no changes, all docs are current
45
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('所有文档'));
44
+ expect(io.outText()).toContain('所有文档');
46
45
 
47
46
  fs.rmSync(testDir, { recursive: true, force: true });
48
47
  });
@@ -55,7 +54,7 @@ describe('sync-docs command', () => {
55
54
  // Create a source file with JSDoc comment
56
55
  fs.writeFileSync(path.join(srcDir, 'mymodule.ts'), '/**\n * My test module\n */\nexport const x = 1;');
57
56
 
58
- await syncDocs({ projectPath: testDir });
57
+ await syncDocs({ projectPath: testDir }, io);
59
58
 
60
59
  const capPath = path.join(testDir, 'CAPABILITIES.md');
61
60
  const content = fs.readFileSync(capPath, 'utf-8');
@@ -75,7 +74,7 @@ describe('sync-docs command', () => {
75
74
 
76
75
  fs.writeFileSync(path.join(srcDir, 'jsdoc-mod.ts'), '/**\n * JSDoc module description\n */\nexport const y = 2;');
77
76
 
78
- await syncDocs({ projectPath: testDir });
77
+ await syncDocs({ projectPath: testDir }, io);
79
78
 
80
79
  const capPath = path.join(testDir, 'CAPABILITIES.md');
81
80
  const content = fs.readFileSync(capPath, 'utf-8');
@@ -93,7 +92,7 @@ describe('sync-docs command', () => {
93
92
  const filePath = path.join(srcDir, 'line-mod.ts');
94
93
  fs.writeFileSync(filePath, '// Line comment module\nexport const z = 3;');
95
94
 
96
- await syncDocs({ projectPath: testDir });
95
+ await syncDocs({ projectPath: testDir }, io);
97
96
 
98
97
  const capPath = path.join(testDir, 'CAPABILITIES.md');
99
98
  const content = fs.readFileSync(capPath, 'utf-8');
@@ -109,7 +108,7 @@ describe('sync-docs command', () => {
109
108
 
110
109
  fs.writeFileSync(path.join(srcDir, 'bare.ts'), 'export const a = 1;');
111
110
 
112
- await syncDocs({ projectPath: testDir });
111
+ await syncDocs({ projectPath: testDir }, io);
113
112
 
114
113
  const capPath = path.join(testDir, 'CAPABILITIES.md');
115
114
  const content = fs.readFileSync(capPath, 'utf-8');
@@ -120,16 +119,15 @@ describe('sync-docs command', () => {
120
119
  });
121
120
  });
122
121
 
123
-
124
122
  describe('syncDocs', () => {
125
123
  it('无 src/ 目录应该跳过模块扫描', async () => {
126
124
  const emptyDir = path.join(tempDir, 'empty');
127
125
  fs.mkdirSync(emptyDir, { recursive: true });
128
126
 
129
- const result = await syncDocs({ projectPath: emptyDir });
130
- expect(result).toBe(true);
127
+ const result = await syncDocs({ projectPath: emptyDir }, io);
128
+ expect(result).toEqual({ kind: 'ok' });
131
129
  // 无 src/ 目录,模块扫描返回空,无差异
132
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('所有文档都是最新的'));
130
+ expect(io.outText()).toContain('所有文档都是最新的');
133
131
 
134
132
  fs.rmSync(emptyDir, { recursive: true, force: true });
135
133
  });
@@ -142,8 +140,8 @@ describe('sync-docs command', () => {
142
140
  // 创建 CAPABILITIES.md,无表格
143
141
  fs.writeFileSync(path.join(testDir, 'CAPABILITIES.md'), '# Capabilities\n\nNo table.');
144
142
 
145
- const result = await syncDocs({ projectPath: testDir });
146
- expect(result).toBe(true);
143
+ const result = await syncDocs({ projectPath: testDir }, io);
144
+ expect(result).toEqual({ kind: 'ok' });
147
145
 
148
146
  fs.rmSync(testDir, { recursive: true, force: true });
149
147
  });
@@ -162,9 +160,9 @@ describe('sync-docs command', () => {
162
160
  '# Capabilities\n\n| 模块 | 文件 | 说明 |\n|------|------|------|\n| old | src/old.ts | old |'
163
161
  );
164
162
 
165
- const result = await syncDocs({ projectPath: testDir, check: true });
166
- expect(result).toBe(false);
167
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('文档不是最新的'));
163
+ const result = await syncDocs({ projectPath: testDir, check: true }, io);
164
+ expect(result).toEqual({ kind: 'fail', reason: expect.stringContaining('文档不是最新') });
165
+ expect(io.outText()).toContain('文档不是最新的');
168
166
 
169
167
  fs.rmSync(testDir, { recursive: true, force: true });
170
168
  });
@@ -183,15 +181,15 @@ describe('sync-docs command', () => {
183
181
  );
184
182
 
185
183
  // 第一轮:生成 CAPABILITIES.md(描述行以 / 结尾)
186
- await syncDocs({ projectPath: testDir });
184
+ await syncDocs({ projectPath: testDir }, io);
187
185
  const cap = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
188
186
  expect(cap).toContain('workspace.ts');
189
187
  expect(cap).toMatch(/gitRepo\s*\/\s*\|/);
190
188
 
191
189
  // 第二轮:生成后立即 --check 必须收敛(修复前此处必 false)
192
- consoleSpy.mockClear();
193
- const check = await syncDocs({ projectPath: testDir, check: true });
194
- expect(check).toBe(true);
190
+ io = captureIO();
191
+ const check = await syncDocs({ projectPath: testDir, check: true }, io);
192
+ expect(check).toEqual({ kind: 'ok' });
195
193
 
196
194
  fs.rmSync(testDir, { recursive: true, force: true });
197
195
  });
@@ -210,8 +208,8 @@ describe('sync-docs command', () => {
210
208
  );
211
209
 
212
210
  // src/ 目录条目覆盖 mymodule.ts → 无 added,check 通过
213
- const result = await syncDocs({ projectPath: testDir, check: true });
214
- expect(result).toBe(true);
211
+ const result = await syncDocs({ projectPath: testDir, check: true }, io);
212
+ expect(result).toEqual({ kind: 'ok' });
215
213
 
216
214
  fs.rmSync(testDir, { recursive: true, force: true });
217
215
  });
@@ -239,9 +237,9 @@ describe('sync-docs command', () => {
239
237
 
240
238
  // 不创建 CONTEXT.md
241
239
 
242
- const result = await syncDocs({ projectPath: testDir });
243
- expect(result).toBe(false);
244
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('缺少 CONTEXT.md'));
240
+ const result = await syncDocs({ projectPath: testDir }, io);
241
+ expect(result).toEqual({ kind: 'ok' });
242
+ expect(io.outText()).toContain('缺少 CONTEXT.md');
245
243
 
246
244
  fs.rmSync(testDir, { recursive: true, force: true });
247
245
  });
@@ -269,8 +267,8 @@ describe('sync-docs command', () => {
269
267
  // 创建 CONTEXT.md
270
268
  fs.writeFileSync(path.join(srcDir, 'CONTEXT.md'), '# src\n\nTest context');
271
269
 
272
- const result = await syncDocs({ projectPath: testDir });
273
- expect(result).toBe(true);
270
+ const result = await syncDocs({ projectPath: testDir }, io);
271
+ expect(result).toEqual({ kind: 'ok' });
274
272
 
275
273
  fs.rmSync(testDir, { recursive: true, force: true });
276
274
  });
@@ -281,8 +279,8 @@ describe('sync-docs command', () => {
281
279
 
282
280
  // 无 .harness/config.yml
283
281
 
284
- const result = await syncDocs({ projectPath: testDir });
285
- expect(result).toBe(true);
282
+ const result = await syncDocs({ projectPath: testDir }, io);
283
+ expect(result).toEqual({ kind: 'ok' });
286
284
 
287
285
  fs.rmSync(testDir, { recursive: true, force: true });
288
286
  });
@@ -307,7 +305,7 @@ describe('sync-docs command', () => {
307
305
  })
308
306
  );
309
307
 
310
- await syncDocs({ projectPath: testDir });
308
+ await syncDocs({ projectPath: testDir }, io);
311
309
 
312
310
  // 验证 CONTEXT.md 被创建
313
311
  const contextPath = path.join(srcDir, 'CONTEXT.md');
@@ -326,7 +324,7 @@ describe('sync-docs command', () => {
326
324
  // 创建源文件
327
325
  fs.writeFileSync(path.join(srcDir, 'module.ts'), 'export const x = 1;');
328
326
 
329
- await syncDocs({ projectPath: testDir });
327
+ await syncDocs({ projectPath: testDir }, io);
330
328
 
331
329
  // 验证 CAPABILITIES.md 被创建
332
330
  const capPath = path.join(testDir, 'CAPABILITIES.md');
@@ -346,11 +344,11 @@ describe('sync-docs command', () => {
346
344
  '# Capabilities\n\n| 模块 | 文件 | 说明 |\n|------|------|------|\n| old | src/old.ts | old |'
347
345
  );
348
346
 
349
- const result = await syncDocs({ projectPath: testDir, check: true, json: true });
350
- expect(result).toBe(false);
347
+ const result = await syncDocs({ projectPath: testDir, check: true, json: true }, io);
348
+ expect(result).toEqual({ kind: 'fail', reason: expect.stringContaining('文档不是最新') });
351
349
 
352
350
  // 验证 console.log 被调用了 JSON
353
- const jsonCall = consoleSpy.mock.calls.find(
351
+ const jsonCall = io.outRecords().map(chunk => [chunk] as string[]).find(
354
352
  (call: unknown[]) => typeof call[0] === 'string' && call[0].startsWith('{')
355
353
  );
356
354
  expect(jsonCall).toBeDefined();
@@ -366,10 +364,10 @@ describe('sync-docs command', () => {
366
364
  fs.mkdirSync(testDir, { recursive: true });
367
365
  fs.writeFileSync(path.join(testDir, 'CAPABILITIES.md'), '# Capabilities\n\nNo table.');
368
366
 
369
- const result = await syncDocs({ projectPath: testDir, check: true, json: true });
370
- expect(result).toBe(true);
367
+ const result = await syncDocs({ projectPath: testDir, check: true, json: true }, io);
368
+ expect(result).toEqual({ kind: 'ok' });
371
369
 
372
- const jsonCall = consoleSpy.mock.calls.find(
370
+ const jsonCall = io.outRecords().map(chunk => [chunk] as string[]).find(
373
371
  (call: unknown[]) => typeof call[0] === 'string' && call[0].startsWith('{')
374
372
  );
375
373
  expect(jsonCall).toBeDefined();
@@ -389,7 +387,7 @@ describe('sync-docs command', () => {
389
387
  fs.writeFileSync(path.join(srcDir, 'removed.ts'), 'export const removed = 1;');
390
388
 
391
389
  // 先让 syncDocs 创建初始 CAPABILITIES.md(包含两个文件)
392
- await syncDocs({ projectPath: testDir });
390
+ await syncDocs({ projectPath: testDir }, io);
393
391
  let content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
394
392
  expect(content).toContain('kept.ts');
395
393
  expect(content).toContain('removed.ts');
@@ -398,7 +396,7 @@ describe('sync-docs command', () => {
398
396
  fs.unlinkSync(path.join(srcDir, 'removed.ts'));
399
397
 
400
398
  // 重新运行 syncDocs(写入模式)
401
- await syncDocs({ projectPath: testDir });
399
+ await syncDocs({ projectPath: testDir }, io);
402
400
  content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
403
401
  expect(content).toContain('kept.ts');
404
402
  expect(content).not.toContain('removed.ts');
@@ -430,14 +428,14 @@ describe('sync-docs command', () => {
430
428
  ].join('\n')
431
429
  );
432
430
 
433
- await syncDocs({ projectPath: testDir });
431
+ await syncDocs({ projectPath: testDir }, io);
434
432
  const content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
435
433
  expect(content).toContain('DistillProposalCard.tsx');
436
434
 
437
435
  // 修复前:--check 每跑必报「包含已删除的模块」不收敛
438
- consoleSpy.mockClear();
439
- const check = await syncDocs({ projectPath: testDir, check: true });
440
- expect(check).toBe(true);
436
+ io = captureIO();
437
+ const check = await syncDocs({ projectPath: testDir, check: true }, io);
438
+ expect(check).toEqual({ kind: 'ok' });
441
439
 
442
440
  fs.rmSync(testDir, { recursive: true, force: true });
443
441
  });
@@ -454,11 +452,11 @@ describe('sync-docs command', () => {
454
452
  '# Capabilities\n\n| 模块 | 文件 | 说明 |\n|------|------|------|\n| util | src/util.ts | util |'
455
453
  );
456
454
 
457
- const check = await syncDocs({ projectPath: testDir, check: true });
458
- expect(check).toBe(false);
459
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Card.tsx'));
455
+ const check = await syncDocs({ projectPath: testDir, check: true }, io);
456
+ expect(check).toEqual({ kind: 'fail', reason: expect.stringContaining('文档不是最新') });
457
+ expect(io.outText()).toContain('Card.tsx');
460
458
 
461
- await syncDocs({ projectPath: testDir });
459
+ await syncDocs({ projectPath: testDir }, io);
462
460
  const content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
463
461
  expect(content).toContain('Card.tsx');
464
462
 
@@ -484,14 +482,14 @@ describe('sync-docs command', () => {
484
482
  ].join('\n')
485
483
  );
486
484
 
487
- await syncDocs({ projectPath: testDir });
485
+ await syncDocs({ projectPath: testDir }, io);
488
486
  const content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
489
487
  expect(content).toContain('src/helper.js');
490
488
  expect(content).toContain('src/Helper.jsx');
491
489
 
492
- consoleSpy.mockClear();
493
- const check = await syncDocs({ projectPath: testDir, check: true });
494
- expect(check).toBe(true);
490
+ io = captureIO();
491
+ const check = await syncDocs({ projectPath: testDir, check: true }, io);
492
+ expect(check).toEqual({ kind: 'ok' });
495
493
 
496
494
  fs.rmSync(testDir, { recursive: true, force: true });
497
495
  });
@@ -518,7 +516,7 @@ describe('sync-docs command', () => {
518
516
  ].join('\n'),
519
517
  );
520
518
 
521
- await syncDocs({ projectPath: testDir });
519
+ await syncDocs({ projectPath: testDir }, io);
522
520
  const content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
523
521
  expect(content).toContain('src/a/routes.ts');
524
522
  expect(content).not.toContain('src/ghost/routes.ts');
@@ -551,9 +549,9 @@ describe('sync-docs command', () => {
551
549
  '# Capabilities\n\n| 模块 | 文件 | 说明 |\n|------|------|------|\n| 核心 | src/core/ | 核心模块 |'
552
550
  );
553
551
 
554
- const result = await syncDocs({ projectPath: testDir });
552
+ const result = await syncDocs({ projectPath: testDir }, io);
555
553
  // 有未覆盖目录,仍报不同步
556
- expect(result).toBe(false);
554
+ expect(result).toEqual({ kind: 'ok' });
557
555
  // 但不自动给 b.ts 加表格行
558
556
  const content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
559
557
  expect(content).not.toContain('b.ts');
@@ -575,10 +573,10 @@ describe('sync-docs command', () => {
575
573
  '# Capabilities\n\n| 模块 | 文件 | 说明 |\n|------|------|------|\n| 核心 | src/core/ | 核心模块 |'
576
574
  );
577
575
 
578
- const result = await syncDocs({ projectPath: testDir, check: true });
579
- expect(result).toBe(false);
580
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('未登记以下模块(目录)'));
581
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('src/newdir/'));
576
+ const result = await syncDocs({ projectPath: testDir, check: true }, io);
577
+ expect(result).toEqual({ kind: 'fail', reason: expect.stringContaining('文档不是最新') });
578
+ expect(io.outText()).toContain('未登记以下模块(目录)');
579
+ expect(io.outText()).toContain('src/newdir/');
582
580
 
583
581
  fs.rmSync(testDir, { recursive: true, force: true });
584
582
  });
@@ -596,10 +594,10 @@ describe('sync-docs command', () => {
596
594
  '# Capabilities\n\n| 模块 | 文件 | 说明 |\n|------|------|------|\n| 核心 | src/core/ | 核心模块 |'
597
595
  );
598
596
 
599
- const result = await syncDocs({ projectPath: testDir, check: true, json: true });
600
- expect(result).toBe(false);
597
+ const result = await syncDocs({ projectPath: testDir, check: true, json: true }, io);
598
+ expect(result).toEqual({ kind: 'fail', reason: expect.stringContaining('文档不是最新') });
601
599
 
602
- const jsonCall = consoleSpy.mock.calls.find(
600
+ const jsonCall = io.outRecords().map(chunk => [chunk] as string[]).find(
603
601
  (call: unknown[]) => typeof call[0] === 'string' && call[0].startsWith('{')
604
602
  );
605
603
  expect(jsonCall).toBeDefined();
@@ -628,7 +626,7 @@ describe('sync-docs command', () => {
628
626
  ].join('\n')
629
627
  );
630
628
 
631
- await syncDocs({ projectPath: testDir });
629
+ await syncDocs({ projectPath: testDir }, io);
632
630
  const content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
633
631
  expect(content).toContain('src/a/kept.ts');
634
632
  expect(content).not.toContain('ghost.ts');
@@ -658,8 +656,8 @@ describe('sync-docs command', () => {
658
656
  ].join('\n')
659
657
  );
660
658
 
661
- const result = await syncDocs({ projectPath: testDir, compact: true });
662
- expect(result).toBe(true);
659
+ const result = await syncDocs({ projectPath: testDir, compact: true }, io);
660
+ expect(result).toEqual({ kind: 'ok' });
663
661
 
664
662
  const content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
665
663
  // 折叠为目录条目(说明取组内第一行)
@@ -672,7 +670,7 @@ describe('sync-docs command', () => {
672
670
  expect(content).toContain('手工说明');
673
671
 
674
672
  // 幂等:再跑一遍内容不变
675
- await syncDocs({ projectPath: testDir, compact: true });
673
+ await syncDocs({ projectPath: testDir, compact: true }, io);
676
674
  const content2 = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
677
675
  expect(content2).toBe(content);
678
676
 
@@ -683,8 +681,8 @@ describe('sync-docs command', () => {
683
681
  const testDir = path.join(tempDir, 'compact-missing');
684
682
  fs.mkdirSync(testDir, { recursive: true });
685
683
 
686
- const result = await syncDocs({ projectPath: testDir, compact: true });
687
- expect(result).toBe(false);
684
+ const result = await syncDocs({ projectPath: testDir, compact: true }, io);
685
+ expect(result).toEqual({ kind: 'ok' });
688
686
 
689
687
  fs.rmSync(testDir, { recursive: true, force: true });
690
688
  });
@@ -8,8 +8,9 @@
8
8
  */
9
9
 
10
10
  import * as fs from 'fs';
11
+ import { captureIO, lastJsonOutput, type CapturingIO } from '../../command-contract';
11
12
  import * as path from 'path';
12
- import { updateUserModel } from '../update-user-model';
13
+ import { updateUserModel, resolveUserModelPaths } from '../update-user-model';
13
14
  import { readTranscriptSessions, type MinedSession } from '../../session-mining';
14
15
 
15
16
  const TEST_HOME = '/tmp/harness-uum-test-home';
@@ -53,6 +54,7 @@ const mockReadTranscriptSessions = readTranscriptSessions as jest.MockedFunction
53
54
  >;
54
55
 
55
56
  const STATE_FILE = path.join(TEST_HOME, '.claude', 'user-model-state.json');
57
+ const PROFILE_FILE = path.join(TEST_HOME, '.claude', 'projects', '-root-projects', 'memory', 'user_profile.md');
56
58
 
57
59
  function todayStr(offsetDays = 0): string {
58
60
  return new Date(Date.now() - offsetDays * 86_400_000).toISOString().slice(0, 10);
@@ -72,33 +74,63 @@ function mkSession(partial: Partial<MinedSession> = {}): MinedSession {
72
74
  };
73
75
  }
74
76
 
75
- function lastJsonOutput(consoleSpy: jest.SpyInstance): Record<string, unknown> {
76
- const jsonLine = consoleSpy.mock.calls.map(c => c[0]).join('\n');
77
- return JSON.parse(jsonLine);
78
- }
77
+ let io: CapturingIO;
78
+ beforeEach(() => {
79
+ io = captureIO();
80
+ });
79
81
 
80
82
  describe('update-user-model command', () => {
81
- let consoleSpy: jest.SpyInstance;
82
83
 
83
84
  beforeEach(() => {
84
85
  jest.clearAllMocks();
85
86
  fs.rmSync(TEST_HOME, { recursive: true, force: true });
86
87
  fs.mkdirSync(path.join(TEST_HOME, '.claude', 'projects', '-root-projects', 'memory'), { recursive: true });
87
88
  process.env.CLAUDE_TRANSCRIPTS_DIR = path.join(TEST_HOME, 'transcripts');
88
- consoleSpy = jest.spyOn(console, 'log').mockImplementation();
89
89
  });
90
90
 
91
91
  afterEach(() => {
92
- consoleSpy.mockRestore();
93
92
  delete process.env.CLAUDE_TRANSCRIPTS_DIR;
93
+ delete process.env.HARNESS_UUM_STATE_FILE;
94
+ delete process.env.HARNESS_UUM_PROFILE_FILE;
95
+ });
96
+
97
+ // 路径解析(harness#116):默认值保持原行为,env 覆盖生效
98
+ test('resolveUserModelPaths 默认:homedir 下的原路径(现行为不变)', () => {
99
+ expect(resolveUserModelPaths({})).toEqual({
100
+ stateFile: STATE_FILE,
101
+ profileFile: PROFILE_FILE,
102
+ });
103
+ });
104
+
105
+ test('resolveUserModelPaths env 覆盖:HARNESS_UUM_STATE_FILE / HARNESS_UUM_PROFILE_FILE', () => {
106
+ expect(resolveUserModelPaths({
107
+ HARNESS_UUM_STATE_FILE: '/custom/state.json',
108
+ HARNESS_UUM_PROFILE_FILE: '/custom/profile.md',
109
+ })).toEqual({
110
+ stateFile: '/custom/state.json',
111
+ profileFile: '/custom/profile.md',
112
+ });
113
+ });
114
+
115
+ test('env 覆盖端到端:state 落 env 指定路径,不写默认路径', async () => {
116
+ const customState = path.join(TEST_HOME, 'custom', 'state.json');
117
+ process.env.HARNESS_UUM_STATE_FILE = customState;
118
+ mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'session-a' })]);
119
+
120
+ await updateUserModel({}, io);
121
+
122
+ expect(fs.existsSync(customState)).toBe(true);
123
+ expect(fs.existsSync(STATE_FILE)).toBe(false);
124
+ const state = JSON.parse(fs.readFileSync(customState, 'utf-8'));
125
+ expect(state.sessionsProcessed).toContain('session-a');
94
126
  });
95
127
 
96
128
  test('无新会话:提示 No new sessions to process', async () => {
97
129
  mockReadTranscriptSessions.mockReturnValue([]);
98
130
 
99
- await updateUserModel({});
131
+ await updateUserModel({}, io);
100
132
 
101
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No new sessions to process'));
133
+ expect(io.outText()).toContain('No new sessions to process');
102
134
  expect(fs.existsSync(STATE_FILE)).toBe(false);
103
135
  });
104
136
 
@@ -108,9 +140,9 @@ describe('update-user-model command', () => {
108
140
  mkSession({ id: 'session-b' }),
109
141
  ]);
110
142
 
111
- await updateUserModel({ json: true, dryRun: true });
143
+ await updateUserModel({ json: true, dryRun: true }, io);
112
144
 
113
- const output = lastJsonOutput(consoleSpy);
145
+ const output = lastJsonOutput(io);
114
146
  expect(output.newSessions).toBe(2);
115
147
  expect(output.changes).toEqual(
116
148
  expect.arrayContaining([
@@ -125,23 +157,37 @@ describe('update-user-model command', () => {
125
157
  test('--dry-run:只展示变化,不写 state 文件', async () => {
126
158
  mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'session-a' })]);
127
159
 
128
- await updateUserModel({ dryRun: true });
160
+ await updateUserModel({ dryRun: true }, io);
129
161
 
130
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Processed 1 new sessions'));
162
+ expect(io.outText()).toContain('Processed 1 new sessions');
131
163
  expect(fs.existsSync(STATE_FILE)).toBe(false);
132
164
  });
133
165
 
134
166
  test('默认(非 dry-run):落盘 state 并记录已处理会话', async () => {
135
167
  mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'session-a' })]);
136
168
 
137
- await updateUserModel({});
169
+ await updateUserModel({}, io);
138
170
 
139
171
  expect(fs.existsSync(STATE_FILE)).toBe(true);
140
172
  const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
141
173
  expect(state.sessionsProcessed).toContain('session-a');
142
174
  });
143
175
 
144
- test('--days 1:只处理最近 1 天的会话', async () => {
176
+ test('--days 1:since 过滤下推到 seam(自然日窗口,含今天)', async () => {
177
+ mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'today' })]);
178
+
179
+ await updateUserModel({ json: true, dryRun: true, days: 1 }, io);
180
+
181
+ const output = lastJsonOutput(io);
182
+ expect(output.newSessions).toBe(1);
183
+ // date >= 今日 ⟺ mtimeMs >= 今日 UTC 零点,过滤在 seam 内 stat 级完成
184
+ expect(mockReadTranscriptSessions).toHaveBeenCalledWith(
185
+ process.env.CLAUDE_TRANSCRIPTS_DIR,
186
+ { excludeIds: [], since: Date.parse(todayStr()) },
187
+ );
188
+ });
189
+
190
+ test('缺省 --days:处理全部未处理会话(向后兼容)', async () => {
145
191
  mockReadTranscriptSessions.mockReturnValue([
146
192
  mkSession({ id: 'today' }),
147
193
  mkSession({
@@ -154,32 +200,48 @@ describe('update-user-model command', () => {
154
200
  }),
155
201
  ]);
156
202
 
157
- await updateUserModel({ json: true, dryRun: true, days: 1 });
203
+ await updateUserModel({ json: true, dryRun: true }, io);
158
204
 
159
- const output = lastJsonOutput(consoleSpy);
160
- expect(output.newSessions).toBe(1);
205
+ const output = lastJsonOutput(io);
206
+ expect(output.newSessions).toBe(2);
207
+ // 缺省 days:不下推 since(向后兼容全量)
208
+ expect(mockReadTranscriptSessions).toHaveBeenCalledWith(
209
+ process.env.CLAUDE_TRANSCRIPTS_DIR,
210
+ { excludeIds: [] },
211
+ );
161
212
  });
162
213
 
163
- test('缺省 --days:处理全部未处理会话(向后兼容)', async () => {
214
+ test('同一 concept 只计一次:occurrences 等于 merged 聚合值(#113 双计 bug)', async () => {
215
+ mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'session-a' })]);
216
+
217
+ await updateUserModel({}, io);
218
+
219
+ const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
220
+ // userText 整条中文序列恰为一个 concept(count=1);聚合后只应出现一次,
221
+ // 双计 bug 下会被 merged 循环 + per-session 循环各加一次变成 2
222
+ expect(state.patterns['数据库迁移方案需要执行'].occurrences).toBe(1);
223
+ expect(state.patterns['数据库迁移方案需要执行'].sessions).toEqual(['session-a']);
224
+ });
225
+
226
+ test('correction phrase 的 ×3 加权不受影响(buildMergedConcepts 内)', async () => {
164
227
  mockReadTranscriptSessions.mockReturnValue([
165
- mkSession({ id: 'today' }),
166
228
  mkSession({
167
- id: 'three-days-ago',
168
- date: todayStr(3),
229
+ id: 'session-a',
169
230
  turns: [
170
- { role: 'user', content: '旧会话概念内容测试' },
231
+ { role: 'user', content: '这种问题反复出现' },
171
232
  { role: 'assistant', content: '' },
172
233
  ],
173
234
  }),
174
235
  ]);
175
236
 
176
- await updateUserModel({ json: true, dryRun: true });
237
+ await updateUserModel({}, io);
177
238
 
178
- const output = lastJsonOutput(consoleSpy);
179
- expect(output.newSessions).toBe(2);
239
+ const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
240
+ // 纠正语句提取的 concept 按 ×3 计入(裁决:权重在 buildMergedConcepts,删循环不影响)
241
+ expect(state.patterns['这种问题'].occurrences).toBe(3);
180
242
  });
181
243
 
182
- test('sessionsProcessed 去重:已处理会话不重复计入(与 --days 正交)', async () => {
244
+ test('sessionsProcessed 去重:已处理会话经 excludeIds 下推排除', async () => {
183
245
  fs.writeFileSync(
184
246
  STATE_FILE,
185
247
  JSON.stringify({
@@ -192,14 +254,15 @@ describe('update-user-model command', () => {
192
254
  }),
193
255
  'utf-8',
194
256
  );
195
- mockReadTranscriptSessions.mockReturnValue([
196
- mkSession({ id: 'session-a' }),
197
- mkSession({ id: 'session-b' }),
198
- ]);
257
+ mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'session-b' })]);
199
258
 
200
- await updateUserModel({ json: true, dryRun: true, days: 7 });
259
+ await updateUserModel({ json: true, dryRun: true, days: 7 }, io);
201
260
 
202
- const output = lastJsonOutput(consoleSpy);
261
+ const output = lastJsonOutput(io);
203
262
  expect(output.newSessions).toBe(1);
263
+ expect(mockReadTranscriptSessions).toHaveBeenCalledWith(
264
+ process.env.CLAUDE_TRANSCRIPTS_DIR,
265
+ { excludeIds: ['session-a'], since: expect.any(Number) },
266
+ );
204
267
  });
205
268
  });