@dommaker/harness 0.16.9 → 0.17.1

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 (337) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +33 -32
  3. package/bin/harness.js +33 -20
  4. package/dist/cli/commands/check.d.ts +3 -2
  5. package/dist/cli/commands/check.d.ts.map +1 -1
  6. package/dist/cli/commands/check.js +69 -36
  7. package/dist/cli/commands/check.js.map +1 -1
  8. package/dist/cli/commands/constraints-report.d.ts +42 -0
  9. package/dist/cli/commands/constraints-report.d.ts.map +1 -0
  10. package/dist/cli/commands/constraints-report.js +233 -0
  11. package/dist/cli/commands/constraints-report.js.map +1 -0
  12. package/dist/cli/commands/constraints-retire.d.ts +71 -0
  13. package/dist/cli/commands/constraints-retire.d.ts.map +1 -0
  14. package/dist/cli/commands/constraints-retire.js +384 -0
  15. package/dist/cli/commands/constraints-retire.js.map +1 -0
  16. package/dist/cli/commands/constraints.d.ts +1 -1
  17. package/dist/cli/commands/constraints.d.ts.map +1 -1
  18. package/dist/cli/commands/constraints.js +4 -4
  19. package/dist/cli/commands/constraints.js.map +1 -1
  20. package/dist/cli/commands/index.d.ts +2 -1
  21. package/dist/cli/commands/index.d.ts.map +1 -1
  22. package/dist/cli/commands/index.js +10 -4
  23. package/dist/cli/commands/index.js.map +1 -1
  24. package/dist/cli/commands/init.d.ts +20 -0
  25. package/dist/cli/commands/init.d.ts.map +1 -1
  26. package/dist/cli/commands/init.js +81 -59
  27. package/dist/cli/commands/init.js.map +1 -1
  28. package/dist/cli/commands/report.js +4 -4
  29. package/dist/cli/commands/report.js.map +1 -1
  30. package/dist/cli/commands/status.d.ts.map +1 -1
  31. package/dist/cli/commands/status.js +3 -4
  32. package/dist/cli/commands/status.js.map +1 -1
  33. package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts +5 -1
  34. package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts.map +1 -1
  35. package/dist/cli/commands/sync-docs/capabilities-syncer.js +2 -10
  36. package/dist/cli/commands/sync-docs/capabilities-syncer.js.map +1 -1
  37. package/dist/cli/commands/validate.d.ts.map +1 -1
  38. package/dist/cli/commands/validate.js +1 -5
  39. package/dist/cli/commands/validate.js.map +1 -1
  40. package/dist/core/constraints/capabilities-parser.d.ts +7 -0
  41. package/dist/core/constraints/capabilities-parser.d.ts.map +1 -1
  42. package/dist/core/constraints/capabilities-parser.js +13 -0
  43. package/dist/core/constraints/capabilities-parser.js.map +1 -1
  44. package/dist/core/constraints/checker.d.ts +13 -5
  45. package/dist/core/constraints/checker.d.ts.map +1 -1
  46. package/dist/core/constraints/checker.js +62 -25
  47. package/dist/core/constraints/checker.js.map +1 -1
  48. package/dist/core/constraints/checkers/capability-sync.d.ts +9 -2
  49. package/dist/core/constraints/checkers/capability-sync.d.ts.map +1 -1
  50. package/dist/core/constraints/checkers/capability-sync.js +55 -22
  51. package/dist/core/constraints/checkers/capability-sync.js.map +1 -1
  52. package/dist/core/constraints/checkers/context-doc-sync.d.ts +3 -0
  53. package/dist/core/constraints/checkers/context-doc-sync.d.ts.map +1 -1
  54. package/dist/core/constraints/checkers/context-doc-sync.js +8 -2
  55. package/dist/core/constraints/checkers/context-doc-sync.js.map +1 -1
  56. package/dist/core/constraints/checkers/docs-freshness.d.ts +1 -0
  57. package/dist/core/constraints/checkers/docs-freshness.d.ts.map +1 -1
  58. package/dist/core/constraints/checkers/docs-freshness.js +39 -0
  59. package/dist/core/constraints/checkers/docs-freshness.js.map +1 -1
  60. package/dist/core/constraints/checkers/index.d.ts +8 -4
  61. package/dist/core/constraints/checkers/index.d.ts.map +1 -1
  62. package/dist/core/constraints/checkers/index.js +29 -46
  63. package/dist/core/constraints/checkers/index.js.map +1 -1
  64. package/dist/core/constraints/checkers/iron-flags.d.ts +6 -7
  65. package/dist/core/constraints/checkers/iron-flags.d.ts.map +1 -1
  66. package/dist/core/constraints/checkers/iron-flags.js +10 -17
  67. package/dist/core/constraints/checkers/iron-flags.js.map +1 -1
  68. package/dist/core/constraints/checkers/no-hardcoded-credentials.d.ts +13 -0
  69. package/dist/core/constraints/checkers/no-hardcoded-credentials.d.ts.map +1 -0
  70. package/dist/core/constraints/checkers/no-hardcoded-credentials.js +91 -0
  71. package/dist/core/constraints/checkers/no-hardcoded-credentials.js.map +1 -0
  72. package/dist/core/constraints/checkers/types.d.ts +15 -4
  73. package/dist/core/constraints/checkers/types.d.ts.map +1 -1
  74. package/dist/core/constraints/checkers/types.js +16 -4
  75. package/dist/core/constraints/checkers/types.js.map +1 -1
  76. package/dist/core/constraints/context-builder.d.ts +6 -3
  77. package/dist/core/constraints/context-builder.d.ts.map +1 -1
  78. package/dist/core/constraints/context-builder.js +29 -20
  79. package/dist/core/constraints/context-builder.js.map +1 -1
  80. package/dist/core/constraints/definitions/guidelines.d.ts +6 -2
  81. package/dist/core/constraints/definitions/guidelines.d.ts.map +1 -1
  82. package/dist/core/constraints/definitions/guidelines.js +29 -572
  83. package/dist/core/constraints/definitions/guidelines.js.map +1 -1
  84. package/dist/core/constraints/definitions/iron-laws.d.ts +6 -2
  85. package/dist/core/constraints/definitions/iron-laws.d.ts.map +1 -1
  86. package/dist/core/constraints/definitions/iron-laws.js +48 -231
  87. package/dist/core/constraints/definitions/iron-laws.js.map +1 -1
  88. package/dist/core/constraints/definitions/prompts.d.ts +12 -0
  89. package/dist/core/constraints/definitions/prompts.d.ts.map +1 -0
  90. package/dist/core/constraints/definitions/prompts.js +405 -0
  91. package/dist/core/constraints/definitions/prompts.js.map +1 -0
  92. package/dist/core/constraints/definitions.d.ts +15 -9
  93. package/dist/core/constraints/definitions.d.ts.map +1 -1
  94. package/dist/core/constraints/definitions.js +18 -12
  95. package/dist/core/constraints/definitions.js.map +1 -1
  96. package/dist/core/constraints/index.d.ts +3 -2
  97. package/dist/core/constraints/index.d.ts.map +1 -1
  98. package/dist/core/constraints/index.js +8 -2
  99. package/dist/core/constraints/index.js.map +1 -1
  100. package/dist/core/constraints/injection-drift.d.ts +48 -0
  101. package/dist/core/constraints/injection-drift.d.ts.map +1 -0
  102. package/dist/core/constraints/injection-drift.js +140 -0
  103. package/dist/core/constraints/injection-drift.js.map +1 -0
  104. package/dist/core/constraints/injection-renderer.d.ts +23 -0
  105. package/dist/core/constraints/injection-renderer.d.ts.map +1 -0
  106. package/dist/core/constraints/injection-renderer.js +44 -0
  107. package/dist/core/constraints/injection-renderer.js.map +1 -0
  108. package/dist/core/constraints/interceptor.d.ts.map +1 -1
  109. package/dist/core/constraints/interceptor.js +3 -2
  110. package/dist/core/constraints/interceptor.js.map +1 -1
  111. package/dist/core/constraints/usage-report.d.ts +95 -0
  112. package/dist/core/constraints/usage-report.d.ts.map +1 -0
  113. package/dist/core/constraints/usage-report.js +206 -0
  114. package/dist/core/constraints/usage-report.js.map +1 -0
  115. package/dist/core/effective-constraints.d.ts +47 -0
  116. package/dist/core/effective-constraints.d.ts.map +1 -0
  117. package/dist/core/effective-constraints.js +60 -0
  118. package/dist/core/effective-constraints.js.map +1 -0
  119. package/dist/core/index.d.ts +1 -0
  120. package/dist/core/index.d.ts.map +1 -1
  121. package/dist/core/index.js +2 -0
  122. package/dist/core/index.js.map +1 -1
  123. package/dist/core/project-config-loader.d.ts +17 -2
  124. package/dist/core/project-config-loader.d.ts.map +1 -1
  125. package/dist/core/project-config-loader.js +55 -8
  126. package/dist/core/project-config-loader.js.map +1 -1
  127. package/dist/dashboard/data.d.ts +1 -9
  128. package/dist/dashboard/data.d.ts.map +1 -1
  129. package/dist/dashboard/data.js +7 -15
  130. package/dist/dashboard/data.js.map +1 -1
  131. package/dist/dashboard/types.d.ts +13 -1
  132. package/dist/dashboard/types.d.ts.map +1 -1
  133. package/dist/index.d.ts +0 -4
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +1 -13
  136. package/dist/index.js.map +1 -1
  137. package/dist/monitoring/index.d.ts +0 -2
  138. package/dist/monitoring/index.d.ts.map +1 -1
  139. package/dist/monitoring/index.js +0 -2
  140. package/dist/monitoring/index.js.map +1 -1
  141. package/dist/monitoring/knowledge-evolver.d.ts +1 -1
  142. package/dist/monitoring/knowledge-evolver.js +1 -1
  143. package/dist/monitoring/trace-analyzer.d.ts +1 -1
  144. package/dist/monitoring/trace-analyzer.d.ts.map +1 -1
  145. package/dist/monitoring/trace-analyzer.js +16 -10
  146. package/dist/monitoring/trace-analyzer.js.map +1 -1
  147. package/dist/monitoring/traces.d.ts +2 -1
  148. package/dist/monitoring/traces.d.ts.map +1 -1
  149. package/dist/monitoring/traces.js +5 -3
  150. package/dist/monitoring/traces.js.map +1 -1
  151. package/dist/presets/standard.d.ts +3 -1
  152. package/dist/presets/standard.d.ts.map +1 -1
  153. package/dist/presets/standard.js +18 -8
  154. package/dist/presets/standard.js.map +1 -1
  155. package/dist/types/constraint.d.ts +51 -11
  156. package/dist/types/constraint.d.ts.map +1 -1
  157. package/dist/types/constraint.js +8 -4
  158. package/dist/types/constraint.js.map +1 -1
  159. package/dist/types/index.d.ts +1 -1
  160. package/dist/types/index.d.ts.map +1 -1
  161. package/dist/types/index.js.map +1 -1
  162. package/dist/types/monitoring-types.d.ts +2 -63
  163. package/dist/types/monitoring-types.d.ts.map +1 -1
  164. package/dist/types/monitoring-types.js +2 -2
  165. package/dist/types/project-config.d.ts +26 -1
  166. package/dist/types/project-config.d.ts.map +1 -1
  167. package/dist/types/trace.d.ts +16 -7
  168. package/dist/types/trace.d.ts.map +1 -1
  169. package/dist/types/trace.js +8 -0
  170. package/dist/types/trace.js.map +1 -1
  171. package/package.json +1 -1
  172. package/src/CONTEXT.md +2 -2
  173. package/src/__tests__/checker-extra.test.ts +228 -333
  174. package/src/__tests__/checker.test.ts +87 -724
  175. package/src/__tests__/constraint-doctor.test.ts +5 -5
  176. package/src/__tests__/doctor-extra.test.ts +164 -0
  177. package/src/__tests__/effective-constraints.test.ts +147 -0
  178. package/src/__tests__/extend-exceptions.test.ts +3 -3
  179. package/src/__tests__/iron-laws.test.ts +115 -91
  180. package/src/__tests__/no-hardcoded-credentials.test.ts +90 -0
  181. package/src/__tests__/presets.test.ts +7 -7
  182. package/src/__tests__/project-config-loader-extra.test.ts +41 -4
  183. package/src/__tests__/skip-semantics.test.ts +322 -0
  184. package/src/cli/commands/CONTEXT.md +5 -3
  185. package/src/cli/commands/__tests__/check-drift.test.ts +127 -0
  186. package/src/cli/commands/__tests__/check-skip-output.test.ts +128 -0
  187. package/src/cli/commands/__tests__/check.test.ts +14 -25
  188. package/src/cli/commands/__tests__/constraints-report.test.ts +327 -0
  189. package/src/cli/commands/__tests__/constraints-retire.test.ts +283 -0
  190. package/src/cli/commands/__tests__/init-injection.test.ts +234 -0
  191. package/src/cli/commands/__tests__/status-extra.test.ts +11 -11
  192. package/src/cli/commands/__tests__/status.test.ts +1 -1
  193. package/src/cli/commands/check.ts +72 -38
  194. package/src/cli/commands/constraints-report.ts +234 -0
  195. package/src/cli/commands/constraints-retire.ts +443 -0
  196. package/src/cli/commands/constraints.ts +5 -5
  197. package/src/cli/commands/index.ts +2 -1
  198. package/src/cli/commands/init.ts +94 -66
  199. package/src/cli/commands/report.ts +6 -6
  200. package/src/cli/commands/status.ts +3 -4
  201. package/src/cli/commands/sync-docs/capabilities-syncer.ts +5 -7
  202. package/src/cli/commands/validate.ts +1 -5
  203. package/src/core/CONTEXT.md +7 -4
  204. package/src/core/constraints/__tests__/injection-drift.test.ts +164 -0
  205. package/src/core/constraints/__tests__/interceptor.test.ts +4 -0
  206. package/src/core/constraints/capabilities-parser.ts +13 -0
  207. package/src/core/constraints/checker.ts +73 -28
  208. package/src/core/constraints/checkers/capability-sync.ts +72 -29
  209. package/src/core/constraints/checkers/context-doc-sync.ts +9 -2
  210. package/src/core/constraints/checkers/docs-freshness.ts +43 -0
  211. package/src/core/constraints/checkers/index.ts +38 -71
  212. package/src/core/constraints/checkers/iron-flags.ts +13 -49
  213. package/src/core/constraints/checkers/no-hardcoded-credentials.ts +93 -0
  214. package/src/core/constraints/checkers/types.ts +27 -5
  215. package/src/core/constraints/context-builder.ts +28 -19
  216. package/src/core/constraints/definitions/guidelines.ts +29 -597
  217. package/src/core/constraints/definitions/iron-laws.ts +48 -239
  218. package/src/core/constraints/definitions/prompts.ts +419 -0
  219. package/src/core/constraints/definitions.ts +18 -11
  220. package/src/core/constraints/index.ts +10 -1
  221. package/src/core/constraints/injection-drift.ts +139 -0
  222. package/src/core/constraints/injection-renderer.ts +44 -0
  223. package/src/core/constraints/interceptor.ts +3 -2
  224. package/src/core/constraints/usage-report.ts +254 -0
  225. package/src/core/effective-constraints.ts +85 -0
  226. package/src/core/index.ts +4 -1
  227. package/src/core/project-config-loader.ts +59 -9
  228. package/src/dashboard/__tests__/data.test.ts +5 -12
  229. package/src/dashboard/data.ts +9 -19
  230. package/src/dashboard/types.ts +18 -1
  231. package/src/index.ts +0 -12
  232. package/src/knowledge/CONTEXT.md +2 -2
  233. package/src/monitoring/CONTEXT.md +3 -4
  234. package/src/monitoring/index.ts +0 -2
  235. package/src/monitoring/knowledge-evolver.ts +1 -1
  236. package/src/monitoring/trace-analyzer.ts +15 -10
  237. package/src/monitoring/traces.ts +10 -6
  238. package/src/presets/__tests__/standard.test.ts +11 -9
  239. package/src/presets/standard.ts +24 -15
  240. package/src/types/constraint.ts +59 -14
  241. package/src/types/index.ts +1 -3
  242. package/src/types/monitoring-types.ts +2 -84
  243. package/src/types/project-config.ts +30 -1
  244. package/src/types/trace.ts +18 -7
  245. package/src/verification/CONTEXT.md +1 -1
  246. package/dist/cli/commands/flow.d.ts +0 -22
  247. package/dist/cli/commands/flow.d.ts.map +0 -1
  248. package/dist/cli/commands/flow.js +0 -275
  249. package/dist/cli/commands/flow.js.map +0 -1
  250. package/dist/constraints/lifecycle-runner.d.ts +0 -52
  251. package/dist/constraints/lifecycle-runner.d.ts.map +0 -1
  252. package/dist/constraints/lifecycle-runner.js +0 -151
  253. package/dist/constraints/lifecycle-runner.js.map +0 -1
  254. package/dist/constraints/registry.d.ts +0 -27
  255. package/dist/constraints/registry.d.ts.map +0 -1
  256. package/dist/constraints/registry.js +0 -128
  257. package/dist/constraints/registry.js.map +0 -1
  258. package/dist/constraints/types.d.ts +0 -53
  259. package/dist/constraints/types.d.ts.map +0 -1
  260. package/dist/constraints/types.js +0 -8
  261. package/dist/constraints/types.js.map +0 -1
  262. package/dist/core/constraints/checkers/always-pass.d.ts +0 -17
  263. package/dist/core/constraints/checkers/always-pass.d.ts.map +0 -1
  264. package/dist/core/constraints/checkers/always-pass.js +0 -23
  265. package/dist/core/constraints/checkers/always-pass.js.map +0 -1
  266. package/dist/core/constraints/checkers/guideline-flags.d.ts +0 -9
  267. package/dist/core/constraints/checkers/guideline-flags.d.ts.map +0 -1
  268. package/dist/core/constraints/checkers/guideline-flags.js +0 -13
  269. package/dist/core/constraints/checkers/guideline-flags.js.map +0 -1
  270. package/dist/core/constraints/checkers/no-any-type.d.ts +0 -6
  271. package/dist/core/constraints/checkers/no-any-type.d.ts.map +0 -1
  272. package/dist/core/constraints/checkers/no-any-type.js +0 -42
  273. package/dist/core/constraints/checkers/no-any-type.js.map +0 -1
  274. package/dist/core/constraints/checkers/no-excuse-patterns.d.ts +0 -6
  275. package/dist/core/constraints/checkers/no-excuse-patterns.d.ts.map +0 -1
  276. package/dist/core/constraints/checkers/no-excuse-patterns.js +0 -25
  277. package/dist/core/constraints/checkers/no-excuse-patterns.js.map +0 -1
  278. package/dist/core/constraints/checkers/no-fuzzy-completion-claim.d.ts +0 -6
  279. package/dist/core/constraints/checkers/no-fuzzy-completion-claim.d.ts.map +0 -1
  280. package/dist/core/constraints/checkers/no-fuzzy-completion-claim.js +0 -34
  281. package/dist/core/constraints/checkers/no-fuzzy-completion-claim.js.map +0 -1
  282. package/dist/core/constraints/checkers/no-performative-agreement.d.ts +0 -8
  283. package/dist/core/constraints/checkers/no-performative-agreement.d.ts.map +0 -1
  284. package/dist/core/constraints/checkers/no-performative-agreement.js +0 -23
  285. package/dist/core/constraints/checkers/no-performative-agreement.js.map +0 -1
  286. package/dist/core/constraints/checkers/no-simplification-without-approval.d.ts +0 -6
  287. package/dist/core/constraints/checkers/no-simplification-without-approval.d.ts.map +0 -1
  288. package/dist/core/constraints/checkers/no-simplification-without-approval.js +0 -30
  289. package/dist/core/constraints/checkers/no-simplification-without-approval.js.map +0 -1
  290. package/dist/core/constraints/checkers/test-coverage.d.ts +0 -6
  291. package/dist/core/constraints/checkers/test-coverage.d.ts.map +0 -1
  292. package/dist/core/constraints/checkers/test-coverage.js +0 -33
  293. package/dist/core/constraints/checkers/test-coverage.js.map +0 -1
  294. package/dist/core/constraints/checkers/yagni.d.ts +0 -8
  295. package/dist/core/constraints/checkers/yagni.d.ts.map +0 -1
  296. package/dist/core/constraints/checkers/yagni.js +0 -96
  297. package/dist/core/constraints/checkers/yagni.js.map +0 -1
  298. package/dist/core/constraints/definitions/tips.d.ts +0 -7
  299. package/dist/core/constraints/definitions/tips.d.ts.map +0 -1
  300. package/dist/core/constraints/definitions/tips.js +0 -62
  301. package/dist/core/constraints/definitions/tips.js.map +0 -1
  302. package/dist/evolution/auto-evolve.d.ts +0 -35
  303. package/dist/evolution/auto-evolve.d.ts.map +0 -1
  304. package/dist/evolution/auto-evolve.js +0 -64
  305. package/dist/evolution/auto-evolve.js.map +0 -1
  306. package/dist/evolution/index.d.ts +0 -2
  307. package/dist/evolution/index.d.ts.map +0 -1
  308. package/dist/evolution/index.js +0 -18
  309. package/dist/evolution/index.js.map +0 -1
  310. package/dist/monitoring/constraint-evolver.d.ts +0 -120
  311. package/dist/monitoring/constraint-evolver.d.ts.map +0 -1
  312. package/dist/monitoring/constraint-evolver.js +0 -410
  313. package/dist/monitoring/constraint-evolver.js.map +0 -1
  314. package/src/__tests__/constraint-evolver.test.ts +0 -405
  315. package/src/__tests__/doctor-evolver.test.ts +0 -381
  316. package/src/cli/commands/__tests__/flow.test.ts +0 -343
  317. package/src/cli/commands/flow.ts +0 -277
  318. package/src/constraints/__tests__/lifecycle-runner.test.ts +0 -246
  319. package/src/constraints/__tests__/registry.test.ts +0 -196
  320. package/src/constraints/lifecycle-runner.ts +0 -184
  321. package/src/constraints/registry.ts +0 -134
  322. package/src/constraints/types.ts +0 -59
  323. package/src/core/constraints/checkers/always-pass.ts +0 -22
  324. package/src/core/constraints/checkers/guideline-flags.ts +0 -30
  325. package/src/core/constraints/checkers/no-any-type.ts +0 -42
  326. package/src/core/constraints/checkers/no-excuse-patterns.ts +0 -24
  327. package/src/core/constraints/checkers/no-fuzzy-completion-claim.ts +0 -33
  328. package/src/core/constraints/checkers/no-performative-agreement.ts +0 -22
  329. package/src/core/constraints/checkers/no-simplification-without-approval.ts +0 -29
  330. package/src/core/constraints/checkers/test-coverage.ts +0 -35
  331. package/src/core/constraints/checkers/yagni.ts +0 -65
  332. package/src/core/constraints/definitions/tips.ts +0 -63
  333. package/src/evolution/CONTEXT.md +0 -22
  334. package/src/evolution/__tests__/auto-evolve.test.ts +0 -319
  335. package/src/evolution/auto-evolve.ts +0 -98
  336. package/src/evolution/index.ts +0 -1
  337. package/src/monitoring/constraint-evolver.ts +0 -501
