@dommaker/harness 0.16.9 → 0.17.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 (337) hide show
  1. package/CHANGELOG.md +34 -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 +54 -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 +18 -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 +4 -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 +58 -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 +21 -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,283 @@
1
+ /**
2
+ * harness constraints retire 测试(ADR-0001 P5)
3
+ *
4
+ * 执行逻辑(retireConstraint,纯函数化):config.yml 写入形态、
5
+ * KnowledgeStore 记录、CLAUDE.md 注入段同步、重复 retire / 未知 id 保护。
6
+ * 交互流程(runRetireInteractive):注入 stdin 流测核心分支
7
+ * (候选选择 + iron 二次确认拒绝;无候选手动输入 + 确认执行)。
8
+ *
9
+ * 使用真实临时目录。
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+ import * as os from 'os';
14
+ import * as path from 'path';
15
+ import { PassThrough, Writable } from 'stream';
16
+ import * as yaml from 'js-yaml';
17
+ import type { ExecutionTrace } from '../../../types/trace';
18
+ import { getConstraint } from '../../../core/constraints/definitions';
19
+ import { getEffectiveConstraints } from '../../../core/effective-constraints';
20
+ import { renderConstraintsSection } from '../../../core/constraints/injection-renderer';
21
+ import { FileKnowledgeStore } from '../../../knowledge/store';
22
+ import { retireConstraint, constraintsRetire, runRetireInteractive, printRetireResult } from '../constraints-retire';
23
+
24
+ const FIXED_NOW = new Date('2026-08-08T12:00:00.000Z');
25
+
26
+ function makeTmpProject(): string {
27
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'harness-retire-test-'));
28
+ }
29
+
30
+ function writeTraces(root: string, traces: Partial<ExecutionTrace>[]): void {
31
+ const dir = path.join(root, '.harness', 'logs');
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ const lines = traces.map(t =>
34
+ JSON.stringify({ level: 'guideline', timestamp: 1700000000000, result: 'pass', ...t })
35
+ );
36
+ fs.writeFileSync(path.join(dir, 'traces.log'), lines.join('\n') + '\n', 'utf-8');
37
+ }
38
+
39
+ function readConfig(root: string): any {
40
+ return yaml.load(fs.readFileSync(path.join(root, '.harness', 'config.yml'), 'utf-8'));
41
+ }
42
+
43
+ let logSpy: jest.SpyInstance;
44
+
45
+ beforeEach(() => {
46
+ logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
47
+ });
48
+
49
+ afterEach(() => {
50
+ logSpy.mockRestore();
51
+ });
52
+
53
+ describe('retireConstraint 执行逻辑', () => {
54
+ it('config.yml 写入形态:enabled:false + retired 元数据(at/reason/stats)', () => {
55
+ const root = makeTmpProject();
56
+ writeTraces(root, [
57
+ { constraintId: 'no_hardcoded_credentials', result: 'pass' },
58
+ { constraintId: 'no_hardcoded_credentials', result: 'pass', timestamp: 1700000001000 },
59
+ { constraintId: 'no_hardcoded_credentials', result: 'fail', timestamp: 1700000002000 },
60
+ ]);
61
+
62
+ const result = retireConstraint(root, 'no_hardcoded_credentials', {
63
+ reason: '由 secret 扫描工具链覆盖',
64
+ now: FIXED_NOW,
65
+ });
66
+
67
+ expect(result.status).toBe('retired');
68
+ expect(result.isIronLaw).toBe(false);
69
+ expect(result.stats).toEqual({ total: 3, fail: 1, failRate: 1 / 3 });
70
+
71
+ const config = readConfig(root);
72
+ const entry = config.constraints.no_hardcoded_credentials;
73
+ expect(entry.enabled).toBe(false);
74
+ expect(entry.retired.at).toBe(FIXED_NOW.toISOString());
75
+ expect(entry.retired.reason).toBe('由 secret 扫描工具链覆盖');
76
+ expect(entry.retired.stats.total).toBe(3);
77
+ expect(entry.retired.stats.fail).toBe(1);
78
+ expect(entry.retired.stats.failRate).toBeCloseTo(1 / 3);
79
+ });
80
+
81
+ it('config.yml 不存在时创建;已有其他配置时保留', () => {
82
+ const root = makeTmpProject();
83
+ fs.mkdirSync(path.join(root, '.harness'), { recursive: true });
84
+ fs.writeFileSync(path.join(root, '.harness', 'config.yml'), 'preset: strict\nscenes:\n - llm-app\n', 'utf-8');
85
+
86
+ const result = retireConstraint(root, 'capability_sync', { now: FIXED_NOW });
87
+ expect(result.status).toBe('retired');
88
+
89
+ const config = readConfig(root);
90
+ expect(config.preset).toBe('strict');
91
+ expect(config.scenes).toEqual(['llm-app']);
92
+ expect(config.constraints.capability_sync.enabled).toBe(false);
93
+ expect(config.constraints.capability_sync.retired.reason).toBe('');
94
+ });
95
+
96
+ it('KnowledgeStore 写入退役记录:规则原文 + 原因 + 统计 + signal 模式', () => {
97
+ const root = makeTmpProject();
98
+ writeTraces(root, [{ constraintId: 'no_bypass_checkpoint', result: 'fail' }]);
99
+
100
+ const result = retireConstraint(root, 'no_bypass_checkpoint', { reason: '流程已内置门禁', now: FIXED_NOW });
101
+ expect(result.status).toBe('retired');
102
+ expect(result.knowledgeEntryId).toBe('constraint-retired-no_bypass_checkpoint');
103
+
104
+ const store = new FileKnowledgeStore({ baseDir: path.join(root, '.harness', 'knowledge') });
105
+ const entry = store.get('constraint-retired-no_bypass_checkpoint');
106
+ expect(entry).toBeDefined();
107
+ expect(entry!.consumptionMode).toBe('signal');
108
+ expect(entry!.origin).toBe('human');
109
+ expect(entry!.tags).toContain('constraint-retired');
110
+ expect(entry!.tags).toContain('constraint:no_bypass_checkpoint');
111
+ expect(entry!.title).toContain('no_bypass_checkpoint');
112
+
113
+ const def = getConstraint('no_bypass_checkpoint')!;
114
+ expect(entry!.content).toContain(def.description!);
115
+ expect(entry!.content).toContain('流程已内置门禁');
116
+ expect(entry!.content).toContain('total: 1');
117
+ expect(entry!.content).toContain('failRate: 100%');
118
+ expect(entry!.content).toContain(FIXED_NOW.toISOString());
119
+ });
120
+
121
+ it('CLAUDE.md 含标记段时同步重渲染(退役条目消失、标记保留)', () => {
122
+ const root = makeTmpProject();
123
+ // 先用当前生效集渲染一个合法注入段
124
+ const before = renderConstraintsSection(getEffectiveConstraints(root), '0.0.0-test');
125
+ expect(before).toContain('no_bypass_checkpoint');
126
+ fs.writeFileSync(path.join(root, 'CLAUDE.md'), `# 项目\n\n## Governance Rules\n${before}\n其他内容\n`, 'utf-8');
127
+
128
+ const result = retireConstraint(root, 'no_bypass_checkpoint', { now: FIXED_NOW });
129
+ expect(result.status).toBe('retired');
130
+ expect(result.claudeMdSynced).toBe(true);
131
+
132
+ const after = fs.readFileSync(path.join(root, 'CLAUDE.md'), 'utf-8');
133
+ expect(after).toContain('HARNESS_CONSTRAINTS_START');
134
+ expect(after).toContain('HARNESS_CONSTRAINTS_END');
135
+ expect(after).not.toContain('**no_bypass_checkpoint**');
136
+ expect(after).toContain('其他内容');
137
+
138
+ printRetireResult(result);
139
+ const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
140
+ expect(output).toContain('已同步 CLAUDE.md 注入段');
141
+ expect(output).toContain('恢复方法');
142
+ });
143
+
144
+ it('CLAUDE.md 无标记段或不存在时不创建、不同步', () => {
145
+ const root = makeTmpProject();
146
+ fs.writeFileSync(path.join(root, 'CLAUDE.md'), '# 用户自写\n', 'utf-8');
147
+ const result = retireConstraint(root, 'capability_sync', { now: FIXED_NOW });
148
+ expect(result.status).toBe('retired');
149
+ expect(result.claudeMdSynced).toBe(false);
150
+ expect(fs.readFileSync(path.join(root, 'CLAUDE.md'), 'utf-8')).toBe('# 用户自写\n');
151
+ });
152
+
153
+ it('重复 retire:already_retired,不覆盖原 retired 元数据', () => {
154
+ const root = makeTmpProject();
155
+ const first = retireConstraint(root, 'capability_sync', { reason: '第一次', now: FIXED_NOW });
156
+ expect(first.status).toBe('retired');
157
+
158
+ const later = new Date('2026-08-09T00:00:00.000Z');
159
+ const second = retireConstraint(root, 'capability_sync', { reason: '第二次', now: later });
160
+ expect(second.status).toBe('already_retired');
161
+
162
+ const config = readConfig(root);
163
+ expect(config.constraints.capability_sync.retired.at).toBe(FIXED_NOW.toISOString());
164
+ expect(config.constraints.capability_sync.retired.reason).toBe('第一次');
165
+ });
166
+
167
+ it('未知 id:unknown_id,不落盘任何文件', () => {
168
+ const root = makeTmpProject();
169
+ const result = retireConstraint(root, 'not_a_constraint', { now: FIXED_NOW });
170
+ expect(result.status).toBe('unknown_id');
171
+ expect(fs.existsSync(path.join(root, '.harness'))).toBe(false);
172
+ });
173
+
174
+ it('check 层 iron:isIronLaw 标记为 true(供交互模式二次确认)', () => {
175
+ const root = makeTmpProject();
176
+ const result = retireConstraint(root, 'docs_freshness', { now: FIXED_NOW });
177
+ expect(result.status).toBe('retired');
178
+ expect(result.isIronLaw).toBe(true);
179
+ });
180
+
181
+ it('退役后生效集不再包含该约束', () => {
182
+ const root = makeTmpProject();
183
+ expect(getEffectiveConstraints(root).some(c => c.id === 'capability_sync')).toBe(true);
184
+ retireConstraint(root, 'capability_sync', { now: FIXED_NOW });
185
+ expect(getEffectiveConstraints(root).some(c => c.id === 'capability_sync')).toBe(false);
186
+ });
187
+ });
188
+
189
+ describe('constraintsRetire 非交互直达', () => {
190
+ it('iron 直达退役时打印额外警示', async () => {
191
+ const root = makeTmpProject();
192
+ await constraintsRetire('docs_freshness', { projectPath: root, reason: '直接退役' });
193
+
194
+ const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
195
+ expect(output).toContain('Iron Law');
196
+ expect(output).toContain('已退役');
197
+
198
+ const config = readConfig(root);
199
+ expect(config.constraints.docs_freshness.enabled).toBe(false);
200
+ });
201
+
202
+ it('未知 id 直达:明确提示', async () => {
203
+ const root = makeTmpProject();
204
+ await constraintsRetire('ghost', { projectPath: root });
205
+ const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
206
+ expect(output).toContain('约束不存在');
207
+ });
208
+ });
209
+
210
+ describe('runRetireInteractive 交互流程(注入 IO 流)', () => {
211
+ /** 逐行 drip-feed,等待 readline 消费上一行 */
212
+ function makeIo(lines: string[]) {
213
+ const input = new PassThrough();
214
+ const output = new Writable({ write(_c, _e, cb) { cb(); } });
215
+ let i = 0;
216
+ const timer = setInterval(() => {
217
+ if (i < lines.length) {
218
+ input.write(lines[i] + '\n');
219
+ i++;
220
+ } else {
221
+ clearInterval(timer);
222
+ }
223
+ }, 30);
224
+ return { input, output, done: () => clearInterval(timer) };
225
+ }
226
+
227
+ it('候选编号选择 + iron 二次确认拒绝 → 不落盘', async () => {
228
+ const root = makeTmpProject();
229
+ // 无 trace → 候选全部为零触发,1 号候选是第一条 iron(no_completion_without_verification)
230
+ const io = makeIo(['1', 'n']); // 选 1 号 → iron 确认拒绝
231
+
232
+ await runRetireInteractive(root, io);
233
+ io.done();
234
+
235
+ const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
236
+ expect(output).toContain('退役候选');
237
+ expect(output).toContain('已跳过 no_completion_without_verification');
238
+ expect(output).toContain('无可执行项');
239
+ expect(fs.existsSync(path.join(root, '.harness', 'config.yml'))).toBe(false);
240
+ });
241
+
242
+ it('无候选 → 手动输入 id → 确认执行 → 落盘', async () => {
243
+ const root = makeTmpProject();
244
+ // 所有 check 约束给少量健康数据(低于一切候选阈值)
245
+ const healthy = getEffectiveConstraints(root)
246
+ .filter(c => c.kind === 'check')
247
+ .flatMap(c => [
248
+ { constraintId: c.id, result: 'pass' as const },
249
+ { constraintId: c.id, result: 'pass' as const, timestamp: 1700000001000 },
250
+ { constraintId: c.id, result: 'fail' as const, timestamp: 1700000002000 },
251
+ ]);
252
+ writeTraces(root, healthy);
253
+
254
+ const io = makeIo(['no_hardcoded_credentials', '误报太多', 'y']);
255
+ await runRetireInteractive(root, io);
256
+ io.done();
257
+
258
+ const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
259
+ expect(output).toContain('没有退役候选');
260
+ expect(output).toContain('已退役');
261
+
262
+ const config = readConfig(root);
263
+ expect(config.constraints.no_hardcoded_credentials.enabled).toBe(false);
264
+ expect(config.constraints.no_hardcoded_credentials.retired.reason).toBe('误报太多');
265
+ });
266
+
267
+ it('无候选 → 手动输入未知 id → 提示不存在并取消', async () => {
268
+ const root = makeTmpProject();
269
+ const healthy = getEffectiveConstraints(root)
270
+ .filter(c => c.kind === 'check')
271
+ .flatMap(c => [{ constraintId: c.id, result: 'pass' as const }]);
272
+ writeTraces(root, healthy);
273
+
274
+ const io = makeIo(['ghost_id']);
275
+ await runRetireInteractive(root, io);
276
+ io.done();
277
+
278
+ const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
279
+ expect(output).toContain('ghost_id');
280
+ expect(output).toContain('约束不存在');
281
+ expect(fs.existsSync(path.join(root, '.harness', 'config.yml'))).toBe(false);
282
+ });
283
+ });
@@ -0,0 +1,234 @@
1
+ /**
2
+ * init 注入重写测试(ADR-0001 P3)
3
+ *
4
+ * - renderConstraintsSection 纯函数
5
+ * - setupClaudeMdConstraints:消费生效集、幂等、尊重 config.yml 禁用与 scenes
6
+ * - setupClaudeMdOutputStyle:标记化三种情形(无段插入 / 旧 harness 段迁移 / 用户自有段跳过)
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import {
13
+ renderConstraintsSection,
14
+ CONSTRAINTS_START_MARKER,
15
+ CONSTRAINTS_END_MARKER,
16
+ } from '../../../core/constraints/injection-renderer';
17
+ import {
18
+ setupClaudeMdConstraints,
19
+ setupClaudeMdOutputStyle,
20
+ } from '../init';
21
+ import { getEffectiveConstraints } from '../../../core/effective-constraints';
22
+ import { getAllConstraints } from '../../../core/constraints/definitions';
23
+ import type { Constraint } from '../../../types/constraint';
24
+
25
+ const OUTPUT_STYLE_START = '<!-- HARNESS_OUTPUT_STYLE_START -->';
26
+ const OUTPUT_STYLE_END = '<!-- HARNESS_OUTPUT_STYLE_END -->';
27
+
28
+ const makeConstraint = (overrides: Partial<Constraint>): Constraint => ({
29
+ id: 'x',
30
+ kind: 'prompt',
31
+ level: 'prompt',
32
+ rule: 'R',
33
+ message: 'M',
34
+ trigger: 'manual',
35
+ enforcement: 'none',
36
+ ...overrides,
37
+ });
38
+
39
+ describe('renderConstraintsSection(纯函数)', () => {
40
+ it('按 level 分组渲染,含标记与版本行', () => {
41
+ const constraints = [
42
+ makeConstraint({ id: 'iron_a', kind: 'check', level: 'iron_law', promptInjection: 'iron 注入' }),
43
+ makeConstraint({ id: 'guide_a', kind: 'check', level: 'guideline', promptInjection: 'guide 注入' }),
44
+ makeConstraint({ id: 'prompt_a', level: 'prompt', promptInjection: 'prompt 注入' }),
45
+ ];
46
+
47
+ const section = renderConstraintsSection(constraints, '9.9.9');
48
+
49
+ expect(section.startsWith(`${CONSTRAINTS_START_MARKER}\n<!-- version: 9.9.9 -->\n`)).toBe(true);
50
+ expect(section.endsWith(`${CONSTRAINTS_END_MARKER}\n`)).toBe(true);
51
+ expect(section).toContain('### Iron Laws');
52
+ expect(section).toContain('### Guidelines');
53
+ expect(section).toContain('### Prompts');
54
+ expect(section).toContain('- **iron_a**: iron 注入');
55
+ expect(section).toContain('- **guide_a**: guide 注入');
56
+ expect(section).toContain('- **prompt_a**: prompt 注入');
57
+ // 分组顺序:Iron Laws → Guidelines → Prompts
58
+ expect(section.indexOf('### Iron Laws')).toBeLessThan(section.indexOf('### Guidelines'));
59
+ expect(section.indexOf('### Guidelines')).toBeLessThan(section.indexOf('### Prompts'));
60
+ });
61
+
62
+ it('无 promptInjection 的 check 条目(如 docs_freshness)不出现在注入段', () => {
63
+ const constraints = [
64
+ makeConstraint({ id: 'docs_freshness', kind: 'check', level: 'iron_law' }),
65
+ makeConstraint({ id: 'iron_a', kind: 'check', level: 'iron_law', promptInjection: 'iron 注入' }),
66
+ ];
67
+
68
+ const section = renderConstraintsSection(constraints, '1.0.0');
69
+
70
+ expect(section).not.toContain('docs_freshness');
71
+ expect(section).toContain('- **iron_a**: iron 注入');
72
+ });
73
+
74
+ it('内置全集渲染:docs_freshness 不出现,场景 prompt 由调用方过滤后传入', () => {
75
+ const section = renderConstraintsSection(getAllConstraints(), '1.0.0');
76
+ expect(section).not.toContain('docs_freshness');
77
+ expect(section).toContain('- **no_fuzzy_completion_claim**');
78
+ });
79
+ });
80
+
81
+ describe('setupClaudeMdConstraints(消费生效集)', () => {
82
+ let tempDir: string;
83
+ let claudeMdPath: string;
84
+
85
+ const writeConfig = (yaml: string) => {
86
+ fs.mkdirSync(path.join(tempDir, '.harness'), { recursive: true });
87
+ fs.writeFileSync(path.join(tempDir, '.harness', 'config.yml'), yaml);
88
+ };
89
+
90
+ beforeEach(() => {
91
+ tempDir = fs.mkdtempSync(path.join(process.cwd(), 'temp-test-init-'));
92
+ claudeMdPath = path.join(tempDir, 'CLAUDE.md');
93
+ jest.spyOn(console, 'log').mockImplementation(() => {});
94
+ });
95
+
96
+ afterEach(() => {
97
+ jest.restoreAllMocks();
98
+ fs.rmSync(tempDir, { recursive: true, force: true });
99
+ });
100
+
101
+ it('CLAUDE.md 不存在时创建并写入约束段', async () => {
102
+ await setupClaudeMdConstraints(tempDir);
103
+
104
+ const content = fs.readFileSync(claudeMdPath, 'utf-8');
105
+ expect(content).toContain('## Governance Rules');
106
+ expect(content).toContain(CONSTRAINTS_START_MARKER);
107
+ expect(content).toContain(CONSTRAINTS_END_MARKER);
108
+ expect(content).toContain('### Iron Laws');
109
+ expect(content).toContain('### Prompts (行为约束)');
110
+ });
111
+
112
+ it('连跑两次输出一致(幂等)', async () => {
113
+ await setupClaudeMdConstraints(tempDir);
114
+ const first = fs.readFileSync(claudeMdPath, 'utf-8');
115
+ await setupClaudeMdConstraints(tempDir);
116
+ const second = fs.readFileSync(claudeMdPath, 'utf-8');
117
+ expect(second).toBe(first);
118
+ });
119
+
120
+ it('标记区间外内容保持不动', async () => {
121
+ fs.writeFileSync(claudeMdPath, '# My Project\n\n用户自定义内容\n');
122
+ await setupClaudeMdConstraints(tempDir);
123
+ const content = fs.readFileSync(claudeMdPath, 'utf-8');
124
+ expect(content).toContain('# My Project');
125
+ expect(content).toContain('用户自定义内容');
126
+ });
127
+
128
+ it('尊重 config.yml 禁用:注入段不含被禁条目', async () => {
129
+ writeConfig('constraints:\n no_fuzzy_completion_claim:\n enabled: false\n');
130
+ await setupClaudeMdConstraints(tempDir);
131
+
132
+ const content = fs.readFileSync(claudeMdPath, 'utf-8');
133
+ expect(content).not.toContain('no_fuzzy_completion_claim');
134
+ expect(content).toContain('- **no_fix_without_root_cause**');
135
+ });
136
+
137
+ it('scenes 缺省不注入场景专属 prompt;配置 scenes 后注入', async () => {
138
+ writeConfig('preset: standard\n');
139
+ await setupClaudeMdConstraints(tempDir);
140
+ let content = fs.readFileSync(claudeMdPath, 'utf-8');
141
+ expect(content).not.toContain('no_skill_without_test');
142
+
143
+ writeConfig('preset: standard\nscenes:\n - agent-skill\n');
144
+ await setupClaudeMdConstraints(tempDir);
145
+ content = fs.readFileSync(claudeMdPath, 'utf-8');
146
+ expect(content).toContain('- **no_skill_without_test**');
147
+ expect(content).not.toContain('no_model_for_deterministic');
148
+ });
149
+
150
+ it('注入段文本与 renderConstraintsSection(getEffectiveConstraints()) 一致', async () => {
151
+ writeConfig('constraints:\n no_bypass_checkpoint:\n enabled: false\n');
152
+ await setupClaudeMdConstraints(tempDir);
153
+
154
+ const content = fs.readFileSync(claudeMdPath, 'utf-8');
155
+ const start = content.indexOf(CONSTRAINTS_START_MARKER);
156
+ const end = content.indexOf(CONSTRAINTS_END_MARKER) + CONSTRAINTS_END_MARKER.length + 1;
157
+ const actual = content.slice(start, end);
158
+ const version = /<!-- version: ([^ ]+) -->/.exec(content)![1];
159
+ // P6 漂移校验同款比对:期望段(生效集渲染) vs 实际段
160
+ expect(actual).toBe(renderConstraintsSection(getEffectiveConstraints(tempDir), version));
161
+ });
162
+ });
163
+
164
+ describe('setupClaudeMdOutputStyle(标记化)', () => {
165
+ let tempDir: string;
166
+ let claudeMdPath: string;
167
+
168
+ beforeEach(() => {
169
+ tempDir = fs.mkdtempSync(path.join(process.cwd(), 'temp-test-output-style-'));
170
+ claudeMdPath = path.join(tempDir, 'CLAUDE.md');
171
+ jest.spyOn(console, 'log').mockImplementation(() => {});
172
+ });
173
+
174
+ afterEach(() => {
175
+ jest.restoreAllMocks();
176
+ fs.rmSync(tempDir, { recursive: true, force: true });
177
+ });
178
+
179
+ it('CLAUDE.md 不存在时不创建(由 constraints 注入负责创建)', async () => {
180
+ await setupClaudeMdOutputStyle(tempDir);
181
+ expect(fs.existsSync(claudeMdPath)).toBe(false);
182
+ });
183
+
184
+ it('无 Output Style 段:插入带标记段到文件顶部,二次运行幂等', async () => {
185
+ fs.writeFileSync(claudeMdPath, '# My Project\n\n正文\n');
186
+ await setupClaudeMdOutputStyle(tempDir);
187
+
188
+ const content = fs.readFileSync(claudeMdPath, 'utf-8');
189
+ expect(content.startsWith(OUTPUT_STYLE_START)).toBe(true);
190
+ expect(content).toContain('## Output Style');
191
+ expect(content).toContain(OUTPUT_STYLE_END);
192
+ expect(content).toContain('# My Project');
193
+
194
+ await setupClaudeMdOutputStyle(tempDir);
195
+ expect(fs.readFileSync(claudeMdPath, 'utf-8')).toBe(content);
196
+ });
197
+
198
+ it('旧版 harness 无标记段(特征串匹配):迁移为标记版', async () => {
199
+ const legacy = [
200
+ '## Output Style',
201
+ '',
202
+ 'Terse like caveman. Technical substance exact. Only fluff die.',
203
+ 'Drop: articles, filler (just/really/basically), pleasantries (sure/certainly/happy to), hedging.',
204
+ '',
205
+ '# My Project',
206
+ '',
207
+ ].join('\n');
208
+ fs.writeFileSync(claudeMdPath, legacy);
209
+
210
+ await setupClaudeMdOutputStyle(tempDir);
211
+ const content = fs.readFileSync(claudeMdPath, 'utf-8');
212
+
213
+ expect(content).toContain(OUTPUT_STYLE_START);
214
+ expect(content).toContain(OUTPUT_STYLE_END);
215
+ expect(content).toContain('Terse like caveman');
216
+ expect(content).toContain('# My Project');
217
+ // 迁移后只剩一个 Output Style 标题
218
+ expect(content.match(/^## Output Style$/gm)).toHaveLength(1);
219
+ // 幂等
220
+ await setupClaudeMdOutputStyle(tempDir);
221
+ expect(fs.readFileSync(claudeMdPath, 'utf-8')).toBe(content);
222
+ });
223
+
224
+ it('用户自写 Output Style 段(特征串不匹配):不动、不追加、console 提示', async () => {
225
+ const userContent = '# My Project\n\n## Output Style\n\n用户自己的输出风格要求。\n';
226
+ fs.writeFileSync(claudeMdPath, userContent);
227
+ const logSpy = jest.spyOn(console, 'log');
228
+
229
+ await setupClaudeMdOutputStyle(tempDir);
230
+
231
+ expect(fs.readFileSync(claudeMdPath, 'utf-8')).toBe(userContent);
232
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('跳过'));
233
+ });
234
+ });
@@ -75,7 +75,7 @@ describe('status command - 补充覆盖', () => {
75
75
  threshold: 0.5,
76
76
  },
77
77
  {
78
- constraintId: 'no_self_approval',
78
+ constraintId: 'no_completion_without_verification',
79
79
  type: 'frequent_bypass',
80
80
  current: 0.3,
81
81
  threshold: 0.1,
@@ -116,13 +116,13 @@ describe('status command - 补充覆盖', () => {
116
116
  it('应该显示 Iron Laws 统计', async () => {
117
117
  const mockTraces = [
118
118
  { constraintId: 'no_bypass_checkpoint', level: 'iron_law', result: 'pass' },
119
- { constraintId: 'no_self_approval', level: 'iron_law', result: 'pass' },
119
+ { constraintId: 'no_completion_without_verification', level: 'iron_law', result: 'pass' },
120
120
  ];
121
121
  mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
122
122
 
123
123
  const mockSummaries = [
124
124
  { constraintId: 'no_bypass_checkpoint', level: 'iron_law', passRate: 1, totalChecks: 10, bypassRate: 0 },
125
- { constraintId: 'no_self_approval', level: 'iron_law', passRate: 0.8, totalChecks: 5, bypassRate: 0 },
125
+ { constraintId: 'no_completion_without_verification', level: 'iron_law', passRate: 0.8, totalChecks: 5, bypassRate: 0 },
126
126
  ];
127
127
  const mockAnalyze = {
128
128
  summarize: jest.fn().mockReturnValue(mockSummaries),
@@ -138,12 +138,12 @@ describe('status command - 补充覆盖', () => {
138
138
 
139
139
  it('应该显示 Guidelines 统计', async () => {
140
140
  const mockTraces = [
141
- { constraintId: 'no_any_type', level: 'guideline', result: 'pass' },
141
+ { constraintId: 'capability_sync', level: 'guideline', result: 'pass' },
142
142
  ];
143
143
  mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
144
144
 
145
145
  const mockSummaries = [
146
- { constraintId: 'no_any_type', level: 'guideline', passRate: 0.9, totalChecks: 20, bypassRate: 0.1 },
146
+ { constraintId: 'capability_sync', level: 'guideline', passRate: 0.9, totalChecks: 20, bypassRate: 0.1 },
147
147
  ];
148
148
  const mockAnalyze = {
149
149
  summarize: jest.fn().mockReturnValue(mockSummaries),
@@ -158,12 +158,12 @@ describe('status command - 补充覆盖', () => {
158
158
 
159
159
  it('应该显示 Tips 统计', async () => {
160
160
  const mockTraces = [
161
- { constraintId: 'readme_required', level: 'tip', result: 'pass' },
161
+ { constraintId: 'context_doc_sync', level: 'tip', result: 'pass' },
162
162
  ];
163
163
  mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
164
164
 
165
165
  const mockSummaries = [
166
- { constraintId: 'readme_required', level: 'tip', passRate: 1, totalChecks: 5, bypassRate: 0 },
166
+ { constraintId: 'context_doc_sync', level: 'tip', passRate: 1, totalChecks: 5, bypassRate: 0 },
167
167
  ];
168
168
  const mockAnalyze = {
169
169
  summarize: jest.fn().mockReturnValue(mockSummaries),
@@ -181,15 +181,15 @@ describe('status command - 补充覆盖', () => {
181
181
  it('详细模式应该显示所有级别详情', async () => {
182
182
  const mockTraces = [
183
183
  { constraintId: 'no_bypass_checkpoint', level: 'iron_law', result: 'pass' },
184
- { constraintId: 'no_any_type', level: 'guideline', result: 'pass' },
185
- { constraintId: 'readme_required', level: 'tip', result: 'pass' },
184
+ { constraintId: 'capability_sync', level: 'guideline', result: 'pass' },
185
+ { constraintId: 'context_doc_sync', level: 'tip', result: 'pass' },
186
186
  ];
187
187
  mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
188
188
 
189
189
  const mockSummaries = [
190
190
  { constraintId: 'no_bypass_checkpoint', level: 'iron_law', passRate: 1, totalChecks: 10, bypassRate: 0 },
191
- { constraintId: 'no_any_type', level: 'guideline', passRate: 0.9, totalChecks: 20, bypassRate: 0.1 },
192
- { constraintId: 'readme_required', level: 'tip', passRate: 1, totalChecks: 5, bypassRate: 0 },
191
+ { constraintId: 'capability_sync', level: 'guideline', passRate: 0.9, totalChecks: 20, bypassRate: 0.1 },
192
+ { constraintId: 'context_doc_sync', level: 'tip', passRate: 1, totalChecks: 5, bypassRate: 0 },
193
193
  ];
194
194
  const mockAnalyze = {
195
195
  summarize: jest.fn().mockReturnValue(mockSummaries),
@@ -283,7 +283,7 @@ describe('status command', () => {
283
283
  (MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
284
284
 
285
285
  await status({});
286
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('harness flow'));
286
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('harness status --detail'));
287
287
  });
288
288
  });
289
289