@dommaker/harness 1.6.0 → 1.8.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 (560) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/bin/harness.js +17 -1
  3. package/dist/agents/index.d.ts +6 -2
  4. package/dist/agents/index.d.ts.map +1 -1
  5. package/dist/agents/index.js +6 -16
  6. package/dist/agents/index.js.map +1 -1
  7. package/dist/cli/commands/__tests__/gate-decision.d.ts +18 -0
  8. package/dist/cli/commands/__tests__/gate-decision.d.ts.map +1 -0
  9. package/dist/cli/commands/__tests__/gate-decision.js +26 -0
  10. package/dist/cli/commands/__tests__/gate-decision.js.map +1 -0
  11. package/dist/cli/commands/acceptance.d.ts.map +1 -1
  12. package/dist/cli/commands/acceptance.js +17 -28
  13. package/dist/cli/commands/acceptance.js.map +1 -1
  14. package/dist/cli/commands/check.d.ts +11 -3
  15. package/dist/cli/commands/check.d.ts.map +1 -1
  16. package/dist/cli/commands/check.js +26 -11
  17. package/dist/cli/commands/check.js.map +1 -1
  18. package/dist/cli/commands/command.d.ts +0 -1
  19. package/dist/cli/commands/command.d.ts.map +1 -1
  20. package/dist/cli/commands/command.js +8 -6
  21. package/dist/cli/commands/command.js.map +1 -1
  22. package/dist/cli/commands/constraints-retire.d.ts.map +1 -1
  23. package/dist/cli/commands/constraints-retire.js +36 -57
  24. package/dist/cli/commands/constraints-retire.js.map +1 -1
  25. package/dist/cli/commands/contract.d.ts.map +1 -1
  26. package/dist/cli/commands/contract.js +36 -42
  27. package/dist/cli/commands/contract.js.map +1 -1
  28. package/dist/cli/commands/definitions.d.ts +2 -0
  29. package/dist/cli/commands/definitions.d.ts.map +1 -1
  30. package/dist/cli/commands/definitions.js +11 -6
  31. package/dist/cli/commands/definitions.js.map +1 -1
  32. package/dist/cli/commands/failure.d.ts.map +1 -1
  33. package/dist/cli/commands/failure.js +16 -2
  34. package/dist/cli/commands/failure.js.map +1 -1
  35. package/dist/cli/commands/init.d.ts +14 -8
  36. package/dist/cli/commands/init.d.ts.map +1 -1
  37. package/dist/cli/commands/init.js +172 -364
  38. package/dist/cli/commands/init.js.map +1 -1
  39. package/dist/cli/commands/knowledge-view.d.ts +90 -0
  40. package/dist/cli/commands/knowledge-view.d.ts.map +1 -0
  41. package/dist/cli/commands/knowledge-view.js +198 -0
  42. package/dist/cli/commands/knowledge-view.js.map +1 -0
  43. package/dist/cli/commands/knowledge.d.ts +137 -52
  44. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  45. package/dist/cli/commands/knowledge.js +534 -400
  46. package/dist/cli/commands/knowledge.js.map +1 -1
  47. package/dist/cli/commands/passes-gate.d.ts +4 -1
  48. package/dist/cli/commands/passes-gate.d.ts.map +1 -1
  49. package/dist/cli/commands/passes-gate.js +11 -2
  50. package/dist/cli/commands/passes-gate.js.map +1 -1
  51. package/dist/cli/commands/performance.d.ts.map +1 -1
  52. package/dist/cli/commands/performance.js +19 -31
  53. package/dist/cli/commands/performance.js.map +1 -1
  54. package/dist/cli/commands/report.d.ts.map +1 -1
  55. package/dist/cli/commands/report.js +2 -1
  56. package/dist/cli/commands/report.js.map +1 -1
  57. package/dist/cli/commands/review.d.ts.map +1 -1
  58. package/dist/cli/commands/review.js +13 -25
  59. package/dist/cli/commands/review.js.map +1 -1
  60. package/dist/cli/commands/scaffold-templates.d.ts +73 -0
  61. package/dist/cli/commands/scaffold-templates.d.ts.map +1 -0
  62. package/dist/cli/commands/scaffold-templates.js +424 -0
  63. package/dist/cli/commands/scaffold-templates.js.map +1 -0
  64. package/dist/cli/commands/scaffold.d.ts +105 -0
  65. package/dist/cli/commands/scaffold.d.ts.map +1 -0
  66. package/dist/cli/commands/scaffold.js +263 -0
  67. package/dist/cli/commands/scaffold.js.map +1 -0
  68. package/dist/cli/commands/security.d.ts.map +1 -1
  69. package/dist/cli/commands/security.js +22 -24
  70. package/dist/cli/commands/security.js.map +1 -1
  71. package/dist/cli/commands/spec-baseline-check.d.ts +43 -0
  72. package/dist/cli/commands/spec-baseline-check.d.ts.map +1 -1
  73. package/dist/cli/commands/spec-baseline-check.js +109 -34
  74. package/dist/cli/commands/spec-baseline-check.js.map +1 -1
  75. package/dist/cli/commands/sync-docs/context-syncer.d.ts +21 -2
  76. package/dist/cli/commands/sync-docs/context-syncer.d.ts.map +1 -1
  77. package/dist/cli/commands/sync-docs/context-syncer.js +112 -28
  78. package/dist/cli/commands/sync-docs/context-syncer.js.map +1 -1
  79. package/dist/cli/commands/sync-docs/index.d.ts +1 -1
  80. package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
  81. package/dist/cli/commands/sync-docs/index.js +86 -27
  82. package/dist/cli/commands/sync-docs/index.js.map +1 -1
  83. package/dist/cli/commands/sync-docs/project-reader.d.ts +24 -1
  84. package/dist/cli/commands/sync-docs/project-reader.d.ts.map +1 -1
  85. package/dist/cli/commands/sync-docs/project-reader.js +33 -7
  86. package/dist/cli/commands/sync-docs/project-reader.js.map +1 -1
  87. package/dist/cli/commands/validate.d.ts +0 -16
  88. package/dist/cli/commands/validate.d.ts.map +1 -1
  89. package/dist/cli/commands/validate.js +2 -97
  90. package/dist/cli/commands/validate.js.map +1 -1
  91. package/dist/cli/gate-command.d.ts +37 -0
  92. package/dist/cli/gate-command.d.ts.map +1 -0
  93. package/dist/cli/gate-command.js +64 -0
  94. package/dist/cli/gate-command.js.map +1 -0
  95. package/dist/completion-checkers/index.d.ts +1 -1
  96. package/dist/completion-checkers/index.d.ts.map +1 -1
  97. package/dist/completion-checkers/index.js +0 -15
  98. package/dist/completion-checkers/index.js.map +1 -1
  99. package/dist/context/knowledge-injector.d.ts +4 -2
  100. package/dist/context/knowledge-injector.d.ts.map +1 -1
  101. package/dist/context/knowledge-injector.js +13 -6
  102. package/dist/context/knowledge-injector.js.map +1 -1
  103. package/dist/core/constraints/agent-prompt-renderer.js +2 -2
  104. package/dist/core/constraints/agent-prompt-renderer.js.map +1 -1
  105. package/dist/core/constraints/capabilities-reconcile.d.ts +20 -0
  106. package/dist/core/constraints/capabilities-reconcile.d.ts.map +1 -1
  107. package/dist/core/constraints/capabilities-reconcile.js +26 -4
  108. package/dist/core/constraints/capabilities-reconcile.js.map +1 -1
  109. package/dist/core/constraints/check-cache.d.ts +1 -1
  110. package/dist/core/constraints/check-cache.js +1 -1
  111. package/dist/core/constraints/checker.d.ts +8 -3
  112. package/dist/core/constraints/checker.d.ts.map +1 -1
  113. package/dist/core/constraints/checker.js +30 -19
  114. package/dist/core/constraints/checker.js.map +1 -1
  115. package/dist/core/constraints/checkers/capability-sync.d.ts +3 -1
  116. package/dist/core/constraints/checkers/capability-sync.d.ts.map +1 -1
  117. package/dist/core/constraints/checkers/capability-sync.js +17 -24
  118. package/dist/core/constraints/checkers/capability-sync.js.map +1 -1
  119. package/dist/core/constraints/checkers/context-doc-sync.d.ts +1 -1
  120. package/dist/core/constraints/checkers/context-doc-sync.d.ts.map +1 -1
  121. package/dist/core/constraints/checkers/context-doc-sync.js +9 -6
  122. package/dist/core/constraints/checkers/context-doc-sync.js.map +1 -1
  123. package/dist/core/constraints/checkers/docs-freshness.d.ts.map +1 -1
  124. package/dist/core/constraints/checkers/docs-freshness.js +24 -30
  125. package/dist/core/constraints/checkers/docs-freshness.js.map +1 -1
  126. package/dist/core/constraints/checkers/governance-presence.d.ts.map +1 -1
  127. package/dist/core/constraints/checkers/governance-presence.js +10 -4
  128. package/dist/core/constraints/checkers/governance-presence.js.map +1 -1
  129. package/dist/core/constraints/checkers/types.d.ts +10 -6
  130. package/dist/core/constraints/checkers/types.d.ts.map +1 -1
  131. package/dist/core/constraints/checkers/types.js +7 -3
  132. package/dist/core/constraints/checkers/types.js.map +1 -1
  133. package/dist/core/constraints/context-builder.d.ts +11 -10
  134. package/dist/core/constraints/context-builder.d.ts.map +1 -1
  135. package/dist/core/constraints/context-builder.js +31 -27
  136. package/dist/core/constraints/context-builder.js.map +1 -1
  137. package/dist/core/constraints/context-reconcile.d.ts +75 -0
  138. package/dist/core/constraints/context-reconcile.d.ts.map +1 -0
  139. package/dist/core/constraints/context-reconcile.js +161 -0
  140. package/dist/core/constraints/context-reconcile.js.map +1 -0
  141. package/dist/core/constraints/doc-freshness/runner.d.ts +7 -0
  142. package/dist/core/constraints/doc-freshness/runner.d.ts.map +1 -1
  143. package/dist/core/constraints/doc-freshness/runner.js +8 -3
  144. package/dist/core/constraints/doc-freshness/runner.js.map +1 -1
  145. package/dist/core/constraints/git-evidence.js +1 -1
  146. package/dist/core/constraints/git-evidence.js.map +1 -1
  147. package/dist/core/constraints/injection-drift.d.ts +7 -2
  148. package/dist/core/constraints/injection-drift.d.ts.map +1 -1
  149. package/dist/core/constraints/injection-drift.js +13 -7
  150. package/dist/core/constraints/injection-drift.js.map +1 -1
  151. package/dist/core/constraints/run-env.d.ts +92 -0
  152. package/dist/core/constraints/run-env.d.ts.map +1 -0
  153. package/dist/core/constraints/run-env.js +175 -0
  154. package/dist/core/constraints/run-env.js.map +1 -0
  155. package/dist/core/constraints/triggers.d.ts +22 -0
  156. package/dist/core/constraints/triggers.d.ts.map +1 -0
  157. package/dist/core/constraints/triggers.js +31 -0
  158. package/dist/core/constraints/triggers.js.map +1 -0
  159. package/dist/core/effective-constraints.d.ts +13 -4
  160. package/dist/core/effective-constraints.d.ts.map +1 -1
  161. package/dist/core/effective-constraints.js +16 -6
  162. package/dist/core/effective-constraints.js.map +1 -1
  163. package/dist/core/effective-set.d.ts +8 -15
  164. package/dist/core/effective-set.d.ts.map +1 -1
  165. package/dist/core/effective-set.js +7 -12
  166. package/dist/core/effective-set.js.map +1 -1
  167. package/dist/core/index.d.ts +2 -1
  168. package/dist/core/index.d.ts.map +1 -1
  169. package/dist/core/index.js.map +1 -1
  170. package/dist/core/project-config-loader.d.ts +39 -17
  171. package/dist/core/project-config-loader.d.ts.map +1 -1
  172. package/dist/core/project-config-loader.js +51 -122
  173. package/dist/core/project-config-loader.js.map +1 -1
  174. package/dist/core/validators/check-handlers/http.d.ts +2 -2
  175. package/dist/core/validators/check-handlers/http.d.ts.map +1 -1
  176. package/dist/core/validators/check-handlers/http.js +12 -7
  177. package/dist/core/validators/check-handlers/http.js.map +1 -1
  178. package/dist/core/validators/index.d.ts +1 -1
  179. package/dist/core/validators/index.d.ts.map +1 -1
  180. package/dist/core/validators/passes-gate.d.ts.map +1 -1
  181. package/dist/core/validators/passes-gate.js +2 -2
  182. package/dist/core/validators/passes-gate.js.map +1 -1
  183. package/dist/failure/classifier.d.ts +1 -1
  184. package/dist/failure/classifier.d.ts.map +1 -1
  185. package/dist/failure/classifier.js +8 -8
  186. package/dist/failure/classifier.js.map +1 -1
  187. package/dist/failure/index.d.ts +2 -1
  188. package/dist/failure/index.d.ts.map +1 -1
  189. package/dist/failure/index.js +9 -17
  190. package/dist/failure/index.js.map +1 -1
  191. package/dist/failure/recorder.d.ts +1 -1
  192. package/dist/failure/recorder.d.ts.map +1 -1
  193. package/dist/gates/checker-gate.d.ts +2 -1
  194. package/dist/gates/checker-gate.d.ts.map +1 -1
  195. package/dist/gates/checker-gate.js +3 -2
  196. package/dist/gates/checker-gate.js.map +1 -1
  197. package/dist/gates/command.d.ts +10 -4
  198. package/dist/gates/command.d.ts.map +1 -1
  199. package/dist/gates/command.js +36 -62
  200. package/dist/gates/command.js.map +1 -1
  201. package/dist/gates/contract.d.ts.map +1 -1
  202. package/dist/gates/contract.js +0 -4
  203. package/dist/gates/contract.js.map +1 -1
  204. package/dist/gates/definitions.d.ts +2 -1
  205. package/dist/gates/definitions.d.ts.map +1 -1
  206. package/dist/gates/definitions.js +6 -5
  207. package/dist/gates/definitions.js.map +1 -1
  208. package/dist/gates/index.d.ts +0 -4
  209. package/dist/gates/index.d.ts.map +1 -1
  210. package/dist/gates/index.js +1 -6
  211. package/dist/gates/index.js.map +1 -1
  212. package/dist/gates/performance.d.ts.map +1 -1
  213. package/dist/gates/performance.js +0 -4
  214. package/dist/gates/performance.js.map +1 -1
  215. package/dist/gates/runner.d.ts +1 -1
  216. package/dist/gates/runner.js +1 -1
  217. package/dist/gates/security.d.ts +1 -1
  218. package/dist/gates/security.d.ts.map +1 -1
  219. package/dist/gates/security.js +5 -6
  220. package/dist/gates/security.js.map +1 -1
  221. package/dist/gates/types.d.ts +14 -17
  222. package/dist/gates/types.d.ts.map +1 -1
  223. package/dist/gates/types.js.map +1 -1
  224. package/dist/hooks/bootstrap.d.ts +6 -4
  225. package/dist/hooks/bootstrap.d.ts.map +1 -1
  226. package/dist/hooks/bootstrap.js +25 -13
  227. package/dist/hooks/bootstrap.js.map +1 -1
  228. package/dist/hooks/config.d.ts +6 -2
  229. package/dist/hooks/config.d.ts.map +1 -1
  230. package/dist/hooks/config.js +5 -1
  231. package/dist/hooks/config.js.map +1 -1
  232. package/dist/hooks/index.d.ts +1 -1
  233. package/dist/hooks/index.d.ts.map +1 -1
  234. package/dist/hooks/index.js +0 -15
  235. package/dist/hooks/index.js.map +1 -1
  236. package/dist/hooks/pipeline.d.ts.map +1 -1
  237. package/dist/hooks/pipeline.js +2 -4
  238. package/dist/hooks/pipeline.js.map +1 -1
  239. package/dist/hooks/registry.d.ts +11 -8
  240. package/dist/hooks/registry.d.ts.map +1 -1
  241. package/dist/hooks/registry.js +22 -7
  242. package/dist/hooks/registry.js.map +1 -1
  243. package/dist/hooks/types.d.ts +23 -7
  244. package/dist/hooks/types.d.ts.map +1 -1
  245. package/dist/index.d.ts +4 -4
  246. package/dist/index.d.ts.map +1 -1
  247. package/dist/index.js +3 -4
  248. package/dist/index.js.map +1 -1
  249. package/dist/knowledge/audit-scoring.d.ts +87 -0
  250. package/dist/knowledge/audit-scoring.d.ts.map +1 -0
  251. package/dist/knowledge/audit-scoring.js +470 -0
  252. package/dist/knowledge/audit-scoring.js.map +1 -0
  253. package/dist/knowledge/audit.d.ts +11 -59
  254. package/dist/knowledge/audit.d.ts.map +1 -1
  255. package/dist/knowledge/audit.js +47 -522
  256. package/dist/knowledge/audit.js.map +1 -1
  257. package/dist/knowledge/import.d.ts.map +1 -1
  258. package/dist/knowledge/import.js +2 -0
  259. package/dist/knowledge/import.js.map +1 -1
  260. package/dist/knowledge/index-generator.d.ts.map +1 -1
  261. package/dist/knowledge/index-generator.js +4 -5
  262. package/dist/knowledge/index-generator.js.map +1 -1
  263. package/dist/knowledge/index.d.ts +3 -2
  264. package/dist/knowledge/index.d.ts.map +1 -1
  265. package/dist/knowledge/index.js +3 -16
  266. package/dist/knowledge/index.js.map +1 -1
  267. package/dist/knowledge/ingest.js +1 -1
  268. package/dist/knowledge/ingest.js.map +1 -1
  269. package/dist/knowledge/lifecycle.d.ts.map +1 -1
  270. package/dist/knowledge/lifecycle.js +4 -2
  271. package/dist/knowledge/lifecycle.js.map +1 -1
  272. package/dist/knowledge/migration.d.ts +3 -0
  273. package/dist/knowledge/migration.d.ts.map +1 -1
  274. package/dist/knowledge/migration.js +8 -2
  275. package/dist/knowledge/migration.js.map +1 -1
  276. package/dist/knowledge/query.d.ts +6 -0
  277. package/dist/knowledge/query.d.ts.map +1 -1
  278. package/dist/knowledge/query.js +8 -2
  279. package/dist/knowledge/query.js.map +1 -1
  280. package/dist/knowledge/reference-tracker.d.ts.map +1 -1
  281. package/dist/knowledge/reference-tracker.js +4 -6
  282. package/dist/knowledge/reference-tracker.js.map +1 -1
  283. package/dist/knowledge/store.d.ts +22 -16
  284. package/dist/knowledge/store.d.ts.map +1 -1
  285. package/dist/knowledge/store.js +53 -12
  286. package/dist/knowledge/store.js.map +1 -1
  287. package/dist/knowledge/tree-walker.d.ts +19 -0
  288. package/dist/knowledge/tree-walker.d.ts.map +1 -0
  289. package/dist/knowledge/tree-walker.js +30 -0
  290. package/dist/knowledge/tree-walker.js.map +1 -0
  291. package/dist/knowledge/types.d.ts +16 -0
  292. package/dist/knowledge/types.d.ts.map +1 -1
  293. package/dist/monitoring/traces.d.ts +5 -3
  294. package/dist/monitoring/traces.d.ts.map +1 -1
  295. package/dist/monitoring/traces.js +19 -5
  296. package/dist/monitoring/traces.js.map +1 -1
  297. package/dist/test-setup/jsonl-fake-fs.d.ts +30 -0
  298. package/dist/test-setup/jsonl-fake-fs.d.ts.map +1 -0
  299. package/dist/test-setup/jsonl-fake-fs.js +49 -0
  300. package/dist/test-setup/jsonl-fake-fs.js.map +1 -0
  301. package/dist/tools/index.d.ts +4 -1
  302. package/dist/tools/index.d.ts.map +1 -1
  303. package/dist/tools/index.js +7 -15
  304. package/dist/tools/index.js.map +1 -1
  305. package/dist/types/checkpoint.d.ts +5 -0
  306. package/dist/types/checkpoint.d.ts.map +1 -1
  307. package/dist/types/passes-gate.d.ts +0 -13
  308. package/dist/types/passes-gate.d.ts.map +1 -1
  309. package/dist/types/project-config.d.ts +18 -0
  310. package/dist/types/project-config.d.ts.map +1 -1
  311. package/dist/types/trace.d.ts +13 -4
  312. package/dist/types/trace.d.ts.map +1 -1
  313. package/dist/types/trace.js +4 -3
  314. package/dist/types/trace.js.map +1 -1
  315. package/dist/utils/exec.d.ts +4 -27
  316. package/dist/utils/exec.d.ts.map +1 -1
  317. package/dist/utils/exec.js +4 -94
  318. package/dist/utils/exec.js.map +1 -1
  319. package/dist/utils/jsonl.d.ts +17 -7
  320. package/dist/utils/jsonl.d.ts.map +1 -1
  321. package/dist/utils/jsonl.js +144 -17
  322. package/dist/utils/jsonl.js.map +1 -1
  323. package/dist/utils/numeric-flag.d.ts +44 -0
  324. package/dist/utils/numeric-flag.d.ts.map +1 -0
  325. package/dist/utils/numeric-flag.js +62 -0
  326. package/dist/utils/numeric-flag.js.map +1 -0
  327. package/package.json +1 -1
  328. package/src/CONTEXT.md +5 -2
  329. package/src/__tests__/acceptance-gate.test.ts +7 -4
  330. package/src/__tests__/checker-extra.test.ts +3 -2
  331. package/src/__tests__/checker.test.ts +4 -3
  332. package/src/__tests__/checkpoint.test.ts +26 -9
  333. package/src/__tests__/context-files-resolution.test.ts +29 -15
  334. package/src/__tests__/contract-gate.test.ts +19 -40
  335. package/src/__tests__/error-classifier.test.ts +1 -1
  336. package/src/__tests__/exec.test.ts +45 -107
  337. package/src/__tests__/failure-recorder.test.ts +7 -5
  338. package/src/__tests__/hooks-pipeline.test.ts +34 -47
  339. package/src/__tests__/layering.test.ts +217 -25
  340. package/src/__tests__/passes-gate.test.ts +9 -4
  341. package/src/__tests__/performance-gate.test.ts +3 -15
  342. package/src/__tests__/project-config-loader-extra.test.ts +31 -57
  343. package/src/__tests__/project-config-loader.test.ts +6 -3
  344. package/src/__tests__/public-exports.test.ts +27 -6
  345. package/src/__tests__/public-type-surface.test.ts +360 -0
  346. package/src/__tests__/public-value-surface.test.ts +182 -0
  347. package/src/__tests__/review-gate.test.ts +4 -6
  348. package/src/__tests__/security-gate.test.ts +5 -20
  349. package/src/__tests__/skip-semantics.test.ts +5 -3
  350. package/src/__tests__/spec-validator.test.ts +3 -2
  351. package/src/__tests__/sub-barrels-explicit.test.ts +87 -0
  352. package/src/__tests__/test-fixture-hygiene.test.ts +53 -0
  353. package/src/__tests__/trace-analyzer-extra.test.ts +5 -3
  354. package/src/__tests__/trace-analyzer.test.ts +5 -3
  355. package/src/__tests__/trace-collector.test.ts +5 -3
  356. package/src/agents/index.ts +13 -2
  357. package/src/cli/__tests__/gate-command.test.ts +162 -0
  358. package/src/cli/commands/CONTEXT.md +29 -5
  359. package/src/cli/commands/__tests__/acceptance.test.ts +24 -25
  360. package/src/cli/commands/__tests__/bin-exit-mapping.test.ts +15 -0
  361. package/src/cli/commands/__tests__/check-drift.test.ts +42 -1
  362. package/src/cli/commands/__tests__/check-read-count.test.ts +167 -0
  363. package/src/cli/commands/__tests__/check-skip-output.test.ts +4 -1
  364. package/src/cli/commands/__tests__/check.test.ts +43 -36
  365. package/src/cli/commands/__tests__/command-gate-dead-options.test.ts +60 -0
  366. package/src/cli/commands/__tests__/command.test.ts +33 -17
  367. package/src/cli/commands/__tests__/constraints-retire.test.ts +52 -0
  368. package/src/cli/commands/__tests__/context-syncer.test.ts +141 -0
  369. package/src/cli/commands/__tests__/contract.test.ts +20 -20
  370. package/src/cli/commands/__tests__/failure.test.ts +5 -4
  371. package/src/cli/commands/__tests__/gate-decision.ts +30 -0
  372. package/src/cli/commands/__tests__/init-dead-options.test.ts +50 -0
  373. package/src/cli/commands/__tests__/init-governance-io.test.ts +139 -0
  374. package/src/cli/commands/__tests__/init-injection.test.ts +3 -3
  375. package/src/cli/commands/__tests__/init-ondisk.test.ts +867 -0
  376. package/src/cli/commands/__tests__/init.test.ts +82 -6
  377. package/src/cli/commands/__tests__/knowledge-audit-threshold.test.ts +146 -0
  378. package/src/cli/commands/__tests__/knowledge-flywheel-consistency.test.ts +1 -1
  379. package/src/cli/commands/__tests__/knowledge-tone-mapping.test.ts +244 -0
  380. package/src/cli/commands/__tests__/knowledge-view.test.ts +839 -0
  381. package/src/cli/commands/__tests__/knowledge.test.ts +33 -15
  382. package/src/cli/commands/__tests__/numeric-flag-assembly.test.ts +368 -0
  383. package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +89 -1
  384. package/src/cli/commands/__tests__/performance.test.ts +19 -21
  385. package/src/cli/commands/__tests__/posteval-plan.test.ts +5 -3
  386. package/src/cli/commands/__tests__/pre-push-hook.test.ts +181 -0
  387. package/src/cli/commands/__tests__/project-path-anchoring.test.ts +95 -2
  388. package/src/cli/commands/__tests__/project-path-convention.test.ts +103 -25
  389. package/src/cli/commands/__tests__/registry.test.ts +2 -0
  390. package/src/cli/commands/__tests__/report.test.ts +23 -1
  391. package/src/cli/commands/__tests__/review.test.ts +10 -17
  392. package/src/cli/commands/__tests__/scaffold.test.ts +505 -0
  393. package/src/cli/commands/__tests__/security-default-scan.test.ts +75 -0
  394. package/src/cli/commands/__tests__/security.test.ts +21 -24
  395. package/src/cli/commands/__tests__/spec-baseline-check.test.ts +3 -2
  396. package/src/cli/commands/__tests__/spec-baseline-index-cap.test.ts +131 -0
  397. package/src/cli/commands/__tests__/spec-baseline-read-count.test.ts +325 -0
  398. package/src/cli/commands/__tests__/sync-docs-agents.test.ts +19 -4
  399. package/src/cli/commands/__tests__/sync-docs-context-drift.test.ts +232 -0
  400. package/src/cli/commands/__tests__/sync-docs-module-order.test.ts +111 -0
  401. package/src/cli/commands/__tests__/sync-docs-read-count.test.ts +192 -0
  402. package/src/cli/commands/__tests__/sync-docs.test.ts +3 -2
  403. package/src/cli/commands/__tests__/validate.test.ts +1 -48
  404. package/src/cli/commands/acceptance.ts +24 -29
  405. package/src/cli/commands/check.ts +40 -17
  406. package/src/cli/commands/command.ts +9 -8
  407. package/src/cli/commands/constraints-retire.ts +57 -70
  408. package/src/cli/commands/contract.ts +44 -47
  409. package/src/cli/commands/definitions.ts +12 -6
  410. package/src/cli/commands/failure.ts +16 -3
  411. package/src/cli/commands/init.ts +242 -389
  412. package/src/cli/commands/knowledge-view.ts +224 -0
  413. package/src/cli/commands/knowledge.ts +643 -448
  414. package/src/cli/commands/passes-gate.ts +12 -3
  415. package/src/cli/commands/performance.ts +31 -38
  416. package/src/cli/commands/report.ts +3 -2
  417. package/src/cli/commands/review.ts +22 -28
  418. package/src/cli/commands/scaffold-templates.ts +406 -0
  419. package/src/cli/commands/scaffold.ts +302 -0
  420. package/src/cli/commands/security.ts +27 -23
  421. package/src/cli/commands/spec-baseline-check.ts +158 -33
  422. package/src/cli/commands/sync-docs/__tests__/context-export-surface.test.ts +102 -0
  423. package/src/cli/commands/sync-docs/__tests__/module-descriptions.test.ts +196 -0
  424. package/src/cli/commands/sync-docs/context-syncer.ts +137 -31
  425. package/src/cli/commands/sync-docs/index.ts +103 -28
  426. package/src/cli/commands/sync-docs/project-reader.ts +51 -7
  427. package/src/cli/commands/validate.ts +1 -104
  428. package/src/cli/gate-command.ts +87 -0
  429. package/src/completion-checkers/index.ts +10 -1
  430. package/src/context/CONTEXT.md +2 -1
  431. package/src/context/__tests__/knowledge-injector.test.ts +27 -0
  432. package/src/context/__tests__/session-manager.test.ts +19 -12
  433. package/src/context/knowledge-injector.ts +14 -7
  434. package/src/core/CONTEXT.md +10 -7
  435. package/src/core/__tests__/effective-set.test.ts +36 -60
  436. package/src/core/constraints/__tests__/context-reconcile.test.ts +246 -0
  437. package/src/core/constraints/__tests__/run-env.test.ts +377 -0
  438. package/src/core/constraints/__tests__/triggers.test.ts +70 -0
  439. package/src/core/constraints/agent-prompt-renderer.ts +1 -1
  440. package/src/core/constraints/capabilities-reconcile.ts +28 -4
  441. package/src/core/constraints/check-cache.ts +1 -1
  442. package/src/core/constraints/checker.ts +30 -14
  443. package/src/core/constraints/checkers/__tests__/context-doc-sync.test.ts +8 -3
  444. package/src/core/constraints/checkers/__tests__/governance-presence.test.ts +16 -7
  445. package/src/core/constraints/checkers/__tests__/no-hardcoded-credentials.test.ts +2 -1
  446. package/src/core/constraints/checkers/capability-sync.ts +27 -28
  447. package/src/core/constraints/checkers/context-doc-sync.ts +9 -7
  448. package/src/core/constraints/checkers/docs-freshness.ts +28 -33
  449. package/src/core/constraints/checkers/governance-presence.ts +10 -6
  450. package/src/core/constraints/checkers/types.ts +14 -8
  451. package/src/core/constraints/context-builder.ts +35 -26
  452. package/src/core/constraints/context-reconcile.ts +200 -0
  453. package/src/core/constraints/doc-freshness/__tests__/runner.test.ts +25 -0
  454. package/src/core/constraints/doc-freshness/runner.ts +18 -4
  455. package/src/core/constraints/git-evidence.ts +1 -1
  456. package/src/core/constraints/injection-drift.ts +19 -8
  457. package/src/core/constraints/run-env.ts +207 -0
  458. package/src/core/constraints/triggers.ts +33 -0
  459. package/src/core/effective-constraints.ts +17 -6
  460. package/src/core/effective-set.ts +8 -24
  461. package/src/core/index.ts +2 -1
  462. package/src/core/project-config-loader.ts +54 -102
  463. package/src/core/validators/__tests__/http-retry-sleep.test.ts +231 -0
  464. package/src/core/validators/check-handlers/http.ts +18 -7
  465. package/src/core/validators/index.ts +0 -1
  466. package/src/core/validators/passes-gate.ts +2 -3
  467. package/src/failure/CONTEXT.md +2 -1
  468. package/src/failure/classifier.ts +2 -2
  469. package/src/failure/index.ts +14 -2
  470. package/src/failure/recorder.ts +1 -1
  471. package/src/gates/CONTEXT.md +13 -7
  472. package/src/gates/__tests__/checker-gate.test.ts +1 -1
  473. package/src/gates/__tests__/command-single-predicate.test.ts +135 -0
  474. package/src/gates/__tests__/command.test.ts +8 -15
  475. package/src/gates/__tests__/contract.test.ts +5 -5
  476. package/src/gates/__tests__/gate-interface.test.ts +0 -17
  477. package/src/gates/__tests__/performance.test.ts +0 -13
  478. package/src/gates/__tests__/review.test.ts +0 -1
  479. package/src/gates/__tests__/runner.test.ts +1 -1
  480. package/src/gates/__tests__/security-default-scan.test.ts +97 -0
  481. package/src/gates/__tests__/security.test.ts +0 -13
  482. package/src/gates/checker-gate.ts +3 -2
  483. package/src/gates/command.ts +51 -74
  484. package/src/gates/contract.ts +0 -5
  485. package/src/gates/definitions.ts +6 -5
  486. package/src/gates/index.ts +0 -5
  487. package/src/gates/performance.ts +1 -6
  488. package/src/gates/runner.ts +1 -1
  489. package/src/gates/security.ts +9 -11
  490. package/src/gates/types.ts +20 -23
  491. package/src/hooks/CONTEXT.md +10 -9
  492. package/src/hooks/__tests__/bootstrap.test.ts +29 -6
  493. package/src/hooks/__tests__/pipeline.test.ts +28 -17
  494. package/src/hooks/__tests__/registry.test.ts +51 -3
  495. package/src/hooks/bootstrap.ts +35 -15
  496. package/src/hooks/config.ts +6 -2
  497. package/src/hooks/index.ts +10 -1
  498. package/src/hooks/pipeline.ts +4 -6
  499. package/src/hooks/registry.ts +31 -12
  500. package/src/hooks/types.ts +24 -7
  501. package/src/index.ts +1 -3
  502. package/src/knowledge/CONTEXT.md +14 -7
  503. package/src/knowledge/__tests__/audit-labels.test.ts +8 -5
  504. package/src/knowledge/__tests__/audit-scoring.test.ts +230 -0
  505. package/src/knowledge/__tests__/audit-write-count.test.ts +131 -0
  506. package/src/knowledge/__tests__/audit.test.ts +291 -208
  507. package/src/knowledge/__tests__/doctor.test.ts +3 -2
  508. package/src/knowledge/__tests__/ingest.test.ts +3 -2
  509. package/src/knowledge/__tests__/lifecycle.test.ts +57 -2
  510. package/src/knowledge/__tests__/migration.test.ts +35 -6
  511. package/src/knowledge/__tests__/query.test.ts +3 -2
  512. package/src/knowledge/__tests__/reference-tracker.test.ts +23 -2
  513. package/src/knowledge/__tests__/store.test.ts +149 -4
  514. package/src/knowledge/__tests__/tree-walker.test.ts +132 -0
  515. package/src/knowledge/audit-scoring.ts +589 -0
  516. package/src/knowledge/audit.ts +55 -599
  517. package/src/knowledge/import.ts +2 -0
  518. package/src/knowledge/index-generator.ts +4 -6
  519. package/src/knowledge/index.ts +27 -2
  520. package/src/knowledge/ingest.ts +1 -1
  521. package/src/knowledge/lifecycle.ts +5 -2
  522. package/src/knowledge/migration.ts +8 -2
  523. package/src/knowledge/query.ts +8 -1
  524. package/src/knowledge/reference-tracker.ts +5 -7
  525. package/src/knowledge/store.ts +75 -16
  526. package/src/knowledge/tree-walker.ts +29 -0
  527. package/src/knowledge/types.ts +19 -0
  528. package/src/monitoring/CONTEXT.md +5 -2
  529. package/src/monitoring/__tests__/context-tracker.test.ts +16 -8
  530. package/src/monitoring/__tests__/trace-file-anchoring.test.ts +80 -0
  531. package/src/monitoring/traces.ts +19 -5
  532. package/src/release/CONTEXT.md +1 -1
  533. package/src/release/__tests__/integrity.test.ts +21 -12
  534. package/src/test-setup/__tests__/jsonl-fake-fs.test.ts +98 -0
  535. package/src/test-setup/jsonl-fake-fs.ts +59 -0
  536. package/src/tools/index.ts +5 -1
  537. package/src/types/checkpoint.ts +6 -0
  538. package/src/types/passes-gate.ts +0 -17
  539. package/src/types/project-config.ts +21 -0
  540. package/src/types/trace.ts +14 -4
  541. package/src/utils/__tests__/file-walk.test.ts +3 -2
  542. package/src/utils/__tests__/jsonl-bounded-read.test.ts +106 -0
  543. package/src/utils/__tests__/jsonl-skip-disposition.test.ts +10 -6
  544. package/src/utils/__tests__/jsonl.test.ts +61 -14
  545. package/src/utils/__tests__/numeric-flag.test.ts +80 -0
  546. package/src/utils/__tests__/package-version.test.ts +2 -2
  547. package/src/utils/exec.ts +4 -64
  548. package/src/utils/jsonl.ts +159 -20
  549. package/src/utils/numeric-flag.ts +64 -0
  550. package/dist/failure/types.d.ts +0 -9
  551. package/dist/failure/types.d.ts.map +0 -1
  552. package/dist/failure/types.js +0 -25
  553. package/dist/failure/types.js.map +0 -1
  554. package/dist/gates/effective-gates.d.ts +0 -30
  555. package/dist/gates/effective-gates.d.ts.map +0 -1
  556. package/dist/gates/effective-gates.js +0 -81
  557. package/dist/gates/effective-gates.js.map +0 -1
  558. package/src/failure/types.ts +0 -9
  559. package/src/gates/__tests__/effective-gates.test.ts +0 -77
  560. package/src/gates/effective-gates.ts +0 -99