@@ -0,0 +1,128 @@
1
+ /**
2
+ * check 命令 skip 输出形态测试(ADR-0001 P4)
3
+ *
4
+ * skipped 结果单独列示,不进 pass/fail 统计。
5
+ */
6
+
7
+ import { check } from '../check';
8
+ import * as fs from 'fs';
9
+ import { constraintChecker } from '../../../core/constraints/checker';
10
+ import { ProjectConfigLoader } from '../../../core/project-config-loader';
11
+
12
+ // Mock fs
13
+ jest.mock('fs', () => ({
14
+ existsSync: jest.fn(),
15
+ readFileSync: jest.fn(),
16
+ mkdirSync: jest.fn(),
17
+ writeFileSync: jest.fn(),
18
+ readdirSync: jest.fn(),
19
+ }));
20
+
21
+ // Mock execAsync(getChangedFiles 失败 → 空变更列表)
22
+ jest.mock('../../../utils/exec', () => ({
23
+ execAsync: jest.fn(),
24
+ }));
25
+
26
+ jest.mock('child_process', () => ({
27
+ exec: jest.fn(),
28
+ execSync: jest.fn(() => Buffer.from('')),
29
+ }));
30
+
31
+ // Mock constraintChecker
32
+ jest.mock('../../../core/constraints/checker', () => ({
33
+ constraintChecker: {
34
+ setCustomConfig: jest.fn(),
35
+ setTraceRecorder: jest.fn(),
36
+ checkConstraints: jest.fn(),
37
+ },
38
+ }));
39
+
40
+ // Mock ProjectConfigLoader
41
+ jest.mock('../../../core/project-config-loader', () => ({
42
+ ProjectConfigLoader: jest.fn().mockImplementation(() => ({
43
+ load: jest.fn(),
44
+ hasCustomConfig: jest.fn().mockReturnValue(false),
45
+ mergeConstraints: jest.fn().mockReturnValue({ custom: [], disabled: [] }),
46
+ })),
47
+ }));
48
+
49
+ // Mock chalk
50
+ jest.mock('chalk', () => ({
51
+ blue: jest.fn((str: string) => str),
52
+ yellow: jest.fn((str: string) => str),
53
+ green: jest.fn((str: string) => str),
54
+ gray: jest.fn((str: string) => str),
55
+ red: jest.fn((str: string) => str),
56
+ }));
57
+
58
+ const mockChecker = constraintChecker as jest.Mocked<typeof constraintChecker>;
59
+
60
+ function fakeConstraint(id: string, level: 'iron_law' | 'guideline') {
61
+ return {
62
+ kind: 'check' as const,
63
+ id,
64
+ rule: 'test',
65
+ message: `msg-${id}`,
66
+ level,
67
+ trigger: 'code_implementation',
68
+ enforcement: 'test',
69
+ };
70
+ }
71
+
72
+ describe('check 命令 skip 输出', () => {
73
+ let consoleSpy: jest.SpyInstance;
74
+
75
+ beforeEach(() => {
76
+ jest.clearAllMocks();
77
+ consoleSpy = jest.spyOn(console, 'log').mockImplementation();
78
+ process.exitCode = 0;
79
+ });
80
+
81
+ afterEach(() => {
82
+ consoleSpy.mockRestore();
83
+ process.exitCode = 0;
84
+ });
85
+
86
+ it('skipped 单独列示,不计入铁律通过条数', async () => {
87
+ mockChecker.checkConstraints.mockResolvedValue({
88
+ passed: true,
89
+ ironLaws: [
90
+ { id: 'law_pass', level: 'iron_law', satisfied: true, checkedAt: new Date(), constraint: fakeConstraint('law_pass', 'iron_law') },
91
+ { id: 'law_skip', level: 'iron_law', satisfied: true, skipped: true, checkedAt: new Date(), constraint: fakeConstraint('law_skip', 'iron_law') },
92
+ ],
93
+ guidelines: [],
94
+ tips: [],
95
+ warningCount: 0,
96
+ tipCount: 0,
97
+ });
98
+
99
+ await check({ preset: 'default', staged: false });
100
+
101
+ // 通过条数只计实际评估的 1 条
102
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('铁律: 全部通过 (1 条)'));
103
+ // skipped 单独列示
104
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('跳过评估: 1 条'));
105
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('- law_skip'));
106
+ // skip 不影响整体通过
107
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('约束检查通过'));
108
+ });
109
+
110
+ it('指导原则通过计数排除 skipped', async () => {
111
+ mockChecker.checkConstraints.mockResolvedValue({
112
+ passed: true,
113
+ ironLaws: [],
114
+ guidelines: [
115
+ { id: 'g_pass', level: 'guideline', satisfied: true, checkedAt: new Date(), constraint: fakeConstraint('g_pass', 'guideline') },
116
+ { id: 'g_skip', level: 'guideline', satisfied: true, skipped: true, checkedAt: new Date(), constraint: fakeConstraint('g_skip', 'guideline') },
117
+ ],
118
+ tips: [],
119
+ warningCount: 0,
120
+ tipCount: 0,
121
+ });
122
+
123
+ await check({ preset: 'default', staged: false });
124
+
125
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('指导原则: 1/1 通过'));
126
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('跳过评估: 1 条'));
127
+ });
128
+ });
@@ -6,7 +6,6 @@ import { check, listLaws } from '../check';
6
6
  import * as fs from 'fs';