@@ -0,0 +1,839 @@
1
+ /**
2
+ * knowledge 投影收口闸(harness#133,架构评审候选4)
3
+ *
4
+ * knowledge 的 11 个子操作原先各自手写「json 投影 + 人读排版」两遍,同一个数据
5
+ * 两处落地、字段名靠人对齐。本文件钉住收口后的四件事:
6
+ *
7
+ * 1. **源形状闸**:`knowledge.ts` 不再出现 chalk / store 构造 / JSON.stringify,
8
+ * `--json` 分支只余 audit 索引重建一处策略豁免(逐行冻结);上色与分派的唯一落点在
9
+ * `knowledge-view.ts`(store 构造与 JSON.stringify 各恰好一处)。
10
+ * 2. **json 形状冻结**:11/11 子操作的字段清单逐条对撞(新增/删字段须显式改清单)。
11
+ * 3. **两投影一致性**:同一 fixture 下,display model 里声明的 `field` 锚点必须能在
12
+ * json 面解析到;json 面没有的派生量必须走 `derived` 并在此登记豁免理由(无化石条目)。
13
+ * 4. **人读视图逐行冻结**:改造前的整屏输出逐行留档,防止收口顺带改文案。
14
+ */
15
+
16
+ import * as fs from 'fs';
17
+ import * as os from 'os';
18
+ import * as path from 'path';
19
+
20
+ // Mock chalk(人读断言看的是文本,不看 ANSI)
21
+ jest.mock('chalk', () => ({
22
+ blue: jest.fn((s: string) => s),
23
+ bold: jest.fn((s: string) => s),
24
+ green: jest.fn((s: string) => s),
25
+ yellow: jest.fn((s: string) => s),
26
+ red: jest.fn((s: string) => s),
27
+ gray: jest.fn((s: string) => s),
28
+ cyan: jest.fn((s: string) => s),
29
+ }));
30
+
31
+ import { captureIO, lastJsonOutput, type CapturingIO } from '../../command-contract';
32
+ import { FileKnowledgeStore } from '../../../knowledge/store';
33
+ import type { KnowledgeEntry } from '../../../knowledge/types';
34
+ import {
35
+ knowledgeList, knowledgeSearch, knowledgeImport, knowledgeDecay, knowledgeStats,
36
+ knowledgeSyncRag, knowledgeAudit, knowledgeSnapshot, knowledgeMigrate, knowledgeIndex, knowledgeHealth,
37
+ knowledgeListView, knowledgeSearchView, knowledgeImportView, knowledgeDecayView, knowledgeStatsView,
38
+ knowledgeSyncRagView, knowledgeAuditView, knowledgeSnapshotView, knowledgeMigrateView,
39
+ knowledgeIndexView, knowledgeHealthView,
40
+ } from '../knowledge';
41
+ import { emitKnowledgeView, toneForMaturity, type DisplayCell, type DisplayModel, type KnowledgeView } from '../knowledge-view';
42
+ import type { MaturityLevel } from '../../../knowledge/types';
43
+
44
+ const SRC_DIR = path.join(__dirname, '..');
45
+ const DAY = 24 * 60 * 60 * 1000;
46
+ const daysAgo = (n: number): string => new Date(Date.now() - n * DAY).toISOString();
47
+
48
+ // ========================================
49
+ // fixture:3 条知识 + 消费统计 + 2 个 RAG 文档 + README(导入源)
50
+ // ========================================
51
+
52
+ function makeEntry(id: string, overrides: Partial<KnowledgeEntry> = {}): KnowledgeEntry {
53
+ return {
54
+ id,
55
+ type: 'guideline',
56
+ title: `条目 ${id}`,
57
+ content: `这是 ${id} 的正文,包含 autolink 关键词,长度足够避开短内容规则所以问题数可控。`,
58
+ maturity: 'verified',
59
+ layer: 'project',
60
+ created: daysAgo(40),
61
+ lastReferenced: daysAgo(5),
62
+ contributors: ['human'],
63
+ projects: ['harness'],
64
+ tags: ['demo'],
65
+ applicablePhases: [],
66
+ sourceReferences: [],
67
+ referencedBy: ['agent-a:2026-09-01'],
68
+ executionResults: [],
69
+ consumptionMode: 'reference',
70
+ origin: 'agent',
71
+ ...overrides,
72
+ } as KnowledgeEntry;
73
+ }
74
+
75
+ /** 每次调用新建一份临时知识库(部分子命令会写盘,fixture 之间必须互不影响) */
76
+ function fixture(): string {
77
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-kview-'));
78
+ const baseDir = path.join(root, '.harness', 'knowledge');
79
+ const store = new FileKnowledgeStore({ baseDir });
80
+ store.save(makeEntry('K-001', { maturity: 'proven', tags: [] }));
81
+ store.save(makeEntry('K-002', { maturity: 'verified' }));
82
+ store.save(makeEntry('K-003', { maturity: 'draft', lastReferenced: '', referencedBy: [], created: daysAgo(200) }));
83
+ fs.writeFileSync(path.join(baseDir, '.consumption-stats.json'), JSON.stringify({ date: '2026-09-02', dailyEvents: 2, searchHits: 1 }));
84
+ const docsDir = path.join(root, '.harness', 'knowledge-docs');
85
+ fs.mkdirSync(docsDir, { recursive: true });
86
+ fs.writeFileSync(path.join(docsDir, 'a.md'), '# a\n');
87
+ fs.writeFileSync(path.join(docsDir, 'b.md'), '# b\n');
88
+ fs.writeFileSync(path.join(root, 'README.md'), '# readme\n\n导入用的说明文档,内容足够长以生成一条知识条目记录。\n');
89
+ return root;
90
+ }
91
+
92
+ /** 易变量归一:临时根、ISO 时间戳、快照文件名里的日期 */
93
+ function normalize(root: string, text: string): string {
94
+ return text
95
+ .split(root).join('<TMP>')
96
+ .replace(/\d{4}-\d{2}-\d{2}T[\d:.]+Z/g, '<TS>')
97
+ .replace(/\d{4}-\d{2}-\d{2}/g, '<DATE>');
98
+ }
99
+
100
+ /** 11 个子命令的驱动面:builder(拿两投影)+ command(拿真实 stdout) */
101
+ type SubName =
102
+ | 'list' | 'search' | 'import' | 'decay' | 'stats' | 'sync-rag'
103
+ | 'audit' | 'snapshot' | 'migrate' | 'index' | 'health';
104
+
105
+ interface SubDriver {
106
+ name: SubName;
107
+ build(root: string, out: CapturingIO): Promise<KnowledgeView> | KnowledgeView;
108
+ run(root: string, out: CapturingIO, json: boolean): Promise<unknown> | unknown;
109
+ }
110
+
111
+ const SUBS: SubDriver[] = [
112
+ {
113
+ name: 'list',
114
+ build: (r, out) => knowledgeListView({ projectPath: r }, out),
115
+ run: (r, out, json) => knowledgeList({ projectPath: r, json }, out),
116
+ },
117
+ {
118
+ name: 'search',
119
+ build: (r, out) => knowledgeSearchView('autolink', { projectPath: r, limit: 20 }, out),
120
+ run: (r, out, json) => knowledgeSearch('autolink', { projectPath: r, json, limit: 20 }, out),
121
+ },
122
+ {
123
+ name: 'import',
124
+ build: (r, out) => knowledgeImportView({ projectPath: r, sources: 'docs,bogus' }, out),
125
+ run: (r, out, json) => knowledgeImport({ projectPath: r, json, sources: 'docs,bogus' }, out),
126
+ },
127
+ {
128
+ name: 'decay',
129
+ build: (r, out) => knowledgeDecayView({ projectPath: r }, out),
130
+ run: (r, out, json) => knowledgeDecay({ projectPath: r, json }, out),
131
+ },
132
+ {
133
+ name: 'stats',
134
+ build: (r, out) => knowledgeStatsView({ projectPath: r }, out),
135
+ run: (r, out, json) => knowledgeStats({ projectPath: r, json }, out),
136
+ },
137
+ {
138
+ name: 'sync-rag',
139
+ build: r => knowledgeSyncRagView({ projectPath: r }),
140
+ run: (r, out, json) => knowledgeSyncRag({ projectPath: r, json }, out),
141
+ },
142
+ {
143
+ name: 'audit',
144
+ build: (r, out) => knowledgeAuditView({ projectPath: r }, out),
145
+ run: (r, out, json) => knowledgeAudit({ projectPath: r, json }, out),
146
+ },
147
+ {
148
+ name: 'snapshot',
149
+ build: (r, out) => knowledgeSnapshotView({ projectPath: r }, out),
150
+ run: (r, out, json) => knowledgeSnapshot({ projectPath: r, json }, out),
151
+ },
152
+ {
153
+ name: 'migrate',
154
+ build: (r, out) => knowledgeMigrateView({ projectPath: r }, out),
155
+ run: (r, out, json) => knowledgeMigrate({ projectPath: r, json }, out),
156
+ },
157
+ {
158
+ name: 'index',
159
+ build: (r, out) => knowledgeIndexView({ projectPath: r }, out),
160
+ run: (r, out, json) => knowledgeIndex({ projectPath: r, json }, out),
161
+ },
162
+ {
163
+ name: 'health',
164
+ build: (r, out) => knowledgeHealthView({ projectPath: r }, out),
165
+ run: (r, out, json) => knowledgeHealth({ projectPath: r, json }, out),
166
+ },
167
+ ];
168
+
169
+ // ========================================
170
+ // display model 遍历面
171
+ // ========================================
172
+
173
+ export function collectCells(model: DisplayModel): DisplayCell[] {
174
+ return model.sections.flatMap(s => s.rows.flatMap(r => r.cells));
175
+ }
176
+
177
+ /** 点分路径解析;数组支持 `.length` 与下标 */
178
+ function resolvePath(root: unknown, dotted: string): { found: boolean; value?: unknown } {
179
+ const parts = dotted.split('.');
180
+ let cursor: unknown = root;
181
+ for (const part of parts) {
182
+ if (part === 'length' && Array.isArray(cursor)) { cursor = cursor.length; continue; }
183
+ if (cursor === null || typeof cursor !== 'object') return { found: false };
184
+ if (!(part in (cursor as Record<string, unknown>))) return { found: false };
185
+ cursor = (cursor as Record<string, unknown>)[part];
186
+ }
187
+ return { found: true, value: cursor };
188
+ }
189
+
190
+ /** 一致性违例:field 锚点在 json 面解析不到,或派生量未登记 */
191
+ function anchorViolations(data: unknown, model: DisplayModel, registeredDerived: string[]): string[] {
192
+ const violations: string[] = [];
193
+ for (const cell of collectCells(model)) {
194
+ if ('field' in cell) {
195
+ if (!resolvePath(data, cell.field).found) violations.push(`field 不在 json 面: ${cell.field}`);
196
+ } else if ('derived' in cell) {
197
+ if (!registeredDerived.includes(cell.derived)) violations.push(`派生量未登记豁免: ${cell.derived}`);
198
+ }
199
+ }
200
+ return violations;
201
+ }
202
+
203
+ /** 被用到的派生量键(用于反向查化石豁免) */
204
+ function usedDerived(model: DisplayModel): string[] {
205
+ return collectCells(model)
206
+ .filter((c): c is Extract<DisplayCell, { derived: string }> => 'derived' in c)
207
+ .map(c => c.derived);
208
+ }
209
+
210
+ /**
211
+ * 派生量豁免登记(验收 2 的「显式登记理由」):人读面呈现、但 json 面按现状冻结未暴露的量。
212
+ * 按**源码**核对而不是按 fixture 跑到的分支——溢出计数这类分支 fixture 够不着,
213
+ * 但源码里再没有该派生量时,豁免同样该跟着删(无化石)。
214
+ */
215
+ const DERIVED_EXEMPTIONS: Record<string, string> = {
216
+ activeCount:
217
+ 'stats 的人读面历史上就多给出一个「活跃」(非 archived)条目数,json 面只有 total。' +
218
+ '#133 冻结 json 现状不新增字段,故该派生量登记豁免。',
219
+ healthIssuesOverflow:
220
+ 'health 人读面的「... 还有 N 条」取全量 issues 数组,而 json 面 issues 截前 50 条——' +
221
+ '锚 issues.length 会指向被截断的另一个数,故按派生量登记。',
222
+ };
223
+
224
+ function keysAt(parsed: unknown, dotted: string): string[] {
225
+ const hit = dotted === '' ? { found: true, value: parsed } : resolvePath(parsed, dotted);
226
+ if (!hit.found || hit.value === null || typeof hit.value !== 'object') {
227
+ throw new Error(`json 路径解析失败: ${dotted || '<root>'}`);
228
+ }
229
+ const target = Array.isArray(hit.value) ? hit.value[0] : hit.value;
230
+ if (target === null || typeof target !== 'object') throw new Error(`json 路径不是对象: ${dotted}`);
231
+ return Object.keys(target).sort();
232
+ }
233
+
234
+ /**
235
+ * KnowledgeEntry 序列化后的 json 键清单(fixture 未设置任何可选键)。
236
+ * 条目类型新增字段 → store 输出多出键 → 本清单对不上即红。
237
+ */
238
+ const ENTRY_KEYS = [
239
+ 'id', 'type', 'title', 'content', 'maturity', 'layer', 'created', 'lastReferenced',
240
+ 'contributors', 'projects', 'tags', 'applicablePhases', 'sourceReferences', 'referencedBy',
241
+ 'executionResults', 'consumptionMode', 'origin',
242
+ ];
243
+
244
+ /**
245
+ * json 字段清单冻结(验收 1):11/11 子操作,逐条点名。
246
+ * 路径 '' = 顶层;'entries.0' = 数组首元素的键。比较按排序后的集合。
247
+ */
248
+ const JSON_FIELD_FREEZE: Record<SubName, Array<{ path: string; keys: string[] }>> = {
249
+ list: [
250
+ { path: '', keys: ['total', 'entries'] },
251
+ { path: 'entries.0', keys: [...ENTRY_KEYS] },
252
+ ],
253
+ search: [
254
+ { path: '', keys: ['query', 'total', 'entries'] },
255
+ { path: 'entries.0', keys: [...ENTRY_KEYS] },
256
+ ],
257
+ import: [
258
+ { path: '', keys: ['totalImported', 'totalErrors', 'results'] },
259
+ { path: 'results.0', keys: ['source', 'entries', 'errors'] },
260
+ ],
261
+ decay: [
262
+ { path: '', keys: ['changes'] },
263
+ { path: 'changes.0', keys: ['entryId', 'from', 'to', 'reason'] },
264
+ ],
265
+ stats: [
266
+ { path: '', keys: ['total', 'byType', 'byMaturity', 'byLayer', 'flywheel'] },
267
+ { path: 'flywheel', keys: ['refCoverage', 'avgRefs', 'consumptionHitRate'] },
268
+ ],
269
+ 'sync-rag': [
270
+ { path: '', keys: ['directory', 'files'] },
271
+ ],
272
+ audit: [
273
+ { path: '', keys: ['timestamp', 'totalEntries', 'issues', 'summary', 'dimensions', 'autoFixed', 'healthScore'] },
274
+ { path: 'issues.0', keys: ['rule', 'entryId', 'title', 'severity', 'action', 'detail'] },
275
+ { path: 'summary', keys: [
276
+ 'frontmatter-missing', 'test-data-pollution', 'daily-audit-noise', 'event-noise', 'zero-content-proven',
277
+ 'maturity-inflation', 'short-content', 'title-duplicate', 'source-refs-bloat', 'fragment-cluster',
278
+ 'promotion-blocked', 'orphan-draft', 'stale-entry', 'deprecated-domain',
279
+ ] },
280
+ { path: 'dimensions', keys: ['structure', 'content', 'dedup', 'maturity', 'freshness', 'flywheel', 'incremental'] },
281
+ { path: 'healthScore', keys: ['before', 'after'] },
282
+ ],
283
+ snapshot: [
284
+ { path: '', keys: ['snapshotPath'] },
285
+ ],
286
+ migrate: [
287
+ { path: '', keys: ['total', 'migrated', 'skipped', 'errors'] },
288
+ ],
289
+ index: [
290
+ { path: '', keys: ['path', 'entries', 'size', 'previousSize'] },
291
+ ],
292
+ health: [
293
+ { path: '', keys: ['healthScore', 'summary', 'issues'] },
294
+ { path: 'summary', keys: ['total', 'lowRefEntries', 'staleEntries', 'consumptionData', 'refCoverage', 'avgRefs'] },
295
+ { path: 'issues.0', keys: ['severity', 'entry', 'detail'] },
296
+ ],
297
+ };
298
+
299
+ /**
300
+ * 人读视图逐行冻结(改造前基线,2026-09-15 实测捕获)。
301
+ * 收口只搬排版、不改文案:任一行漂移即红。
302
+ */
303
+ const HUMAN_BASELINE: Record<SubName, string[]> = {
304
+ list: [
305
+ '📚 知识库 (3 条)',
306
+ '',
307
+ ' [proven] 条目 K-001',
308
+ ' id: K-001 | type: guideline | layer: project',
309
+ ' [verified] 条目 K-002',
310
+ ' id: K-002 | type: guideline | layer: project',
311
+ ' tags: demo',
312
+ ' [draft] 条目 K-003',
313
+ ' id: K-003 | type: guideline | layer: project',
314
+ ' tags: demo',
315
+ ],
316
+ search: [
317
+ '🔍 搜索 "autolink" (3 条结果)',
318
+ '',
319
+ ' 条目 K-001 [proven]',
320
+ ' 这是 K-001 的正文,包含 autolink 关键词,长度足够避开短内容规则所以问题数可控。',
321
+ ' 条目 K-002 [verified]',
322
+ ' 这是 K-002 的正文,包含 autolink 关键词,长度足够避开短内容规则所以问题数可控。',
323
+ ' 条目 K-003 [draft]',
324
+ ' 这是 K-003 的正文,包含 autolink 关键词,长度足够避开短内容规则所以问题数可控。',
325
+ ],
326
+ import: [
327
+ '📥 开始导入知识 (源: docs, bogus)...',
328
+ ' ✅ docs: 1 条',
329
+ ' ❌ bogus: 未知源类型: bogus',
330
+ '',
331
+ '✅ 导入完成: 1 条,1 个错误',
332
+ ],
333
+ decay: [
334
+ '🔄 运行衰减周期...',
335
+ '📉 1 条知识发生衰减:',
336
+ '',
337
+ ' K-003: draft → archived',
338
+ ' Auto-decay: draft → archived (unreferenced for threshold)',
339
+ ],
340
+ stats: [
341
+ '📊 知识库统计',
342
+ '',
343
+ ' 总计: 3 条 (活跃: 3)',
344
+ '',
345
+ ' 按类型:',
346
+ ' guideline: 3',
347
+ '',
348
+ ' 按成熟度:',
349
+ ' proven: 1',
350
+ ' verified: 1',
351
+ ' draft: 1',
352
+ '',
353
+ ' 按层级:',
354
+ ' project: 3',
355
+ '',
356
+ ' 飞轮指标:',
357
+ ' 引用覆盖: 67%',
358
+ ' 平均引用: 0.7',
359
+ ' 消费命中率: 67%',
360
+ ],
361
+ 'sync-rag': [
362
+ '📄 RAG sync candidates: 2 files in <TMP>/.harness/knowledge-docs',
363
+ '',
364
+ ' a.md',
365
+ ' b.md',
366
+ '',
367
+ 'Run `mcp__local-rag__ingest_file` for each to sync to RAG',
368
+ ],
369
+ audit: [
370
+ '🔍 知识库质量审计...',
371
+ '',
372
+ ' 总条目: 3',
373
+ ' 健康分: 67/100',
374
+ '',
375
+ ' 维度评分:',
376
+ ' D1 结构完整性: 100/100 (0 问题)',
377
+ ' D2 内容质量: 0/100 (3 问题)',
378
+ ' D3 去重有效性: 100/100 (0 问题)',
379
+ ' D4 成熟度健康: 0/100 (1 问题)',
380
+ ' D5 新鲜度: 67/100 (1 问题)',
381
+ ' D6 飞轮验证: 56/100 (0 问题)',
382
+ ' D7 增量存活: 100/100 (0 问题)',
383
+ '',
384
+ ' 短内容: 3',
385
+ ' promotion 受阻: 1',
386
+ ' 过期条目: 1',
387
+ '',
388
+ ' 问题详情 (前 20 条):',
389
+ '',
390
+ ' [medium] K-001: 条目 K-001',
391
+ ' 内容 48 字符 (阈值 50) → flag',
392
+ ' [medium] K-002: 条目 K-002',
393
+ ' 内容 48 字符 (阈值 50) → flag',
394
+ ' [medium] K-003: 条目 K-003',
395
+ ' 内容 48 字符 (阈值 50) → flag',
396
+ ' [medium] K-003: 条目 K-003',
397
+ ' draft 已 200 天未被引用 → flag',
398
+ ' [medium] K-003: 条目 K-003',
399
+ ' 超过 200 天未引用 (阈值 90) → flag',
400
+ '',
401
+ ' 使用 --fix 自动修复',
402
+ ' 📇 索引已重建',
403
+ ],
404
+ snapshot: [
405
+ '✅ 快照已保存: <TMP>/.harness/knowledge/.snapshots/index-<DATE>.json',
406
+ ],
407
+ migrate: [
408
+ '🔄 迁移知识条目(添加 consumptionMode/origin 字段)',
409
+ '',
410
+ ' 总计: 3 条',
411
+ ' 已迁移: 0 条',
412
+ ' 已跳过: 3 条',
413
+ '',
414
+ '✅ 所有条目已是最新,无需迁移',
415
+ ],
416
+ index: [
417
+ '📇 索引已重建',
418
+ ' 路径: <TMP>/.harness/knowledge/_index.md',
419
+ ' 条目: 4',
420
+ ' 大小: 0.5 KB',
421
+ ],
422
+ health: [
423
+ '🏥 飞轮健康检查',
424
+ '',
425
+ ' 健康分: 99/100',
426
+ ' 活跃条目: 3',
427
+ '',
428
+ ' 数据流状态:',
429
+ ' 引用密度: ✓',
430
+ ' 新鲜度: ⚠ 1 个过期 draft',
431
+ ' 消费追踪: ✓',
432
+ '',
433
+ ' 飞轮指标:',
434
+ ' 引用覆盖: 67%',
435
+ ' 平均引用: 0.7',
436
+ '',
437
+ ' 问题 (前 20):',
438
+ ' [warn] K-003: draft 超过 90 天未推进',
439
+ ],
440
+ };
441
+
442
+ // ========================================
443
+ // 用例
444
+ // ========================================
445
+
446
+ let io: CapturingIO;
447
+ beforeEach(() => {
448
+ io = captureIO();
449
+ });
450
+
451
+ describe('闸 1:源形状——投影分派与上色只有一处', () => {
452
+ const knowledgeSource = fs.readFileSync(path.join(SRC_DIR, 'knowledge.ts'), 'utf-8').split('\n');
453
+ const viewSource = fs.readFileSync(path.join(SRC_DIR, 'knowledge-view.ts'), 'utf-8').split('\n');
454
+ const code = (lines: string[]): string[] =>
455
+ lines.map(l => l.trim()).filter(l => !l.startsWith('*') && !l.startsWith('//') && !l.startsWith('/*'));
456
+
457
+ it('knowledge.ts 不含 chalk / store 构造 / JSON.stringify', () => {
458
+ const lines = code(knowledgeSource);
459
+ expect(lines.filter(l => l.includes("from 'chalk'"))).toEqual([]);
460
+ expect(lines.filter(l => l.includes('chalk.'))).toEqual([]);
461
+ expect(lines.filter(l => /new (File)?KnowledgeStore\(/.test(l))).toEqual([]);
462
+ expect(lines.filter(l => l.includes('JSON.stringify'))).toEqual([]);
463
+ });
464
+
465
+ it('knowledge.ts 的退出码返回值只余冻结的豁免行(投影出口由 emit 单点给)', () => {
466
+ expect(code(knowledgeSource).filter(l => l.includes('kind:'))).toEqual([
467
+ "return { kind: 'usage-error', reason: 'knowledge search 缺少关键词位置参数' };",
468
+ // #154:search --limit 脏输入在装配点 fail-loud(原先 parseInt 出 NaN 被 `|| 20`
469
+ // 静默兜成缺省量)。豁免理由同上——入参闸门而非投影出口。
470
+ "return { kind: 'usage-error', reason: `knowledge search --limit 非法限制: \"${limit.raw}\"` };",
471
+ // #152:--threshold 脏输入在装配点 fail-loud(原先 parseInt 出 NaN 静默关掉短内容判定)。
472
+ // 豁免理由同 search 那条——入参闸门而非投影出口,取值/排版仍只在 emit 一处。
473
+ "return { kind: 'usage-error', reason: `knowledge audit --threshold 非法阈值: \"${threshold.raw}\"` };",
474
+ ]);
475
+ });
476
+
477
+ it('knowledge.ts 的 json 分支判断只余冻结的豁免行(逐行点名,行变形即红)', () => {
478
+ expect(code(knowledgeSource).filter(l => /^(else )?if\s*\(\s*!?options\.json\b/.test(l))).toEqual([
479
+ 'if (!options.json) {', // audit 的索引重建策略,理由见本目录 CONTEXT.md「knowledge view 收口模型」
480
+ ]);
481
+ });
482
+
483
+ it('knowledge-view.ts 是 store 构造与 json 投影的唯一落点', () => {
484
+ const lines = code(viewSource);
485
+ expect(lines.filter(l => /new FileKnowledgeStore\(/.test(l))).toHaveLength(1);
486
+ expect(lines.filter(l => l.includes('JSON.stringify'))).toHaveLength(1);
487
+ expect(lines.filter(l => l.includes('import chalk'))).toHaveLength(1);
488
+ });
489
+
490
+ it('view 模块的 chalk 调用只准出现在角色→样式的单张表里', () => {
491
+ const sites = code(viewSource).filter(l => l.includes('chalk.'));
492
+ for (const line of sites) {
493
+ expect(line).toMatch(/^\w+: chalk\.\w+,$/);
494
+ }
495
+ expect(sites.length).toBeGreaterThan(0);
496
+ });
497
+ });
498
+
499
+ describe('闸 2:11/11 子操作 --json 字段清单冻结', () => {
500
+ for (const sub of SUBS) {
501
+ it(`${sub.name} 的 json 字段清单 = 冻结清单`, async () => {
502
+ const root = fixture();
503
+ try {
504
+ await sub.run(root, io, true);
505
+ const parsed = JSON.parse(io.outText());
506
+ for (const entry of JSON_FIELD_FREEZE[sub.name]) {
507
+ expect({ path: entry.path, keys: keysAt(parsed, entry.path) })
508
+ .toEqual({ path: entry.path, keys: [...entry.keys].sort() });
509
+ }
510
+ } finally {
511
+ fs.rmSync(root, { recursive: true, force: true });
512
+ }
513
+ });
514
+ }
515
+
516
+ it('冻结清单本身覆盖 11 个子命令(漏登记即红)', () => {
517
+ expect(Object.keys(JSON_FIELD_FREEZE).sort()).toEqual(SUBS.map(s => s.name).sort());
518
+ });
519
+ });
520
+
521
+ describe('闸 3:两投影一致性(同一 fixture)', () => {
522
+ for (const sub of SUBS) {
523
+ it(`${sub.name}:display model 声明的字段锚点全部在 json 面,派生量全部登记`, async () => {
524
+ const root = fixture();
525
+ try {
526
+ const view = await sub.build(root, io);
527
+ const model = view.human();
528
+ expect(anchorViolations(view.data, model, Object.keys(DERIVED_EXEMPTIONS))).toEqual([]);
529
+ for (const derived of usedDerived(model)) {
530
+ expect(DERIVED_EXEMPTIONS[derived]).toBeTruthy();
531
+ }
532
+ } finally {
533
+ fs.rmSync(root, { recursive: true, force: true });
534
+ }
535
+ });
536
+
537
+ it(`${sub.name}:字段锚点在真实 --json 输出里同样解析得到`, async () => {
538
+ const root = fixture();
539
+ try {
540
+ // builder 会就地播报进度行,用独立 io 与之隔离
541
+ const view = await sub.build(root, captureIO());
542
+ const anchored = collectCells(view.human())
543
+ .filter((c): c is Extract<DisplayCell, { field: string }> => 'field' in c)
544
+ .map(c => c.field);
545
+ expect(anchored.length).toBeGreaterThan(0);
546
+ const fresh = fixture();
547
+ try {
548
+ await sub.run(fresh, io, true);
549
+ const parsed = JSON.parse(io.outText());
550
+ for (const field of anchored) {
551
+ expect({ field, found: resolvePath(parsed, field).found }).toEqual({ field, found: true });
552
+ }
553
+ } finally {
554
+ fs.rmSync(fresh, { recursive: true, force: true });
555
+ }
556
+ } finally {
557
+ fs.rmSync(root, { recursive: true, force: true });
558
+ }
559
+ });
560
+ }
561
+
562
+ it('emit 的 json 面就是 data 的序列化:不加壳、不重排、不删键', () => {
563
+ const data = { b: 1, a: 'x', nested: { list: [1, 2] } };
564
+ const jsonIo = captureIO();
565
+ const result = emitKnowledgeView(jsonIo, { json: true }, { data, human: () => ({ sections: [] }) });
566
+ expect(jsonIo.outText()).toBe(JSON.stringify(data, null, 2) + '\n');
567
+ expect(result).toEqual({ kind: 'ok' });
568
+ });
569
+
570
+ it('派生量豁免无化石:双向对撞源码里的 derived 声明与登记表', () => {
571
+ const source = fs.readFileSync(path.join(SRC_DIR, 'knowledge.ts'), 'utf-8');
572
+ const declared = [...source.matchAll(/derived: '([a-zA-Z]+)'/g)].map(m => m[1]);
573
+ expect(declared.length).toBeGreaterThan(0);
574
+ expect([...new Set(declared)].sort()).toEqual(Object.keys(DERIVED_EXEMPTIONS).sort());
575
+ });
576
+
577
+ it('反证探针:编造的字段锚点与未登记派生量都能被本闸抓到', () => {
578
+ const data = { real: { nested: 1 }, list: [1, 2] };
579
+ const fake: DisplayModel = {
580
+ sections: [{
581
+ rows: [
582
+ { cells: [{ field: 'real.nested', text: '1' }, { field: 'nope.missing', text: 'x' }] },
583
+ { cells: [{ field: 'list.length', text: '2' }, { derived: 'notRegistered', text: 'y' }] },
584
+ { cells: [{ label: '纯文案无需锚点' }] },
585
+ ],
586
+ }],
587
+ };
588
+ expect(anchorViolations(data, fake, ['activeCount'])).toEqual([
589
+ 'field 不在 json 面: nope.missing',
590
+ '派生量未登记豁免: notRegistered',
591
+ ]);
592
+ expect(resolvePath(data, 'real.nested').found).toBe(true);
593
+ expect(resolvePath(data, 'nope.missing').found).toBe(false);
594
+ });
595
+ });
596
+
597
+ describe('stats 成熟度格的 tone 传递(harness#158,复审 M7 假闸补钉)', () => {
598
+ // knowledge-tone-mapping.test.ts 钉住了 toneForMaturity 表与渲染链,但没钉「命令侧真的把
599
+ // tone 传进格」——把 knowledge.ts 成熟度格的 `tone:` 摘掉,全仓零红。此处看结构不看 ANSI,
600
+ // 不违背本文件 identity-mock 的立场。
601
+ it('按成熟度段每行首格必须携带 tone 且 = toneForMaturity(maturity)', async () => {
602
+ const root = fixture();
603
+ try {
604
+ const view = await knowledgeStatsView({ projectPath: root }, captureIO());
605
+ const section = view.human().sections.find(s => s.title === ' 按成熟度:');
606
+ expect(section).toBeDefined();
607
+ const rows = section!.rows.filter(r => r.cells.some(c => 'field' in c));
608
+ expect(rows.length).toBeGreaterThan(0);
609
+ for (const row of rows) {
610
+ const cell = row.cells[0];
611
+ expect('field' in cell).toBe(true);
612
+ const field = (cell as { field: string }).field;
613
+ expect(field.startsWith('byMaturity.')).toBe(true);
614
+ const maturity = field.slice('byMaturity.'.length) as MaturityLevel;
615
+ expect({ maturity, tone: (cell as { tone?: string }).tone })
616
+ .toEqual({ maturity, tone: toneForMaturity(maturity) });
617
+ }
618
+ } finally {
619
+ fs.rmSync(root, { recursive: true, force: true });
620
+ }
621
+ });
622
+ });
623
+
624
+ describe('闸 4:人读视图逐行冻结(改造前基线)', () => {
625
+ for (const sub of SUBS) {
626
+ it(`${sub.name} 的人读输出逐行不变`, async () => {
627
+ const root = fixture();
628
+ const humanIo = captureIO();
629
+ try {
630
+ await sub.run(root, humanIo, false);
631
+ expect(humanIo.outLines().map(l => normalize(root, l))).toEqual(HUMAN_BASELINE[sub.name]);
632
+ } finally {
633
+ fs.rmSync(root, { recursive: true, force: true });
634
+ }
635
+ });
636
+ }
637
+ });
638
+
639
+ /**
640
+ * 空知识库态(验收 1 的「每个子操作都有形状断言」的另一半:零条目分支正是两投影
641
+ * 历史上最容易分叉的地方)。以下逐行基线同样取自改造前的实现。
642
+ */
643
+ const EMPTY_HUMAN_BASELINE: Record<SubName, string[]> = {
644
+ list: ['知识库为空'],
645
+ search: ['未找到匹配 "autolink" 的知识条目'],
646
+ import: ['📥 开始导入知识 (源: docs, bogus)...', ' ❌ bogus: 未知源类型: bogus', '', '✅ 导入完成: 0 条,1 个错误'],
647
+ decay: ['🔄 运行衰减周期...', '✅ 没有需要衰减的知识条目'],
648
+ stats: [
649
+ '📊 知识库统计',
650
+ '',
651
+ ' 总计: 0 条 (活跃: 0)',
652
+ '',
653
+ ' 按类型:',
654
+ '',
655
+ ' 按成熟度:',
656
+ '',
657
+ ' 按层级:',
658
+ '',
659
+ ' 飞轮指标:',
660
+ ' 引用覆盖: 0%',
661
+ ' 平均引用: 0',
662
+ ' 消费命中率: 0%',
663
+ ],
664
+ 'sync-rag': ['No .harness/knowledge-docs/ directory'],
665
+ audit: [
666
+ '🔍 知识库质量审计...',
667
+ '',
668
+ ' 总条目: 0',
669
+ ' 健康分: 100/100',
670
+ '',
671
+ ' 维度评分:',
672
+ ' D1 结构完整性: 100/100 (0 问题)',
673
+ ' D2 内容质量: 100/100 (0 问题)',
674
+ ' D3 去重有效性: 100/100 (0 问题)',
675
+ ' D4 成熟度健康: 100/100 (0 问题)',
676
+ ' D5 新鲜度: 100/100 (0 问题)',
677
+ ' D6 飞轮验证: 0/100 (0 问题)',
678
+ ' D7 增量存活: 100/100 (0 问题)',
679
+ '',
680
+ ' 📇 索引已重建',
681
+ ],
682
+ snapshot: ['✅ 快照已保存: <TMP>/.harness/knowledge/.snapshots/index-<DATE>.json'],
683
+ migrate: [
684
+ '🔄 迁移知识条目(添加 consumptionMode/origin 字段)',
685
+ '',
686
+ ' 总计: 0 条',
687
+ ' 已迁移: 0 条',
688
+ ' 已跳过: 0 条',
689
+ '',
690
+ '✅ 所有条目已是最新,无需迁移',
691
+ ],
692
+ index: [
693
+ '📇 索引已重建',
694
+ ' 路径: <TMP>/.harness/knowledge/_index.md',
695
+ ' 条目: 2',
696
+ ' 大小: 0.3 KB',
697
+ ],
698
+ health: [
699
+ '🏥 飞轮健康检查',
700
+ '',
701
+ ' 健康分: 100/100',
702
+ ' 活跃条目: 0',
703
+ '',
704
+ ' 数据流状态:',
705
+ ' 引用密度: ✓',
706
+ ' 新鲜度: ✓',
707
+ ' 消费追踪: ○ 未启用',
708
+ '',
709
+ ' 飞轮指标:',
710
+ ' 引用覆盖: 0%',
711
+ ' 平均引用: 0',
712
+ '',
713
+ ' 问题 (前 20):',
714
+ ' [info] -: 消费追踪数据不存在(<TMP>/.harness/knowledge/.consumption-stats.json)',
715
+ ],
716
+ };
717
+
718
+ function emptyRoot(): string {
719
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'harness-kview-empty-'));
720
+ }
721
+
722
+ describe('闸 4b:空知识库态逐行冻结', () => {
723
+ for (const sub of SUBS) {
724
+ it(`${sub.name} 零条目时的人读输出逐行不变`, async () => {
725
+ const root = emptyRoot();
726
+ const humanIo = captureIO();
727
+ try {
728
+ await sub.run(root, humanIo, false);
729
+ expect(humanIo.outLines().map(l => normalize(root, l))).toEqual(EMPTY_HUMAN_BASELINE[sub.name]);
730
+ } finally {
731
+ fs.rmSync(root, { recursive: true, force: true });
732
+ }
733
+ });
734
+ }
735
+
736
+ it('零条目态的 --json 仍是同一份 data 的形状(空数组不塌成缺键)', async () => {
737
+ const root = emptyRoot();
738
+ try {
739
+ await knowledgeList({ projectPath: root, json: true }, io);
740
+ expect(lastJsonOutput(io)).toEqual({ total: 0, entries: [] });
741
+ const emptyIo = captureIO();
742
+ await knowledgeDecay({ projectPath: root, json: true }, emptyIo);
743
+ expect(lastJsonOutput(emptyIo)).toEqual({ changes: [] });
744
+ } finally {
745
+ fs.rmSync(root, { recursive: true, force: true });
746
+ }
747
+ });
748
+
749
+ /**
750
+ * 本票唯一的行为偏离(#133 收尾须知):sync-rag 的两个空态原先把人类文案
751
+ * 直接打到 stdout——`--json` 下吐出的不是 JSON。收口到单点分派后,空态由
752
+ * display model 承担,json 面恒为 `{directory, files}`。零程序化消费者(决策
753
+ * 记录见本目录 CONTEXT.md),故按缺陷修正处理。
754
+ */
755
+ it('sync-rag 空态在 --json 下产出 JSON 而非人类文案(对改造前行为的有意修正)', async () => {
756
+ const root = emptyRoot();
757
+ try {
758
+ await knowledgeSyncRag({ projectPath: root, json: true }, io);
759
+ expect(lastJsonOutput(io)).toEqual({ directory: `${root}/.harness/knowledge-docs`, files: [] });
760
+ } finally {
761
+ fs.rmSync(root, { recursive: true, force: true });
762
+ }
763
+ });
764
+ });
765
+
766
+ /**
767
+ * 同一子命令的第二空态(harness#151 补 #133 闸 4b 的判据)。
768
+ *
769
+ * `emptyRoot()` 是裸的 mkdtemp 目录,天然没有 `.harness/knowledge-docs/` 子目录,于是
770
+ * `syncRagRows`(`knowledge.ts:394`)的两个空态分支里只有第一个 `!docsPresent` 进过基线;
771
+ * 第二个分支(目录在场、里面没有 `.md`)在测试里**走不到**——按 #133 自己立的判据,
772
+ * 「fixture 走不到的分支不进基线,有基线就是假象」。json 面两分支恒同形
773
+ * (`data = {directory, files}`),所以 `--json` 那条断言也钉不住人读第二行。
774
+ *
775
+ * 数据结构:既有 `EMPTY_HUMAN_BASELINE: Record<SubName, string[]>` 一个子命令只装得下
776
+ * 一种形状,故另立一张平行表按 case 登记,不动既有用例。每个 case 自带建形函数——
777
+ * 多空态不共用一个根。
778
+ */
779
+ interface EmptyShapeCase {
780
+ sub: SubName;
781
+ /** 分支说明,进用例标题 */
782
+ branch: string;
783
+ /** 该分支的建形方式(与 emptyRoot() 的区别就是要建的那个形) */
784
+ makeRoot(): string;
785
+ baseline: string[];
786
+ /** 该形状下 --json 的形状(两投影一致性在此分支的第二形) */
787
+ json: (root: string) => unknown;
788
+ }
789
+
790
+ /** `knowledge-docs/` 在场但目录里没有 `.md` → syncRagRows 第二分支 */
791
+ function emptyDocsRoot(): string {
792
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-kview-emptydocs-'));
793
+ fs.mkdirSync(path.join(root, '.harness', 'knowledge-docs'), { recursive: true });
794
+ return root;
795
+ }
796
+
797
+ const EXTRA_EMPTY_HUMAN_CASES: EmptyShapeCase[] = [
798
+ {
799
+ sub: 'sync-rag',
800
+ branch: 'knowledge-docs/ 在场但目录里没有 .md',
801
+ makeRoot: emptyDocsRoot,
802
+ baseline: ['No knowledge docs found'],
803
+ json: root => ({ directory: `${root}/.harness/knowledge-docs`, files: [] }),
804
+ },
805
+ ];
806
+
807
+ describe('闸 4c:同一子命令的第二空态逐行冻结(#151 补 #133 判据)', () => {
808
+ for (const testCase of EXTRA_EMPTY_HUMAN_CASES) {
809
+ const driver = SUBS.find(s => s.name === testCase.sub);
810
+ if (!driver) throw new Error(`case 登记的子命令不在驱动面里: ${testCase.sub}`);
811
+
812
+ it(`${testCase.sub} 的「${testCase.branch}」人读输出逐行不变`, async () => {
813
+ const root = testCase.makeRoot();
814
+ const humanIo = captureIO();
815
+ try {
816
+ await driver.run(root, humanIo, false);
817
+ expect(humanIo.outLines().map(l => normalize(root, l))).toEqual(testCase.baseline);
818
+ } finally {
819
+ fs.rmSync(root, { recursive: true, force: true });
820
+ }
821
+ });
822
+
823
+ it(`${testCase.sub} 的「${testCase.branch}」在 --json 下仍是两分支同形的那份 data`, async () => {
824
+ const root = testCase.makeRoot();
825
+ try {
826
+ await driver.run(root, io, true);
827
+ expect(lastJsonOutput(io)).toEqual(testCase.json(root));
828
+ } finally {
829
+ fs.rmSync(root, { recursive: true, force: true });
830
+ }
831
+ });
832
+ }
833
+
834
+ it('第二形登记的子命令确有独立基线(不借 闸 4b 的第一形蒙过去)', () => {
835
+ for (const testCase of EXTRA_EMPTY_HUMAN_CASES) {
836
+ expect(EMPTY_HUMAN_BASELINE[testCase.sub]).not.toEqual(testCase.baseline);
837
+ }
838
+ });
839
+ });