7
7
  import { constraintChecker } from '../../../core/constraints/checker';
8
8
  import { ProjectConfigLoader } from '../../../core/project-config-loader';
9
- import { IRON_LAWS, GUIDELINES, TIPS } from '../../../core/constraints/definitions';
10
9
  import { execAsync } from '../../../utils/exec';
11
10
 
12
11
  // Mock fs
@@ -88,7 +87,7 @@ describe('check command', () => {
88
87
  it('应该通过所有约束检查', async () => {
89
88
  mockChecker.checkConstraints.mockResolvedValue({
90
89
  passed: true,
91
- ironLaws: [{ id: 'test', level: 'iron_law', satisfied: true, checkedAt: new Date(), constraint: { id: 'test', rule: 'test', message: 'test', level: 'iron_law', trigger: 'code_implementation', enforcement: 'checkpoint-required' } }],
90
+ ironLaws: [{ id: 'test', level: 'iron_law', satisfied: true, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'test', rule: 'test', message: 'test', level: 'iron_law', trigger: 'code_implementation', enforcement: 'checkpoint-required' } }],
92
91
  guidelines: [],
93
92
  tips: [],
94
93
  warningCount: 0,
@@ -102,7 +101,7 @@ describe('check command', () => {
102
101
  it('应该显示铁律违规', async () => {
103
102
  mockChecker.checkConstraints.mockResolvedValue({
104
103
  passed: false,
105
- ironLaws: [{ id: 'no_bypass_checkpoint', level: 'iron_law', satisfied: false, checkedAt: new Date(), constraint: { id: 'no_bypass_checkpoint', rule: 'test', message: 'test', level: 'iron_law', trigger: 'code_implementation', enforcement: 'checkpoint-required' } }],
104
+ ironLaws: [{ id: 'no_bypass_checkpoint', level: 'iron_law', satisfied: false, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'no_bypass_checkpoint', rule: 'test', message: 'test', level: 'iron_law', trigger: 'code_implementation', enforcement: 'checkpoint-required' } }],
106
105
  guidelines: [],
107
106
  tips: [],
108
107
  warningCount: 0,
@@ -120,7 +119,7 @@ describe('check command', () => {
120
119
  mockChecker.checkConstraints.mockResolvedValue({
121
120
  passed: false,
122
121
  ironLaws: [],
123
- guidelines: [{ id: 'test_guideline', level: 'guideline', satisfied: false, checkedAt: new Date(), constraint: { id: 'test_guideline', rule: 'test', message: 'test', level: 'guideline', trigger: 'code_implementation', enforcement: 'warning' } }],
122
+ guidelines: [{ id: 'test_guideline', level: 'guideline', satisfied: false, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'test_guideline', rule: 'test', message: 'test', level: 'guideline', trigger: 'code_implementation', enforcement: 'warning' } }],
124
123
  tips: [],
125
124
  warningCount: 1,
126
125
  tipCount: 0,
@@ -157,7 +156,7 @@ describe('check command', () => {
157
156
  passed: true,
158
157
  ironLaws: [],
159
158
  guidelines: [],
160
- tips: [{ id: 'test_tip', level: 'tip', satisfied: false, checkedAt: new Date(), constraint: { id: 'test_tip', rule: 'test', message: 'test tip', level: 'tip', trigger: 'code_implementation', enforcement: 'info' } }],
159
+ tips: [{ id: 'test_tip', level: 'tip', satisfied: false, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'test_tip', rule: 'test', message: 'test tip', level: 'tip', trigger: 'code_implementation', enforcement: 'info' } }],
161
160
  warningCount: 0,
162
161
  tipCount: 1,
163
162
  });
@@ -170,7 +169,7 @@ describe('check command', () => {
170
169
  mockChecker.checkConstraints.mockResolvedValue({
171
170
  passed: true,
172
171
  ironLaws: [],
173
- guidelines: [{ id: 'test_guideline', level: 'guideline', satisfied: true, checkedAt: new Date(), constraint: { id: 'test_guideline', rule: 'test', message: 'test', level: 'guideline', trigger: 'code_implementation', enforcement: 'warning' } }],
172
+ guidelines: [{ id: 'test_guideline', level: 'guideline', satisfied: true, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'test_guideline', rule: 'test', message: 'test', level: 'guideline', trigger: 'code_implementation', enforcement: 'warning' } }],
174
173
  tips: [],
175
174
  warningCount: 0,
176
175
  tipCount: 0,
@@ -352,6 +351,7 @@ describe('check command', () => {
352
351
  satisfied: false,
353
352
  checkedAt: new Date(),
354
353
  constraint: {
354
+ kind: 'check' as const,
355
355
  id: 'no_bypass_checkpoint',
356
356
  rule: 'NO BYPASSING CHECKPOINTS',
357
357
  message: '禁止跳过检查点验证',
@@ -384,6 +384,7 @@ describe('check command', () => {
384
384
  satisfied: false,
385
385
  checkedAt: new Date(),
386
386
  constraint: {
387
+ kind: 'check' as const,
387
388
  id: 'prefer_composition',
388
389
  rule: 'PREFER COMPOSITION OVER INHERITANCE',
389
390
  message: '优先使用组合而非继承',
@@ -413,6 +414,7 @@ describe('check command', () => {
413
414
  satisfied: false,
414
415
  checkedAt: new Date(),
415
416
  constraint: {
417
+ kind: 'check' as const,
416
418
  id: 'consider_reuse',
417
419
  rule: 'CONSIDER REUSE',
418
420
  message: '考虑复用',
@@ -455,7 +457,7 @@ describe('check command', () => {
455
457
  it('应该在 trace 数达到 50 时提示查看统计', async () => {
456
458
  const traces = Array(50).fill('{"result":"pass"}').join('\n');
457
459
  mockFs.existsSync.mockImplementation((p: any) => {
458
- if (p.includes('execution.log')) return true;
460
+ if (p.includes('traces.log')) return true;
459
461
  if (p.includes('.state.json')) return false;
460
462
  return false;
461
463
  });
@@ -465,19 +467,6 @@ describe('check command', () => {
465
467
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('记录已足够'));
466
468
  });
467
469
 
468
- it('应该在 trace 数达到 100 且无诊断历史时提示运行 flow', async () => {
469
- const traces = Array(100).fill('{"result":"pass"}').join('\n');
470
- mockFs.existsSync.mockImplementation((p: any) => {
471
- if (p.includes('execution.log')) return true;
472
- if (p.includes('.state.json')) return false;
473
- return false;
474
- });
475
- mockFs.readFileSync.mockReturnValue(traces);
476
-
477
- await check({ preset: 'default', staged: false });
478
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('建议运行 harness flow'));
479
- });
480
-
481
470
  it('应该在绕过率高时提示查看详情', async () => {
482
471
  // 20 条记录,15 条 bypassed (75% > 30%)
483
472
  const traces = [
@@ -485,7 +474,7 @@ describe('check command', () => {
485
474
  ...Array(15).fill('{"result":"bypassed"}'),
486
475
  ].join('\n');
487
476
  mockFs.existsSync.mockImplementation((p: any) => {
488
- if (p.includes('execution.log')) return true;
477
+ if (p.includes('traces.log')) return true;
489
478
  if (p.includes('.state.json')) return false;
490
479
  return false;
491
480
  });
@@ -513,7 +502,7 @@ describe('check command', () => {
513
502
  it('应该在 trace 数少于 50 时不提示', async () => {
514
503
  const traces = Array(10).fill('{"result":"pass"}').join('\n');
515
504
  mockFs.existsSync.mockImplementation((p: any) => {
516
- if (p.includes('execution.log')) return true;
505
+ if (p.includes('traces.log')) return true;
517
506
  if (p.includes('.state.json')) return false;
518
507
  return false;
519
508
  });
@@ -531,7 +520,7 @@ describe('check command', () => {
531
520
  ...Array(2).fill('{"result":"bypassed"}'),
532
521
  ].join('\n');
533
522
  mockFs.existsSync.mockImplementation((p: any) => {
534
- if (p.includes('execution.log')) return true;
523
+ if (p.includes('traces.log')) return true;
535
524
  if (p.includes('.state.json')) return false;
536
525
  return false;
537
526
  });
@@ -550,7 +539,7 @@ describe('check command', () => {
550
539
  ...Array(10).fill('{"result":"bypassed"}'),
551
540
  ].join('\n');
552
541
  mockFs.existsSync.mockImplementation((p: any) => {
553
- if (p.includes('execution.log')) return true;
542
+ if (p.includes('traces.log')) return true;
554
543
  if (p.includes('.state.json')) return false;
555
544
  return false;
556
545
  });
@@ -564,7 +553,7 @@ describe('check command', () => {
564
553
  it('应该保存状态文件当有提示时', async () => {
565
554
  const traces = Array(50).fill('{"result":"pass"}').join('\n');
566
555
  mockFs.existsSync.mockImplementation((p: any) => {
567
- if (p.includes('execution.log')) return true;
556
+ if (p.includes('traces.log')) return true;
568
557
  if (p.includes('.state.json')) return false;
569
558
  return false;
570
559
  });
@@ -0,0 +1,327 @@
1
+ /**
2
+ * harness constraints report 测试(ADR-0001 P5)
3
+ *
4
+ * 覆盖:无 trace 文件、空 trace、含 skip 的统计、四类候选诊断、
5
+ * --export 内容脱敏(无 projectPath)、unknownIds 配置健康提示。
6
+ *
7
+ * 使用真实临时目录(getEffectiveConstraints/ProjectConfigLoader 读真实 fs)。
8
+ */
9
+
10
+ import * as fs from 'fs';
11
+ import * as os from 'os';
12
+ import * as path from 'path';
13
+ import type { ExecutionTrace } from '../../../types/trace';
14
+ import {
15
+ buildConstraintsUsageReport,
16
+ diagnoseRetireCandidates,
17
+ readProjectTraces,
18
+ } from '../../../core/constraints/usage-report';
19
+ import { constraintsReport, renderExportMarkdown } from '../constraints-report';
20
+
21
+ function makeTmpProject(): string {
22
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'harness-report-test-'));
23
+ }
24
+
25
+ function writeTraces(root: string, traces: Partial<ExecutionTrace>[]): void {
26
+ const dir = path.join(root, '.harness', 'logs');
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ const lines = traces.map(t =>
29
+ JSON.stringify({
30
+ level: 'guideline',
31
+ timestamp: 1700000000000,
32
+ result: 'pass',
33
+ ...t,
34
+ })
35
+ );
36
+ fs.writeFileSync(path.join(dir, 'traces.log'), lines.join('\n') + '\n', 'utf-8');
37
+ }
38
+
39
+ /** 生成 N 条同结果 trace */
40
+ function tracesOf(id: string, result: ExecutionTrace['result'], n: number, startTs = 1700000000000): Partial<ExecutionTrace>[] {
41
+ return Array.from({ length: n }, (_, i) => ({ constraintId: id, result, timestamp: startTs + i * 1000 }));
42
+ }
43
+
44
+ let logSpy: jest.SpyInstance;
45
+
46
+ beforeEach(() => {
47
+ logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
48
+ });
49
+
50
+ afterEach(() => {
51
+ logSpy.mockRestore();
52
+ });
53
+
54
+ describe('buildConstraintsUsageReport', () => {
55
+ it('无 trace 文件:生效集 check 约束全部列出且 total=0,全部判为零触发候选', () => {
56
+ const root = makeTmpProject();
57
+ const report = buildConstraintsUsageReport(root);
58
+
59
+ expect(report.traceFileExists).toBe(false);
60
+ expect(report.stats.length).toBe(9); // 5 iron + 4 guideline
61
+ for (const s of report.stats) {
62
+ expect(s.total).toBe(0);
63
+ expect(s.evaluated).toBe(0);
64
+ expect(s.failRate).toBe(0);
65
+ expect(s.firstAt).toBeUndefined();
66
+ }
67
+ expect(report.candidates.length).toBe(9);
68
+ expect(report.candidates.every(c => c.kind === 'zero_trigger')).toBe(true);
69
+ // prompt 注入清单(standard preset、无 scenes → 14 条通用 prompt)
70
+ expect(report.activePromptIds.length).toBe(14);
71
+ expect(report.activePromptIds).toContain('no_fuzzy_completion_claim');
72
+ });
73
+
74
+ it('空 trace 文件:与无文件一致,但 traceFileExists=true', () => {
75
+ const root = makeTmpProject();
76
+ writeTraces(root, []);
77
+ const report = buildConstraintsUsageReport(root);
78
+
79
+ expect(report.traceFileExists).toBe(true);
80
+ expect(report.stats.every(s => s.total === 0)).toBe(true);
81
+ expect(report.candidates.every(c => c.kind === 'zero_trigger')).toBe(true);
82
+ });
83
+
84
+ it('含 skip 的统计:skip 不计入 fail 率分母,首次/最近时间正确', () => {
85
+ const root = makeTmpProject();
86
+ writeTraces(root, [
87
+ ...tracesOf('no_hardcoded_credentials', 'pass', 3, 1700000000000),
88
+ ...tracesOf('no_hardcoded_credentials', 'fail', 1, 1700000100000),
89
+ ...tracesOf('no_hardcoded_credentials', 'skip', 2, 1700000200000),
90
+ ]);
91
+ const report = buildConstraintsUsageReport(root);
92
+ const s = report.stats.find(x => x.id === 'no_hardcoded_credentials')!;
93
+
94
+ expect(s.total).toBe(6);
95
+ expect(s.pass).toBe(3);
96
+ expect(s.fail).toBe(1);
97
+ expect(s.skip).toBe(2);
98
+ expect(s.evaluated).toBe(4);
99
+ expect(s.failRate).toBeCloseTo(0.25);
100
+ expect(s.firstAt).toBe(1700000000000);
101
+ expect(s.lastAt).toBe(1700000201000);
102
+ // 4 次评估 1 次 fail:不构成任何候选
103
+ expect(report.candidates.find(c => c.id === 'no_hardcoded_credentials')).toBeUndefined();
104
+ });
105
+
106
+ it('坏行容错:单行 JSON 损坏不影响其他行统计', () => {
107
+ const root = makeTmpProject();
108
+ const dir = path.join(root, '.harness', 'logs');
109
+ fs.mkdirSync(dir, { recursive: true });
110
+ fs.writeFileSync(
111
+ path.join(dir, 'traces.log'),
112
+ '{"constraintId":"docs_freshness","timestamp":1,"result":"pass","level":"iron_law"}\n{bad json\n',
113
+ 'utf-8'
114
+ );
115
+ expect(readProjectTraces(root).length).toBe(1);
116
+ });
117
+
118
+ it('四类候选诊断:零触发/不可评估(flag)/不可评估(探测)/高噪/零拦截', () => {
119
+ const root = makeTmpProject();
120
+ writeTraces(root, [
121
+ // 不可评估(flag 未接线):全部 skip
122
+ ...tracesOf('no_completion_without_verification', 'skip', 3),
123
+ // 不可评估(存在性探测未命中):全部 skip
124
+ ...tracesOf('capability_sync', 'skip', 5),
125
+ // 高噪:24 次评估 fail 率 83% > 80%
126
+ ...tracesOf('no_bypass_checkpoint', 'fail', 20),
127
+ ...tracesOf('no_bypass_checkpoint', 'pass', 4, 1700001000000),
128
+ // 零拦截:50 次评估 0 fail
129
+ ...tracesOf('no_hardcoded_credentials', 'pass', 50, 1700010000000),
130
+ ]);
131
+ const report = buildConstraintsUsageReport(root);
132
+ const byId = new Map(report.candidates.map(c => [c.id, c]));
133
+
134
+ // 零触发(未出现在 trace 的 check 约束)
135
+ expect(byId.get('docs_freshness')?.kind).toBe('zero_trigger');
136
+
137
+ // 不可评估 · flag 未接线
138
+ const flagC = byId.get('no_completion_without_verification');
139
+ expect(flagC?.kind).toBe('unevaluable');
140
+ expect(flagC?.reason).toContain('证据 flag 未接线');
141
+
142
+ // 不可评估 · 约定未采用
143
+ const probeC = byId.get('capability_sync');
144
+ expect(probeC?.kind).toBe('unevaluable');
145
+ expect(probeC?.reason).toContain('约定未采用');
146
+
147
+ // 高噪
148
+ const noiseC = byId.get('no_bypass_checkpoint');
149
+ expect(noiseC?.kind).toBe('high_noise');
150
+ expect(noiseC?.reason).toContain('83%');
151
+
152
+ // 零拦截
153
+ const zeroC = byId.get('no_hardcoded_credentials');
154
+ expect(zeroC?.kind).toBe('zero_intercept');
155
+ expect(zeroC?.reason).toContain('50 次评估');
156
+ });
157
+
158
+ it('阈值可配:放宽零拦截样本阈值后 10 次全 pass 也成为候选', () => {
159
+ const root = makeTmpProject();
160
+ writeTraces(root, tracesOf('no_hardcoded_credentials', 'pass', 10));
161
+
162
+ const strict = buildConstraintsUsageReport(root);
163
+ expect(strict.candidates.find(c => c.id === 'no_hardcoded_credentials')).toBeUndefined();
164
+
165
+ const relaxed = buildConstraintsUsageReport(root, { zeroInterceptMinEvaluated: 10 });
166
+ expect(relaxed.candidates.find(c => c.id === 'no_hardcoded_credentials')?.kind).toBe('zero_intercept');
167
+ });
168
+
169
+ it('diagnoseRetireCandidates 优先级:全 skip 不重复计入零拦截', () => {
170
+ const candidates = diagnoseRetireCandidates([
171
+ {
172
+ id: 'x', level: 'guideline',
173
+ total: 60, pass: 0, fail: 0, skip: 60, bypassed: 0,
174
+ evaluated: 0, failRate: 0,
175
+ },
176
+ ]);
177
+ expect(candidates.length).toBe(1);
178
+ expect(candidates[0].kind).toBe('unevaluable');
179
+ });
180
+
181
+ it('配置健康:config.yml 中未知 id 进入 unknownIds', () => {
182
+ const root = makeTmpProject();
183
+ fs.mkdirSync(path.join(root, '.harness'), { recursive: true });
184
+ fs.writeFileSync(
185
+ path.join(root, '.harness', 'config.yml'),
186
+ 'constraints:\n ghost_constraint:\n enabled: false\n',
187
+ 'utf-8'
188
+ );
189
+ const report = buildConstraintsUsageReport(root);
190
+ expect(report.lint.unknownIds).toContain('ghost_constraint');
191
+ });
192
+ });
193
+
194
+ describe('constraintsReport CLI', () => {
195
+ it('console 输出包含统计表、候选、注入清单、unknownIds 提示', async () => {
196
+ const root = makeTmpProject();
197
+ fs.mkdirSync(path.join(root, '.harness'), { recursive: true });
198
+ fs.writeFileSync(
199
+ path.join(root, '.harness', 'config.yml'),
200
+ 'constraints:\n ghost_constraint:\n enabled: false\n',
201
+ 'utf-8'
202
+ );
203
+ writeTraces(root, tracesOf('no_hardcoded_credentials', 'pass', 3));
204
+
205
+ await constraintsReport({ projectPath: root });
206
+
207
+ const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
208
+ expect(output).toContain('约束使用报告');
209
+ expect(output).toContain('no_hardcoded_credentials');
210
+ expect(output).toContain('total=3');
211
+ expect(output).toContain('退役候选');
212
+ expect(output).toContain('当前生效 prompt 注入');
213
+ expect(output).toContain('ghost_constraint');
214
+ });
215
+
216
+ it('--export 缺省路径:写入 .harness/reports/constraints-<YYYYMMDD>.md 且内容脱敏', async () => {
217
+ const root = makeTmpProject();
218
+ writeTraces(root, [
219
+ ...tracesOf('no_bypass_checkpoint', 'fail', 20),
220
+ // projectPath 字段进 trace,但不得进 export
221
+ { constraintId: 'no_bypass_checkpoint', result: 'fail', projectPath: root, timestamp: 1700001000000 },
222
+ ]);
223
+
224
+ await constraintsReport({ projectPath: root, export: true });
225
+
226
+ const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
227
+ const exportPath = path.join(root, '.harness', 'reports', `constraints-${date}.md`);
228
+ expect(fs.existsSync(exportPath)).toBe(true);
229
+
230
+ const content = fs.readFileSync(exportPath, 'utf-8');
231
+ expect(content).toContain('harness 版本');
232
+ expect(content).toContain('| id | total | pass | fail | skip |');
233
+ expect(content).toContain('no_bypass_checkpoint');
234
+ expect(content).toContain('高噪');
235
+ // 脱敏:不包含项目路径
236
+ expect(content).not.toContain(root);
237
+ expect(content).not.toContain('projectPath');
238
+
239
+ // 打印了导出路径
240
+ const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
241
+ expect(output).toContain(exportPath);
242
+ });
243
+
244
+ it('--export 指定文件:写到给定路径', async () => {
245
+ const root = makeTmpProject();
246
+ await constraintsReport({ projectPath: root, export: 'my-report.md' });
247
+ expect(fs.existsSync(path.join(root, 'my-report.md'))).toBe(true);
248
+ });
249
+
250
+ it('renderExportMarkdown:无候选时显式标注', () => {
251
+ const root = makeTmpProject();
252
+ writeTraces(root, [
253
+ // 所有 check 约束给少量健康数据(低于一切阈值)
254
+ ...buildConstraintsUsageReport(root).stats.flatMap(s => tracesOf(s.id, 'pass', 5)),
255
+ ]);
256
+ const report = buildConstraintsUsageReport(root);
257
+ expect(report.candidates.length).toBe(0);
258
+ const md = renderExportMarkdown(report, '0.0.0-test', new Date(1700000000000));
259
+ expect(md).toContain('(无候选)');
260
+ expect(md).toContain('0.0.0-test');
261
+ expect(md).toContain('2023-11-14');
262
+ });
263
+ });
264
+
265
+ describe('constraintsReport 注入漂移小节(ADR-0001 决策 7)', () => {
266
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
267
+ const realVersion = require('../../../../package.json').version as string;
268
+
269
+ const writeSyncedClaudeMd = (root: string, version = realVersion) => {
270
+ const { renderConstraintsSection } = require('../../../core/constraints/injection-renderer');
271
+ const { getEffectiveConstraints } = require('../../../core/effective-constraints');
272
+ const section = '## Governance Rules\n' + renderConstraintsSection(getEffectiveConstraints(root), version);
273
+ fs.writeFileSync(path.join(root, 'CLAUDE.md'), `# Test Project\n\n${section}`, 'utf-8');
274
+ };
275
+
276
+ it('有漂移:输出条目级差异(缺失/多余)与修复指引', async () => {
277
+ const root = makeTmpProject();
278
+ writeSyncedClaudeMd(root, '0.0.1-old');
279
+ const claudeMdPath = path.join(root, 'CLAUDE.md');
280
+ const content = fs.readFileSync(claudeMdPath, 'utf-8');
281
+ const originalLine = content.split('\n').find((l: string) => l.startsWith('- **'))!;
282
+ const editedLine = originalLine.replace(/: .+$/, ': 手工篡改');
283
+ fs.writeFileSync(claudeMdPath, content.replace(originalLine, editedLine), 'utf-8');
284
+
285
+ await constraintsReport({ projectPath: root });
286
+
287
+ const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
288
+ expect(output).toContain('注入漂移');
289
+ expect(output).toContain('版本漂移');
290
+ expect(output).toContain('0.0.1-old');
291
+ expect(output).toContain(`缺失: ${originalLine}`);
292
+ expect(output).toContain(`多余: ${editedLine}`);
293
+ expect(output).toContain('npx harness init');
294
+ });
295
+
296
+ it('重复章节:输出重复章节提示', async () => {
297
+ const root = makeTmpProject();
298
+ writeSyncedClaudeMd(root);
299
+ fs.appendFileSync(path.join(root, 'CLAUDE.md'), '\n## Governance Rules\n\n旧版遗留\n', 'utf-8');
300
+
301
+ await constraintsReport({ projectPath: root });
302
+
303
+ const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
304
+ expect(output).toContain('重复章节');
305
+ });
306
+
307
+ it('无漂移:小节显示无漂移', async () => {
308
+ const root = makeTmpProject();
309
+ writeSyncedClaudeMd(root);
310
+
311
+ await constraintsReport({ projectPath: root });
312
+
313
+ const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
314
+ expect(output).toContain('注入漂移');
315
+ expect(output).toContain('无漂移');
316
+ });
317
+
318
+ it('未注入(无标记段):一句话提示,不算漂移', async () => {
319
+ const root = makeTmpProject();
320
+ fs.writeFileSync(path.join(root, 'CLAUDE.md'), '# Test Project\n', 'utf-8');
321
+
322
+ await constraintsReport({ projectPath: root });
323
+
324
+ const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
325
+ expect(output).toContain('未注入');
326
+ });
327
+ });