@dommaker/harness 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (563) hide show
  1. package/CHANGELOG.md +44 -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 +19 -3
  15. package/dist/cli/commands/check.d.ts.map +1 -1
  16. package/dist/cli/commands/check.js +36 -22
  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/status.d.ts +8 -0
  76. package/dist/cli/commands/status.d.ts.map +1 -1
  77. package/dist/cli/commands/status.js +5 -7
  78. package/dist/cli/commands/status.js.map +1 -1
  79. package/dist/cli/commands/sync-docs/context-syncer.d.ts +21 -2
  80. package/dist/cli/commands/sync-docs/context-syncer.d.ts.map +1 -1
  81. package/dist/cli/commands/sync-docs/context-syncer.js +112 -28
  82. package/dist/cli/commands/sync-docs/context-syncer.js.map +1 -1
  83. package/dist/cli/commands/sync-docs/index.d.ts +1 -1
  84. package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
  85. package/dist/cli/commands/sync-docs/index.js +86 -27
  86. package/dist/cli/commands/sync-docs/index.js.map +1 -1
  87. package/dist/cli/commands/sync-docs/project-reader.d.ts +24 -1
  88. package/dist/cli/commands/sync-docs/project-reader.d.ts.map +1 -1
  89. package/dist/cli/commands/sync-docs/project-reader.js +33 -7
  90. package/dist/cli/commands/sync-docs/project-reader.js.map +1 -1
  91. package/dist/cli/commands/validate.d.ts +0 -16
  92. package/dist/cli/commands/validate.d.ts.map +1 -1
  93. package/dist/cli/commands/validate.js +2 -97
  94. package/dist/cli/commands/validate.js.map +1 -1
  95. package/dist/cli/gate-command.d.ts +37 -0
  96. package/dist/cli/gate-command.d.ts.map +1 -0
  97. package/dist/cli/gate-command.js +64 -0
  98. package/dist/cli/gate-command.js.map +1 -0
  99. package/dist/cli/state-io.d.ts +34 -0
  100. package/dist/cli/state-io.d.ts.map +1 -0
  101. package/dist/cli/state-io.js +71 -0
  102. package/dist/cli/state-io.js.map +1 -0
  103. package/dist/completion-checkers/index.d.ts +1 -1
  104. package/dist/completion-checkers/index.d.ts.map +1 -1
  105. package/dist/completion-checkers/index.js +0 -15
  106. package/dist/completion-checkers/index.js.map +1 -1
  107. package/dist/context/knowledge-injector.d.ts +4 -2
  108. package/dist/context/knowledge-injector.d.ts.map +1 -1
  109. package/dist/context/knowledge-injector.js +13 -6
  110. package/dist/context/knowledge-injector.js.map +1 -1
  111. package/dist/core/constraints/agent-prompt-renderer.js +2 -2
  112. package/dist/core/constraints/agent-prompt-renderer.js.map +1 -1
  113. package/dist/core/constraints/capabilities-reconcile.d.ts +20 -0
  114. package/dist/core/constraints/capabilities-reconcile.d.ts.map +1 -1
  115. package/dist/core/constraints/capabilities-reconcile.js +26 -4
  116. package/dist/core/constraints/capabilities-reconcile.js.map +1 -1
  117. package/dist/core/constraints/check-cache.d.ts +1 -1
  118. package/dist/core/constraints/check-cache.js +1 -1
  119. package/dist/core/constraints/checker.d.ts +8 -3
  120. package/dist/core/constraints/checker.d.ts.map +1 -1
  121. package/dist/core/constraints/checker.js +30 -19
  122. package/dist/core/constraints/checker.js.map +1 -1
  123. package/dist/core/constraints/checkers/capability-sync.d.ts +3 -1
  124. package/dist/core/constraints/checkers/capability-sync.d.ts.map +1 -1
  125. package/dist/core/constraints/checkers/capability-sync.js +17 -24
  126. package/dist/core/constraints/checkers/capability-sync.js.map +1 -1
  127. package/dist/core/constraints/checkers/context-doc-sync.d.ts +1 -1
  128. package/dist/core/constraints/checkers/context-doc-sync.d.ts.map +1 -1
  129. package/dist/core/constraints/checkers/context-doc-sync.js +9 -6
  130. package/dist/core/constraints/checkers/context-doc-sync.js.map +1 -1
  131. package/dist/core/constraints/checkers/docs-freshness.d.ts.map +1 -1
  132. package/dist/core/constraints/checkers/docs-freshness.js +24 -30
  133. package/dist/core/constraints/checkers/docs-freshness.js.map +1 -1
  134. package/dist/core/constraints/checkers/governance-presence.d.ts.map +1 -1
  135. package/dist/core/constraints/checkers/governance-presence.js +10 -4
  136. package/dist/core/constraints/checkers/governance-presence.js.map +1 -1
  137. package/dist/core/constraints/checkers/types.d.ts +10 -6
  138. package/dist/core/constraints/checkers/types.d.ts.map +1 -1
  139. package/dist/core/constraints/checkers/types.js +7 -3
  140. package/dist/core/constraints/checkers/types.js.map +1 -1
  141. package/dist/core/constraints/context-builder.d.ts +11 -10
  142. package/dist/core/constraints/context-builder.d.ts.map +1 -1
  143. package/dist/core/constraints/context-builder.js +31 -27
  144. package/dist/core/constraints/context-builder.js.map +1 -1
  145. package/dist/core/constraints/context-reconcile.d.ts +75 -0
  146. package/dist/core/constraints/context-reconcile.d.ts.map +1 -0
  147. package/dist/core/constraints/context-reconcile.js +161 -0
  148. package/dist/core/constraints/context-reconcile.js.map +1 -0
  149. package/dist/core/constraints/doc-freshness/runner.d.ts +7 -0
  150. package/dist/core/constraints/doc-freshness/runner.d.ts.map +1 -1
  151. package/dist/core/constraints/doc-freshness/runner.js +8 -3
  152. package/dist/core/constraints/doc-freshness/runner.js.map +1 -1
  153. package/dist/core/constraints/git-evidence.js +1 -1
  154. package/dist/core/constraints/git-evidence.js.map +1 -1
  155. package/dist/core/constraints/injection-drift.d.ts +7 -2
  156. package/dist/core/constraints/injection-drift.d.ts.map +1 -1
  157. package/dist/core/constraints/injection-drift.js +13 -7
  158. package/dist/core/constraints/injection-drift.js.map +1 -1
  159. package/dist/core/constraints/run-env.d.ts +92 -0
  160. package/dist/core/constraints/run-env.d.ts.map +1 -0
  161. package/dist/core/constraints/run-env.js +175 -0
  162. package/dist/core/constraints/run-env.js.map +1 -0
  163. package/dist/core/constraints/triggers.d.ts +22 -0
  164. package/dist/core/constraints/triggers.d.ts.map +1 -0
  165. package/dist/core/constraints/triggers.js +31 -0
  166. package/dist/core/constraints/triggers.js.map +1 -0
  167. package/dist/core/effective-constraints.d.ts +13 -4
  168. package/dist/core/effective-constraints.d.ts.map +1 -1
  169. package/dist/core/effective-constraints.js +16 -6
  170. package/dist/core/effective-constraints.js.map +1 -1
  171. package/dist/core/effective-set.d.ts +8 -15
  172. package/dist/core/effective-set.d.ts.map +1 -1
  173. package/dist/core/effective-set.js +7 -12
  174. package/dist/core/effective-set.js.map +1 -1
  175. package/dist/core/index.d.ts +1 -0
  176. package/dist/core/index.d.ts.map +1 -1
  177. package/dist/core/index.js.map +1 -1
  178. package/dist/core/project-config-loader.d.ts +39 -17
  179. package/dist/core/project-config-loader.d.ts.map +1 -1
  180. package/dist/core/project-config-loader.js +51 -122
  181. package/dist/core/project-config-loader.js.map +1 -1
  182. package/dist/core/validators/check-handlers/http.d.ts +2 -2
  183. package/dist/core/validators/check-handlers/http.d.ts.map +1 -1
  184. package/dist/core/validators/check-handlers/http.js +12 -7
  185. package/dist/core/validators/check-handlers/http.js.map +1 -1
  186. package/dist/failure/classifier.d.ts +1 -1
  187. package/dist/failure/classifier.d.ts.map +1 -1
  188. package/dist/failure/classifier.js +8 -8
  189. package/dist/failure/classifier.js.map +1 -1
  190. package/dist/failure/index.d.ts +2 -1
  191. package/dist/failure/index.d.ts.map +1 -1
  192. package/dist/failure/index.js +9 -17
  193. package/dist/failure/index.js.map +1 -1
  194. package/dist/failure/recorder.d.ts +1 -1
  195. package/dist/failure/recorder.d.ts.map +1 -1
  196. package/dist/gates/checker-gate.d.ts +2 -1
  197. package/dist/gates/checker-gate.d.ts.map +1 -1
  198. package/dist/gates/checker-gate.js +3 -2
  199. package/dist/gates/checker-gate.js.map +1 -1
  200. package/dist/gates/command.d.ts +10 -4
  201. package/dist/gates/command.d.ts.map +1 -1
  202. package/dist/gates/command.js +36 -62
  203. package/dist/gates/command.js.map +1 -1
  204. package/dist/gates/contract.d.ts.map +1 -1
  205. package/dist/gates/contract.js +0 -4
  206. package/dist/gates/contract.js.map +1 -1
  207. package/dist/gates/definitions.d.ts +2 -1
  208. package/dist/gates/definitions.d.ts.map +1 -1
  209. package/dist/gates/definitions.js +6 -5
  210. package/dist/gates/definitions.js.map +1 -1
  211. package/dist/gates/index.d.ts +0 -4
  212. package/dist/gates/index.d.ts.map +1 -1
  213. package/dist/gates/index.js +1 -6
  214. package/dist/gates/index.js.map +1 -1
  215. package/dist/gates/performance.d.ts.map +1 -1
  216. package/dist/gates/performance.js +0 -4
  217. package/dist/gates/performance.js.map +1 -1
  218. package/dist/gates/runner.d.ts +1 -1
  219. package/dist/gates/runner.js +1 -1
  220. package/dist/gates/security.d.ts +1 -1
  221. package/dist/gates/security.d.ts.map +1 -1
  222. package/dist/gates/security.js +5 -6
  223. package/dist/gates/security.js.map +1 -1
  224. package/dist/gates/types.d.ts +14 -17
  225. package/dist/gates/types.d.ts.map +1 -1
  226. package/dist/gates/types.js.map +1 -1
  227. package/dist/hooks/bootstrap.d.ts +6 -4
  228. package/dist/hooks/bootstrap.d.ts.map +1 -1
  229. package/dist/hooks/bootstrap.js +25 -13
  230. package/dist/hooks/bootstrap.js.map +1 -1
  231. package/dist/hooks/config.d.ts +6 -2
  232. package/dist/hooks/config.d.ts.map +1 -1
  233. package/dist/hooks/config.js +5 -1
  234. package/dist/hooks/config.js.map +1 -1
  235. package/dist/hooks/index.d.ts +1 -1
  236. package/dist/hooks/index.d.ts.map +1 -1
  237. package/dist/hooks/index.js +0 -15
  238. package/dist/hooks/index.js.map +1 -1
  239. package/dist/hooks/pipeline.d.ts +19 -1
  240. package/dist/hooks/pipeline.d.ts.map +1 -1
  241. package/dist/hooks/pipeline.js +42 -4
  242. package/dist/hooks/pipeline.js.map +1 -1
  243. package/dist/hooks/registry.d.ts +11 -8
  244. package/dist/hooks/registry.d.ts.map +1 -1
  245. package/dist/hooks/registry.js +22 -7
  246. package/dist/hooks/registry.js.map +1 -1
  247. package/dist/hooks/types.d.ts +25 -7
  248. package/dist/hooks/types.d.ts.map +1 -1
  249. package/dist/index.d.ts +3 -3
  250. package/dist/index.d.ts.map +1 -1
  251. package/dist/index.js +3 -4
  252. package/dist/index.js.map +1 -1
  253. package/dist/knowledge/audit-scoring.d.ts +87 -0
  254. package/dist/knowledge/audit-scoring.d.ts.map +1 -0
  255. package/dist/knowledge/audit-scoring.js +470 -0
  256. package/dist/knowledge/audit-scoring.js.map +1 -0
  257. package/dist/knowledge/audit.d.ts +11 -59
  258. package/dist/knowledge/audit.d.ts.map +1 -1
  259. package/dist/knowledge/audit.js +47 -522
  260. package/dist/knowledge/audit.js.map +1 -1
  261. package/dist/knowledge/import.d.ts.map +1 -1
  262. package/dist/knowledge/import.js +2 -0
  263. package/dist/knowledge/import.js.map +1 -1
  264. package/dist/knowledge/index-generator.d.ts.map +1 -1
  265. package/dist/knowledge/index-generator.js +4 -5
  266. package/dist/knowledge/index-generator.js.map +1 -1
  267. package/dist/knowledge/index.d.ts +3 -2
  268. package/dist/knowledge/index.d.ts.map +1 -1
  269. package/dist/knowledge/index.js +3 -16
  270. package/dist/knowledge/index.js.map +1 -1
  271. package/dist/knowledge/ingest.js +1 -1
  272. package/dist/knowledge/ingest.js.map +1 -1
  273. package/dist/knowledge/lifecycle.d.ts.map +1 -1
  274. package/dist/knowledge/lifecycle.js +4 -2
  275. package/dist/knowledge/lifecycle.js.map +1 -1
  276. package/dist/knowledge/migration.d.ts +3 -0
  277. package/dist/knowledge/migration.d.ts.map +1 -1
  278. package/dist/knowledge/migration.js +8 -2
  279. package/dist/knowledge/migration.js.map +1 -1
  280. package/dist/knowledge/query.d.ts +6 -0
  281. package/dist/knowledge/query.d.ts.map +1 -1
  282. package/dist/knowledge/query.js +8 -2
  283. package/dist/knowledge/query.js.map +1 -1
  284. package/dist/knowledge/reference-tracker.d.ts.map +1 -1
  285. package/dist/knowledge/reference-tracker.js +4 -6
  286. package/dist/knowledge/reference-tracker.js.map +1 -1
  287. package/dist/knowledge/store.d.ts +22 -16
  288. package/dist/knowledge/store.d.ts.map +1 -1
  289. package/dist/knowledge/store.js +53 -12
  290. package/dist/knowledge/store.js.map +1 -1
  291. package/dist/knowledge/tree-walker.d.ts +19 -0
  292. package/dist/knowledge/tree-walker.d.ts.map +1 -0
  293. package/dist/knowledge/tree-walker.js +30 -0
  294. package/dist/knowledge/tree-walker.js.map +1 -0
  295. package/dist/knowledge/types.d.ts +16 -0
  296. package/dist/knowledge/types.d.ts.map +1 -1
  297. package/dist/monitoring/traces.d.ts +5 -3
  298. package/dist/monitoring/traces.d.ts.map +1 -1
  299. package/dist/monitoring/traces.js +19 -5
  300. package/dist/monitoring/traces.js.map +1 -1
  301. package/dist/test-setup/jsonl-fake-fs.d.ts +30 -0
  302. package/dist/test-setup/jsonl-fake-fs.d.ts.map +1 -0
  303. package/dist/test-setup/jsonl-fake-fs.js +49 -0
  304. package/dist/test-setup/jsonl-fake-fs.js.map +1 -0
  305. package/dist/tools/index.d.ts +4 -1
  306. package/dist/tools/index.d.ts.map +1 -1
  307. package/dist/tools/index.js +7 -15
  308. package/dist/tools/index.js.map +1 -1
  309. package/dist/types/checkpoint.d.ts +5 -0
  310. package/dist/types/checkpoint.d.ts.map +1 -1
  311. package/dist/types/project-config.d.ts +18 -0
  312. package/dist/types/project-config.d.ts.map +1 -1
  313. package/dist/types/trace.d.ts +13 -4
  314. package/dist/types/trace.d.ts.map +1 -1
  315. package/dist/types/trace.js +4 -3
  316. package/dist/types/trace.js.map +1 -1
  317. package/dist/utils/exec.d.ts +4 -27
  318. package/dist/utils/exec.d.ts.map +1 -1
  319. package/dist/utils/exec.js +4 -94
  320. package/dist/utils/exec.js.map +1 -1
  321. package/dist/utils/jsonl.d.ts +17 -7
  322. package/dist/utils/jsonl.d.ts.map +1 -1
  323. package/dist/utils/jsonl.js +144 -17
  324. package/dist/utils/jsonl.js.map +1 -1
  325. package/dist/utils/numeric-flag.d.ts +44 -0
  326. package/dist/utils/numeric-flag.d.ts.map +1 -0
  327. package/dist/utils/numeric-flag.js +62 -0
  328. package/dist/utils/numeric-flag.js.map +1 -0
  329. package/package.json +1 -1
  330. package/src/CONTEXT.md +5 -2
  331. package/src/__tests__/acceptance-gate.test.ts +7 -4
  332. package/src/__tests__/checker-extra.test.ts +3 -2
  333. package/src/__tests__/checker.test.ts +4 -3
  334. package/src/__tests__/checkpoint.test.ts +26 -9
  335. package/src/__tests__/context-files-resolution.test.ts +29 -15
  336. package/src/__tests__/contract-gate.test.ts +19 -40
  337. package/src/__tests__/error-classifier.test.ts +1 -1
  338. package/src/__tests__/exec.test.ts +45 -107
  339. package/src/__tests__/failure-recorder.test.ts +7 -5
  340. package/src/__tests__/hooks-pipeline.test.ts +34 -47
  341. package/src/__tests__/layering.test.ts +217 -25
  342. package/src/__tests__/passes-gate.test.ts +9 -4
  343. package/src/__tests__/performance-gate.test.ts +3 -15
  344. package/src/__tests__/project-config-loader-extra.test.ts +31 -57
  345. package/src/__tests__/project-config-loader.test.ts +6 -3
  346. package/src/__tests__/public-exports.test.ts +0 -1
  347. package/src/__tests__/public-type-surface.test.ts +5 -4
  348. package/src/__tests__/public-value-surface.test.ts +182 -0
  349. package/src/__tests__/review-gate.test.ts +4 -6
  350. package/src/__tests__/security-gate.test.ts +5 -20
  351. package/src/__tests__/skip-semantics.test.ts +5 -3
  352. package/src/__tests__/spec-validator.test.ts +3 -2
  353. package/src/__tests__/sub-barrels-explicit.test.ts +87 -0
  354. package/src/__tests__/test-fixture-hygiene.test.ts +53 -0
  355. package/src/__tests__/trace-analyzer-extra.test.ts +5 -3
  356. package/src/__tests__/trace-analyzer.test.ts +5 -3
  357. package/src/__tests__/trace-collector.test.ts +5 -3
  358. package/src/agents/index.ts +13 -2
  359. package/src/cli/__tests__/gate-command.test.ts +162 -0
  360. package/src/cli/__tests__/state-io.test.ts +50 -0
  361. package/src/cli/commands/CONTEXT.md +30 -5
  362. package/src/cli/commands/__tests__/acceptance.test.ts +24 -25
  363. package/src/cli/commands/__tests__/bin-exit-mapping.test.ts +15 -0
  364. package/src/cli/commands/__tests__/check-drift.test.ts +42 -1
  365. package/src/cli/commands/__tests__/check-read-count.test.ts +170 -0
  366. package/src/cli/commands/__tests__/check-skip-output.test.ts +4 -1
  367. package/src/cli/commands/__tests__/check.test.ts +63 -44
  368. package/src/cli/commands/__tests__/command-gate-dead-options.test.ts +60 -0
  369. package/src/cli/commands/__tests__/command.test.ts +33 -17
  370. package/src/cli/commands/__tests__/constraints-retire.test.ts +52 -0
  371. package/src/cli/commands/__tests__/context-syncer.test.ts +141 -0
  372. package/src/cli/commands/__tests__/contract.test.ts +20 -20
  373. package/src/cli/commands/__tests__/failure.test.ts +5 -4
  374. package/src/cli/commands/__tests__/gate-decision.ts +30 -0
  375. package/src/cli/commands/__tests__/init-dead-options.test.ts +50 -0
  376. package/src/cli/commands/__tests__/init-governance-io.test.ts +139 -0
  377. package/src/cli/commands/__tests__/init-injection.test.ts +3 -3
  378. package/src/cli/commands/__tests__/init-ondisk.test.ts +867 -0
  379. package/src/cli/commands/__tests__/init.test.ts +82 -6
  380. package/src/cli/commands/__tests__/knowledge-audit-threshold.test.ts +146 -0
  381. package/src/cli/commands/__tests__/knowledge-flywheel-consistency.test.ts +1 -1
  382. package/src/cli/commands/__tests__/knowledge-tone-mapping.test.ts +244 -0
  383. package/src/cli/commands/__tests__/knowledge-view.test.ts +839 -0
  384. package/src/cli/commands/__tests__/knowledge.test.ts +33 -15
  385. package/src/cli/commands/__tests__/numeric-flag-assembly.test.ts +368 -0
  386. package/src/cli/commands/__tests__/performance.test.ts +19 -21
  387. package/src/cli/commands/__tests__/posteval-plan.test.ts +5 -3
  388. package/src/cli/commands/__tests__/pre-push-hook.test.ts +181 -0
  389. package/src/cli/commands/__tests__/project-path-anchoring.test.ts +95 -2
  390. package/src/cli/commands/__tests__/project-path-convention.test.ts +103 -25
  391. package/src/cli/commands/__tests__/registry.test.ts +2 -0
  392. package/src/cli/commands/__tests__/report.test.ts +23 -1
  393. package/src/cli/commands/__tests__/review.test.ts +10 -17
  394. package/src/cli/commands/__tests__/scaffold.test.ts +505 -0
  395. package/src/cli/commands/__tests__/security-default-scan.test.ts +75 -0
  396. package/src/cli/commands/__tests__/security.test.ts +21 -24
  397. package/src/cli/commands/__tests__/spec-baseline-check.test.ts +3 -2
  398. package/src/cli/commands/__tests__/spec-baseline-index-cap.test.ts +131 -0
  399. package/src/cli/commands/__tests__/spec-baseline-read-count.test.ts +325 -0
  400. package/src/cli/commands/__tests__/status-extra.test.ts +25 -8
  401. package/src/cli/commands/__tests__/status.test.ts +46 -18
  402. package/src/cli/commands/__tests__/sync-docs-agents.test.ts +19 -4
  403. package/src/cli/commands/__tests__/sync-docs-context-drift.test.ts +232 -0
  404. package/src/cli/commands/__tests__/sync-docs-module-order.test.ts +111 -0
  405. package/src/cli/commands/__tests__/sync-docs-read-count.test.ts +192 -0
  406. package/src/cli/commands/__tests__/sync-docs.test.ts +3 -2
  407. package/src/cli/commands/__tests__/validate.test.ts +1 -48
  408. package/src/cli/commands/acceptance.ts +24 -29
  409. package/src/cli/commands/check.ts +60 -35
  410. package/src/cli/commands/command.ts +9 -8
  411. package/src/cli/commands/constraints-retire.ts +57 -70
  412. package/src/cli/commands/contract.ts +44 -47
  413. package/src/cli/commands/definitions.ts +12 -6
  414. package/src/cli/commands/failure.ts +16 -3
  415. package/src/cli/commands/init.ts +242 -389
  416. package/src/cli/commands/knowledge-view.ts +224 -0
  417. package/src/cli/commands/knowledge.ts +643 -448
  418. package/src/cli/commands/passes-gate.ts +12 -3
  419. package/src/cli/commands/performance.ts +31 -38
  420. package/src/cli/commands/report.ts +3 -2
  421. package/src/cli/commands/review.ts +22 -28
  422. package/src/cli/commands/scaffold-templates.ts +406 -0
  423. package/src/cli/commands/scaffold.ts +302 -0
  424. package/src/cli/commands/security.ts +27 -23
  425. package/src/cli/commands/spec-baseline-check.ts +158 -33
  426. package/src/cli/commands/status.ts +12 -7
  427. package/src/cli/commands/sync-docs/__tests__/context-export-surface.test.ts +102 -0
  428. package/src/cli/commands/sync-docs/__tests__/module-descriptions.test.ts +196 -0
  429. package/src/cli/commands/sync-docs/context-syncer.ts +137 -31
  430. package/src/cli/commands/sync-docs/index.ts +103 -28
  431. package/src/cli/commands/sync-docs/project-reader.ts +51 -7
  432. package/src/cli/commands/validate.ts +1 -104
  433. package/src/cli/gate-command.ts +87 -0
  434. package/src/cli/state-io.ts +53 -0
  435. package/src/completion-checkers/index.ts +10 -1
  436. package/src/context/CONTEXT.md +2 -1
  437. package/src/context/__tests__/knowledge-injector.test.ts +27 -0
  438. package/src/context/__tests__/session-manager.test.ts +19 -12
  439. package/src/context/knowledge-injector.ts +14 -7
  440. package/src/core/CONTEXT.md +10 -7
  441. package/src/core/__tests__/effective-set.test.ts +36 -60
  442. package/src/core/constraints/__tests__/context-reconcile.test.ts +246 -0
  443. package/src/core/constraints/__tests__/run-env.test.ts +377 -0
  444. package/src/core/constraints/__tests__/triggers.test.ts +70 -0
  445. package/src/core/constraints/agent-prompt-renderer.ts +1 -1
  446. package/src/core/constraints/capabilities-reconcile.ts +28 -4
  447. package/src/core/constraints/check-cache.ts +1 -1
  448. package/src/core/constraints/checker.ts +30 -14
  449. package/src/core/constraints/checkers/__tests__/context-doc-sync.test.ts +8 -3
  450. package/src/core/constraints/checkers/__tests__/governance-presence.test.ts +16 -7
  451. package/src/core/constraints/checkers/__tests__/no-hardcoded-credentials.test.ts +2 -1
  452. package/src/core/constraints/checkers/capability-sync.ts +27 -28
  453. package/src/core/constraints/checkers/context-doc-sync.ts +9 -7
  454. package/src/core/constraints/checkers/docs-freshness.ts +28 -33
  455. package/src/core/constraints/checkers/governance-presence.ts +10 -6
  456. package/src/core/constraints/checkers/types.ts +14 -8
  457. package/src/core/constraints/context-builder.ts +35 -26
  458. package/src/core/constraints/context-reconcile.ts +200 -0
  459. package/src/core/constraints/doc-freshness/__tests__/runner.test.ts +25 -0
  460. package/src/core/constraints/doc-freshness/runner.ts +18 -4
  461. package/src/core/constraints/git-evidence.ts +1 -1
  462. package/src/core/constraints/injection-drift.ts +19 -8
  463. package/src/core/constraints/run-env.ts +207 -0
  464. package/src/core/constraints/triggers.ts +33 -0
  465. package/src/core/effective-constraints.ts +17 -6
  466. package/src/core/effective-set.ts +8 -24
  467. package/src/core/index.ts +2 -0
  468. package/src/core/project-config-loader.ts +54 -102
  469. package/src/core/validators/__tests__/http-retry-sleep.test.ts +231 -0
  470. package/src/core/validators/check-handlers/http.ts +18 -7
  471. package/src/failure/CONTEXT.md +2 -1
  472. package/src/failure/classifier.ts +2 -2
  473. package/src/failure/index.ts +14 -2
  474. package/src/failure/recorder.ts +1 -1
  475. package/src/gates/CONTEXT.md +13 -7
  476. package/src/gates/__tests__/checker-gate.test.ts +1 -1
  477. package/src/gates/__tests__/command-single-predicate.test.ts +135 -0
  478. package/src/gates/__tests__/command.test.ts +8 -15
  479. package/src/gates/__tests__/contract.test.ts +5 -5
  480. package/src/gates/__tests__/gate-interface.test.ts +0 -17
  481. package/src/gates/__tests__/performance.test.ts +0 -13
  482. package/src/gates/__tests__/review.test.ts +0 -1
  483. package/src/gates/__tests__/runner.test.ts +1 -1
  484. package/src/gates/__tests__/security-default-scan.test.ts +97 -0
  485. package/src/gates/__tests__/security.test.ts +0 -13
  486. package/src/gates/checker-gate.ts +3 -2
  487. package/src/gates/command.ts +51 -74
  488. package/src/gates/contract.ts +0 -5
  489. package/src/gates/definitions.ts +6 -5
  490. package/src/gates/index.ts +0 -5
  491. package/src/gates/performance.ts +1 -6
  492. package/src/gates/runner.ts +1 -1
  493. package/src/gates/security.ts +9 -11
  494. package/src/gates/types.ts +20 -23
  495. package/src/hooks/CONTEXT.md +10 -9
  496. package/src/hooks/__tests__/bootstrap.test.ts +29 -6
  497. package/src/hooks/__tests__/pipeline.test.ts +133 -17
  498. package/src/hooks/__tests__/registry.test.ts +51 -3
  499. package/src/hooks/bootstrap.ts +35 -15
  500. package/src/hooks/config.ts +6 -2
  501. package/src/hooks/index.ts +10 -1
  502. package/src/hooks/pipeline.ts +51 -6
  503. package/src/hooks/registry.ts +31 -12
  504. package/src/hooks/types.ts +26 -7
  505. package/src/index.ts +1 -2
  506. package/src/knowledge/CONTEXT.md +14 -7
  507. package/src/knowledge/__tests__/audit-labels.test.ts +8 -5
  508. package/src/knowledge/__tests__/audit-scoring.test.ts +230 -0
  509. package/src/knowledge/__tests__/audit-write-count.test.ts +131 -0
  510. package/src/knowledge/__tests__/audit.test.ts +291 -208
  511. package/src/knowledge/__tests__/doctor.test.ts +3 -2
  512. package/src/knowledge/__tests__/ingest.test.ts +3 -2
  513. package/src/knowledge/__tests__/lifecycle.test.ts +57 -2
  514. package/src/knowledge/__tests__/migration.test.ts +35 -6
  515. package/src/knowledge/__tests__/query.test.ts +3 -2
  516. package/src/knowledge/__tests__/reference-tracker.test.ts +23 -2
  517. package/src/knowledge/__tests__/store.test.ts +149 -4
  518. package/src/knowledge/__tests__/tree-walker.test.ts +132 -0
  519. package/src/knowledge/audit-scoring.ts +589 -0
  520. package/src/knowledge/audit.ts +55 -599
  521. package/src/knowledge/import.ts +2 -0
  522. package/src/knowledge/index-generator.ts +4 -6
  523. package/src/knowledge/index.ts +27 -2
  524. package/src/knowledge/ingest.ts +1 -1
  525. package/src/knowledge/lifecycle.ts +5 -2
  526. package/src/knowledge/migration.ts +8 -2
  527. package/src/knowledge/query.ts +8 -1
  528. package/src/knowledge/reference-tracker.ts +5 -7
  529. package/src/knowledge/store.ts +75 -16
  530. package/src/knowledge/tree-walker.ts +29 -0
  531. package/src/knowledge/types.ts +19 -0
  532. package/src/monitoring/CONTEXT.md +5 -2
  533. package/src/monitoring/__tests__/context-tracker.test.ts +16 -8
  534. package/src/monitoring/__tests__/trace-file-anchoring.test.ts +80 -0
  535. package/src/monitoring/traces.ts +19 -5
  536. package/src/release/CONTEXT.md +1 -1
  537. package/src/release/__tests__/integrity.test.ts +21 -12
  538. package/src/test-setup/__tests__/jsonl-fake-fs.test.ts +98 -0
  539. package/src/test-setup/jsonl-fake-fs.ts +59 -0
  540. package/src/tools/index.ts +5 -1
  541. package/src/types/checkpoint.ts +6 -0
  542. package/src/types/project-config.ts +21 -0
  543. package/src/types/trace.ts +14 -4
  544. package/src/utils/__tests__/file-walk.test.ts +3 -2
  545. package/src/utils/__tests__/jsonl-bounded-read.test.ts +106 -0
  546. package/src/utils/__tests__/jsonl-skip-disposition.test.ts +10 -6
  547. package/src/utils/__tests__/jsonl.test.ts +61 -14
  548. package/src/utils/__tests__/numeric-flag.test.ts +80 -0
  549. package/src/utils/__tests__/package-version.test.ts +2 -2
  550. package/src/utils/exec.ts +4 -64
  551. package/src/utils/jsonl.ts +159 -20
  552. package/src/utils/numeric-flag.ts +64 -0
  553. package/dist/failure/types.d.ts +0 -9
  554. package/dist/failure/types.d.ts.map +0 -1
  555. package/dist/failure/types.js +0 -25
  556. package/dist/failure/types.js.map +0 -1
  557. package/dist/gates/effective-gates.d.ts +0 -30
  558. package/dist/gates/effective-gates.d.ts.map +0 -1
  559. package/dist/gates/effective-gates.js +0 -81
  560. package/dist/gates/effective-gates.js.map +0 -1
  561. package/src/failure/types.ts +0 -9
  562. package/src/gates/__tests__/effective-gates.test.ts +0 -77
  563. package/src/gates/effective-gates.ts +0 -99
@@ -1,24 +1,41 @@
1
1
  /**
2
2
  * harness knowledge 命令
3
3
  *
4
- * 知识库管理:listsearchimportdecaystats
4
+ * 知识库管理 11 子操作(list/search/import/decay/stats/sync-rag/audit/snapshot/migrate/index/health)。
5
+ * 本文件只做「取数 + 声明两投影」:json 面正本在 `data`,人读排版在 `human()` 的
6
+ * display model。上色、label 映射、json/人读分派、退出码、路径解析与 store 构造
7
+ * 统一在 knowledge-view.ts(harness#133,架构评审候选4)。
5
8
  */
6
9
 
7
- import chalk from 'chalk';
8
10
  import * as fs from 'fs';
9
- import * as os from 'os';
10
11
  import * as path from 'path';
11
- import { FileKnowledgeStore as KnowledgeStore } from '../../knowledge/store';
12
12
  import { KnowledgeQuery } from '../../knowledge/query';
13
13
  import { KnowledgeLifecycle } from '../../knowledge/lifecycle';
14
14
  import { ColdStartImporter } from '../../knowledge/import';
15
- import { KnowledgeAudit, AUDIT_RULE_LABELS } from '../../knowledge/audit';
16
- import type { AuditReport, AuditRuleName } from '../../knowledge/audit';
15
+ import { KnowledgeAudit } from '../../knowledge/audit';
16
+ import type { AuditReport, AuditRuleName, DimensionMetrics } from '../../knowledge/audit-scoring';
17
17
  import { evaluateFlywheel } from '../../knowledge/flywheel-metrics';
18
18
  import { migrateKnowledgeEntries } from '../../knowledge/migration';
19
19
  import { KnowledgeIndexGenerator } from '../../knowledge/index-generator';
20
- import type { KnowledgeSubsystem, MaturityLevel, QueryFilter } from '../../knowledge/types';
21
- import { log, logError, processIO, type CommandIO, type CommandResult } from '../command-contract';
20
+ import type { KnowledgeEntry, KnowledgeSubsystem, MaturityChange, MaturityLevel, QueryFilter } from '../../knowledge/types';
21
+ import { logError, processIO, type CommandIO, type CommandResult } from '../command-contract';
22
+ import { numericFlagMessage, parseNumericFlag } from '../../utils/numeric-flag';
23
+ import {
24
+ announce,
25
+ blankLine,
26
+ dimensionLabel,
27
+ emitKnowledgeView,
28
+ openKnowledgeStore,
29
+ resolveKnowledgeBaseDir,
30
+ ruleLabel,
31
+ toneForMaturity,
32
+ toneForScore,
33
+ toneForSeverity,
34
+ type DisplayModel,
35
+ type DisplayRow,
36
+ type DisplaySection,
37
+ type KnowledgeView,
38
+ } from './knowledge-view';
22
39
 
23
40
  export interface KnowledgeOptions {
24
41
  /** 项目路径 */
@@ -27,89 +44,121 @@ export interface KnowledgeOptions {
27
44
  json?: boolean;
28
45
  }
29
46
 
30
- /**
31
- * 知识库列表
32
- */
33
- export async function knowledgeList(options: KnowledgeOptions & {
47
+ /** 子操作共用的入参面:知识库目录覆盖(audit/snapshot/migrate/index/health 收) */
48
+ type KnowledgeDirOption = { dir?: string };
49
+
50
+ // ========================================
51
+ // list
52
+ // ========================================
53
+
54
+ function listFilter(options: KnowledgeListOptions): QueryFilter {
55
+ const filter: QueryFilter = { excludeArchived: false };
56
+ if (options.type) filter.types = options.type.split(',') as KnowledgeSubsystem[];
57
+ if (options.maturity) filter.maturity = options.maturity.split(',') as MaturityLevel[];
58
+ if (options.tag) filter.tags = options.tag.split(',');
59
+ return filter;
60
+ }
61
+
62
+ export interface KnowledgeListOptions extends KnowledgeOptions {
34
63
  type?: string;
35
64
  maturity?: string;
36
65
  tag?: string;
37
- }, io: CommandIO = processIO): Promise<CommandResult> {
38
- const store = new KnowledgeStore({ baseDir: getKnowledgeDir(options.projectPath, io) });
39
-
40
- const filter: QueryFilter = { excludeArchived: false };
41
- if (options.type) {
42
- filter.types = options.type.split(',') as KnowledgeSubsystem[];
43
- }
44
- if (options.maturity) {
45
- filter.maturity = options.maturity.split(',') as MaturityLevel[];
46
- }
47
- if (options.tag) {
48
- filter.tags = options.tag.split(',');
49
- }
66
+ }
50
67
 
51
- const entries = store.list(filter);
68
+ export function knowledgeListView(options: KnowledgeListOptions, io: CommandIO) {
69
+ const entries = openKnowledgeStore(options, io).list(listFilter(options));
70
+ const data = { total: entries.length, entries };
52
71
 
53
- if (options.json) {
54
- log(io, JSON.stringify({ total: entries.length, entries }, null, 2));
55
- return { kind: 'ok' };
56
- }
72
+ return { data, human: (): DisplayModel => ({ sections: [{ rows: listRows(entries) }] }) };
73
+ }
57
74
 
75
+ function listRows(entries: KnowledgeEntry[]): DisplayRow[] {
58
76
  if (entries.length === 0) {
59
- log(io, chalk.yellow('知识库为空'));
60
- return { kind: 'ok' };
61
- }
62
-
63
- log(io, chalk.blue(`📚 知识库 (${entries.length} 条)\n`));
64
- for (const entry of entries) {
65
- const maturityColor = entry.maturity === 'proven' ? chalk.green
66
- : entry.maturity === 'verified' ? chalk.cyan
67
- : entry.maturity === 'archived' ? chalk.gray
68
- : chalk.yellow;
69
- log(io, ` ${maturityColor(`[${entry.maturity}]`)} ${chalk.bold(entry.title)}`);
70
- log(io, ` ${chalk.gray(`id: ${entry.id} | type: ${entry.type} | layer: ${entry.layer}`)}`);
77
+ return [{ cells: [{ label: '知识库为空', tone: 'warn' }] }];
78
+ }
79
+ const rows: DisplayRow[] = [
80
+ { cells: [
81
+ { label: '📚 知识库 (', tone: 'heading' },
82
+ { field: 'total', text: String(entries.length), tone: 'heading' },
83
+ { label: ' 条)', tone: 'heading' },
84
+ ] },
85
+ blankLine(),
86
+ ];
87
+ entries.forEach((entry, i) => {
88
+ rows.push({ cells: [
89
+ { field: `entries.${i}.maturity`, text: ` [${entry.maturity}]`, tone: toneForMaturity(entry.maturity) },
90
+ { field: `entries.${i}.title`, text: ` ${entry.title}`, tone: 'emph' },
91
+ ] });
92
+ rows.push({ cells: [
93
+ { field: `entries.${i}.id`, text: ` id: ${entry.id}`, tone: 'muted' },
94
+ { field: `entries.${i}.type`, text: ` | type: ${entry.type}`, tone: 'muted' },
95
+ { field: `entries.${i}.layer`, text: ` | layer: ${entry.layer}`, tone: 'muted' },
96
+ ] });
71
97
  if (entry.tags.length > 0) {
72
- log(io, ` ${chalk.gray(`tags: ${entry.tags.join(', ')}`)}`);
98
+ rows.push({ cells: [{ field: `entries.${i}.tags`, text: ` tags: ${entry.tags.join(', ')}`, tone: 'muted' }] });
73
99
  }
74
- }
75
- return { kind: 'ok' };
100
+ });
101
+ return rows;
76
102
  }
77
103
 
78
- /**
79
- * 知识库搜索
80
- */
81
- export async function knowledgeSearch(
104
+ export async function knowledgeList(options: KnowledgeListOptions, io: CommandIO = processIO): Promise<CommandResult> {
105
+ return emitKnowledgeView(io, options, knowledgeListView(options, io));
106
+ }
107
+
108
+ // ========================================
109
+ // search
110
+ // ========================================
111
+
112
+ export function knowledgeSearchView(
82
113
  query: string,
83
114
  options: KnowledgeOptions & { limit?: number },
84
- io: CommandIO = processIO,
85
- ): Promise<CommandResult> {
86
- const store = new KnowledgeStore({ baseDir: getKnowledgeDir(options.projectPath, io) });
87
- const queryEngine = new KnowledgeQuery(store);
115
+ io: CommandIO,
116
+ ) {
117
+ const store = openKnowledgeStore(options, io);
118
+ const matched = new KnowledgeQuery(store).search(query, { limit: options.limit ?? 20 });
119
+ const data = { query, total: matched.length, entries: matched };
88
120
 
89
- const matched = queryEngine.search(query, { limit: options.limit || 20 });
90
-
91
- if (options.json) {
92
- log(io, JSON.stringify({ query, total: matched.length, entries: matched }, null, 2));
93
- return { kind: 'ok' };
94
- }
121
+ return { data, human: (): DisplayModel => ({ sections: [{ rows: searchRows(query, matched) }] }) };
122
+ }
95
123
 
124
+ function searchRows(query: string, matched: KnowledgeEntry[]): DisplayRow[] {
96
125
  if (matched.length === 0) {
97
- log(io, chalk.yellow(`未找到匹配 "${query}" 的知识条目`));
98
- return { kind: 'ok' };
99
- }
126
+ return [{ cells: [{ label: `未找到匹配 "${query}" 的知识条目`, tone: 'warn' }] }];
127
+ }
128
+ const rows: DisplayRow[] = [
129
+ { cells: [
130
+ { label: '🔍 搜索 "', tone: 'heading' },
131
+ { field: 'query', text: query, tone: 'heading' },
132
+ { label: '" (', tone: 'heading' },
133
+ { field: 'total', text: String(matched.length), tone: 'heading' },
134
+ { label: ' 条结果)', tone: 'heading' },
135
+ ] },
136
+ blankLine(),
137
+ ];
138
+ matched.forEach((entry, i) => {
139
+ const preview = entry.content.slice(0, 100).replace(/\n/g, ' ') + (entry.content.length > 100 ? '...' : '');
140
+ rows.push({ cells: [
141
+ { field: `entries.${i}.title`, text: ` ${entry.title}`, tone: 'emph' },
142
+ { field: `entries.${i}.maturity`, text: ` [${entry.maturity}]`, tone: 'muted' },
143
+ ] });
144
+ rows.push({ cells: [{ field: `entries.${i}.content`, text: ` ${preview}`, tone: 'muted' }] });
145
+ });
146
+ return rows;
147
+ }
100
148
 
101
- log(io, chalk.blue(`🔍 搜索 "${query}" (${matched.length} 条结果)\n`));
102
- for (const entry of matched) {
103
- log(io, ` ${chalk.bold(entry.title)} ${chalk.gray(`[${entry.maturity}]`)}`);
104
- const preview = entry.content.slice(0, 100).replace(/\n/g, ' ');
105
- log(io, ` ${chalk.gray(preview)}${entry.content.length > 100 ? '...' : ''}`);
106
- }
107
- return { kind: 'ok' };
149
+ export async function knowledgeSearch(
150
+ query: string,
151
+ options: KnowledgeOptions & { limit?: number },
152
+ io: CommandIO = processIO,
153
+ ): Promise<CommandResult> {
154
+ return emitKnowledgeView(io, options, knowledgeSearchView(query, options, io));
108
155
  }
109
156
 
110
157
  /**
111
- * knowledge search 子命令入口(候选7):缺参闸门 + limit 强转编组,
158
+ * knowledge search 子命令入口(候选7):缺参闸门 + limit 装配窄化,
112
159
  * 自 definitions.ts 的 args 复印块移回命令模块(interface/测试面所在)。
160
+ * `--limit` 给到的恒是字符串(缺省 '20' 也是),脏输入 fail-loud(harness#154,
161
+ * 原先 parseInt 出 NaN 被下游 `|| 20` 静默兜成缺省量)。
113
162
  */
114
163
  export async function knowledgeSearchCommand(
115
164
  positionals: (string | undefined)[],
@@ -121,112 +170,138 @@ export async function knowledgeSearchCommand(
121
170
  logError(io, '请提供搜索关键词');
122
171
  return { kind: 'usage-error', reason: 'knowledge search 缺少关键词位置参数' };
123
172
  }
173
+ const limit = parseNumericFlag(options.limit as string | undefined, 'int');
174
+ if (!limit.ok) {
175
+ logError(io, numericFlagMessage('--limit', limit.raw, 'int'));
176
+ return { kind: 'usage-error', reason: `knowledge search --limit 非法限制: "${limit.raw}"` };
177
+ }
124
178
  return knowledgeSearch(String(query), {
125
179
  projectPath: options.projectPath as string | undefined,
126
180
  json: options.json as boolean | undefined,
127
- limit: parseInt(String(options.limit), 10),
128
- });
181
+ limit: limit.value,
182
+ }, io);
129
183
  }
130
184
 
131
- /**
132
- * 知识库导入(冷启动)
133
- */
134
- export async function knowledgeImport(
185
+ // ========================================
186
+ // import(冷启动)
187
+ // ========================================
188
+
189
+ export async function knowledgeImportView(
135
190
  options: KnowledgeOptions & { sources?: string; reset?: boolean },
136
- io: CommandIO = processIO,
137
- ): Promise<CommandResult> {
191
+ io: CommandIO,
192
+ ) {
138
193
  const projectPath = options.projectPath || process.cwd();
139
- const store = new KnowledgeStore({ baseDir: getKnowledgeDir(options.projectPath, io) });
140
-
194
+ const store = openKnowledgeStore(options, io);
141
195
  const sources = options.sources
142
196
  ? options.sources.split(',') as Array<'code' | 'git' | 'docs' | 'manual'>
143
197
  : ['code', 'git', 'docs'] as Array<'code' | 'git' | 'docs'>;
144
198
 
145
- const importer = new ColdStartImporter({
146
- projectRoot: projectPath,
147
- store,
148
- sources,
149
- });
199
+ const importer = new ColdStartImporter({ projectRoot: projectPath, store, sources });
150
200
 
151
201
  if (options.reset) {
152
202
  importer.resetState();
153
- if (!options.json) {
154
- log(io, chalk.yellow('🔄 已重置导入状态'));
155
- }
156
- }
157
-
158
- if (!options.json) {
159
- log(io, chalk.blue(`📥 开始导入知识 (源: ${sources.join(', ')})...`));
203
+ announce(io, options.json, '🔄 已重置导入状态', 'warn');
160
204
  }
205
+ announce(io, options.json, `📥 开始导入知识 (源: ${sources.join(', ')})...`);
161
206
 
162
207
  const results = await importer.importAll();
208
+ const data = {
209
+ totalImported: results.reduce((sum, r) => sum + r.entries.length, 0),
210
+ totalErrors: results.reduce((sum, r) => sum + r.errors.length, 0),
211
+ results,
212
+ };
163
213
 
164
- const totalImported = results.reduce((sum, r) => sum + r.entries.length, 0);
165
- const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
214
+ return { data, human: (): DisplayModel => ({ sections: [{ rows: importRows(data) }] }) };
215
+ }
166
216
 
167
- if (options.json) {
168
- log(io, JSON.stringify({ totalImported, totalErrors, results }, null, 2));
169
- return { kind: 'ok' };
170
- }
217
+ type ImportOutcome = Awaited<ReturnType<ColdStartImporter['importAll']>>;
171
218
 
172
- for (const result of results) {
219
+ function importRows(data: { totalImported: number; totalErrors: number; results: ImportOutcome }): DisplayRow[] {
220
+ const rows: DisplayRow[] = [];
221
+ data.results.forEach((result, i) => {
173
222
  if (result.entries.length > 0) {
174
- log(io, chalk.green(` ✅ ${result.source.type}: ${result.entries.length} 条`));
175
- }
176
- if (result.errors.length > 0) {
177
- for (const err of result.errors) {
178
- log(io, chalk.red(` ❌ ${result.source.type}: ${err.message}`));
179
- }
223
+ rows.push({ cells: [
224
+ { field: `results.${i}.source.type`, text: ` ✅ ${result.source.type}: `, tone: 'ok' },
225
+ { field: `results.${i}.entries.length`, text: `${result.entries.length} 条`, tone: 'ok' },
226
+ ] });
180
227
  }
181
- }
182
-
183
- log(io, chalk.green(`\n✅ 导入完成: ${totalImported} 条,${totalErrors} 个错误`));
184
- return { kind: 'ok' };
228
+ result.errors.forEach((err, j) => {
229
+ rows.push({ cells: [
230
+ { field: `results.${i}.source.type`, text: ` ❌ ${result.source.type}: `, tone: 'error' },
231
+ { field: `results.${i}.errors.${j}.message`, text: err.message, tone: 'error' },
232
+ ] });
233
+ });
234
+ });
235
+ rows.push(blankLine(), { cells: [
236
+ { label: '✅ 导入完成: ', tone: 'ok' },
237
+ { field: 'totalImported', text: String(data.totalImported), tone: 'ok' },
238
+ { label: ' 条,', tone: 'ok' },
239
+ { field: 'totalErrors', text: String(data.totalErrors), tone: 'ok' },
240
+ { label: ' 个错误', tone: 'ok' },
241
+ ] });
242
+ return rows;
185
243
  }
186
244
 
187
- /**
188
- * 知识库衰减周期
189
- */
190
- export async function knowledgeDecay(options: KnowledgeOptions, io: CommandIO = processIO): Promise<CommandResult> {
191
- const store = new KnowledgeStore({ baseDir: getKnowledgeDir(options.projectPath, io) });
192
- const lifecycle = new KnowledgeLifecycle(store);
245
+ export async function knowledgeImport(
246
+ options: KnowledgeOptions & { sources?: string; reset?: boolean },
247
+ io: CommandIO = processIO,
248
+ ): Promise<CommandResult> {
249
+ return emitKnowledgeView(io, options, await knowledgeImportView(options, io));
250
+ }
193
251
 
194
- if (!options.json) {
195
- log(io, chalk.blue('🔄 运行衰减周期...'));
196
- }
252
+ // ========================================
253
+ // decay
254
+ // ========================================
197
255
 
256
+ export function knowledgeDecayView(options: KnowledgeOptions, io: CommandIO) {
257
+ const lifecycle = new KnowledgeLifecycle(openKnowledgeStore(options, io));
258
+ announce(io, options.json, '🔄 运行衰减周期...');
198
259
  const changes = lifecycle.runDecayCycle();
260
+ const data = { changes };
199
261
 
200
- if (options.json) {
201
- log(io, JSON.stringify({ changes }, null, 2));
202
- return { kind: 'ok' };
203
- }
262
+ return { data, human: (): DisplayModel => ({ sections: [{ rows: decayRows(changes) }] }) };
263
+ }
204
264
 
265
+ function decayRows(changes: MaturityChange[]): DisplayRow[] {
205
266
  if (changes.length === 0) {
206
- log(io, chalk.green('✅ 没有需要衰减的知识条目'));
207
- return { kind: 'ok' };
208
- }
267
+ return [{ cells: [{ label: '✅ 没有需要衰减的知识条目', tone: 'ok' }] }];
268
+ }
269
+ const rows: DisplayRow[] = [
270
+ { cells: [
271
+ { label: '📉 ', tone: 'warn' },
272
+ { field: 'changes.length', text: String(changes.length), tone: 'warn' },
273
+ { label: ' 条知识发生衰减:', tone: 'warn' },
274
+ ] },
275
+ blankLine(),
276
+ ];
277
+ changes.forEach((change, i) => {
278
+ rows.push({ cells: [
279
+ { field: `changes.${i}.entryId`, text: ` ${change.entryId}: ` },
280
+ { field: `changes.${i}.from`, text: change.from, tone: 'error' },
281
+ { label: ' → ' },
282
+ { field: `changes.${i}.to`, text: change.to, tone: 'ok' },
283
+ ] });
284
+ rows.push({ cells: [{ field: `changes.${i}.reason`, text: ` ${change.reason}`, tone: 'muted' }] });
285
+ });
286
+ return rows;
287
+ }
209
288
 
210
- log(io, chalk.yellow(`📉 ${changes.length} 条知识发生衰减:\n`));
211
- for (const change of changes) {
212
- log(io, ` ${change.entryId}: ${chalk.red(change.from)} → ${chalk.green(change.to)}`);
213
- log(io, ` ${chalk.gray(change.reason)}`);
214
- }
215
- return { kind: 'ok' };
289
+ export async function knowledgeDecay(options: KnowledgeOptions, io: CommandIO = processIO): Promise<CommandResult> {
290
+ return emitKnowledgeView(io, options, knowledgeDecayView(options, io));
216
291
  }
217
292
 
218
- /**
219
- * 知识库统计
220
- */
221
- export async function knowledgeStats(options: KnowledgeOptions, io: CommandIO = processIO): Promise<CommandResult> {
222
- const store = new KnowledgeStore({ baseDir: getKnowledgeDir(options.projectPath, io) });
293
+ // ========================================
294
+ // stats
295
+ // ========================================
296
+
297
+ export function knowledgeStatsView(options: KnowledgeOptions, io: CommandIO) {
298
+ const store = openKnowledgeStore(options, io);
223
299
  const entries = store.list({ excludeArchived: false });
224
300
  const active = entries.filter(e => e.maturity !== 'archived');
225
301
 
226
302
  const byType: Record<string, number> = {};
227
303
  const byMaturity: Record<string, number> = {};
228
304
  const byLayer: Record<string, number> = {};
229
-
230
305
  for (const entry of entries) {
231
306
  byType[entry.type] = (byType[entry.type] || 0) + 1;
232
307
  byMaturity[entry.maturity] = (byMaturity[entry.maturity] || 0) + 1;
@@ -244,84 +319,380 @@ export async function knowledgeStats(options: KnowledgeOptions, io: CommandIO =
244
319
  } catch { /* best-effort */ }
245
320
 
246
321
  const metrics = evaluateFlywheel({ entries: active, dailyConsumptionEvents });
247
- const refCoverage = Math.round(metrics.refCoverage * 100);
248
- const avgRefs = Math.round(metrics.avgRefs * 10) / 10;
249
- const consumptionHitRate = Math.round(metrics.consumptionHitRate * 100);
322
+ const flywheel = {
323
+ refCoverage: Math.round(metrics.refCoverage * 100),
324
+ avgRefs: Math.round(metrics.avgRefs * 10) / 10,
325
+ consumptionHitRate: Math.round(metrics.consumptionHitRate * 100),
326
+ };
250
327
 
251
- const flywheel = { refCoverage, avgRefs, consumptionHitRate };
328
+ const data = { total: entries.length, byType, byMaturity, byLayer, flywheel };
329
+ const activeCount = active.length;
330
+
331
+ return {
332
+ data,
333
+ human: (): DisplayModel => ({
334
+ sections: [
335
+ {
336
+ rows: [
337
+ { cells: [{ label: '📊 知识库统计', tone: 'heading' }] },
338
+ blankLine(),
339
+ {
340
+ cells: [
341
+ { label: ' 总计: ', tone: 'emph' },
342
+ { field: 'total', text: String(data.total), tone: 'emph' },
343
+ { label: ' 条 (活跃: ', tone: 'emph' },
344
+ { derived: 'activeCount', text: String(activeCount), tone: 'emph' },
345
+ { label: ')', tone: 'emph' },
346
+ ],
347
+ },
348
+ blankLine(),
349
+ ],
350
+ },
351
+ { title: ' 按类型:', rows: [...countRows('byType', byType), blankLine()] },
352
+ {
353
+ title: ' 按成熟度:',
354
+ rows: [...Object.entries(byMaturity).map(([maturity, count]) => ({
355
+ cells: [
356
+ { field: `byMaturity.${maturity}`, text: ` ${maturity}`, tone: toneForMaturity(maturity as MaturityLevel) },
357
+ { field: `byMaturity.${maturity}`, text: `: ${count}` },
358
+ ],
359
+ })), blankLine()],
360
+ },
361
+ { title: ' 按层级:', rows: [...countRows('byLayer', byLayer), blankLine()] },
362
+ {
363
+ title: ' 飞轮指标:',
364
+ rows: [
365
+ { cells: [{ label: ' 引用覆盖: ' }, { field: 'flywheel.refCoverage', text: `${flywheel.refCoverage}%` }] },
366
+ { cells: [{ label: ' 平均引用: ' }, { field: 'flywheel.avgRefs', text: String(flywheel.avgRefs) }] },
367
+ { cells: [{ label: ' 消费命中率: ' }, { field: 'flywheel.consumptionHitRate', text: `${flywheel.consumptionHitRate}%` }] },
368
+ ],
369
+ },
370
+ ],
371
+ }),
372
+ };
373
+ }
252
374
 
253
- if (options.json) {
254
- log(io, JSON.stringify({ total: entries.length, byType, byMaturity, byLayer, flywheel }, null, 2));
255
- return { kind: 'ok' };
256
- }
375
+ function countRows(field: string, counts: Record<string, number>): DisplayRow[] {
376
+ return Object.entries(counts).map(([key, count]) => ({
377
+ cells: [
378
+ { label: ' ' },
379
+ { field: `${field}.${key}`, text: `${key}: ${count}` },
380
+ ],
381
+ }));
382
+ }
257
383
 
258
- log(io, chalk.blue(`📊 知识库统计\n`));
259
- log(io, chalk.bold(` 总计: ${entries.length} 条 (活跃: ${active.length})\n`));
384
+ export async function knowledgeStats(options: KnowledgeOptions, io: CommandIO = processIO): Promise<CommandResult> {
385
+ return emitKnowledgeView(io, options, knowledgeStatsView(options, io));
386
+ }
260
387
 
261
- log(io, chalk.bold(' 按类型:'));
262
- for (const [type, count] of Object.entries(byType)) {
263
- log(io, ` ${type}: ${count}`);
264
- }
388
+ // ========================================
389
+ // sync-rag
390
+ // ========================================
265
391
 
266
- log(io, chalk.bold('\n 按成熟度:'));
267
- for (const [maturity, count] of Object.entries(byMaturity)) {
268
- const color = maturity === 'proven' ? chalk.green : maturity === 'archived' ? chalk.gray : chalk.yellow;
269
- log(io, ` ${color(maturity)}: ${count}`);
270
- }
392
+ export function knowledgeSyncRagView(options: KnowledgeOptions) {
393
+ const projectPath = options.projectPath || process.cwd();
394
+ const docsDir = `${projectPath}/.harness/knowledge-docs`;
395
+ const docsPresent = fs.existsSync(docsDir);
396
+ const files = docsPresent ? fs.readdirSync(docsDir).filter(f => f.endsWith('.md')) : [];
397
+ const data = { directory: docsDir, files };
271
398
 
272
- log(io, chalk.bold('\n 按层级:'));
273
- for (const [layer, count] of Object.entries(byLayer)) {
274
- log(io, ` ${layer}: ${count}`);
275
- }
399
+ return { data, human: (): DisplayModel => ({ sections: [{ rows: syncRagRows(docsPresent, docsDir, files) }] }) };
400
+ }
401
+
402
+ function syncRagRows(docsPresent: boolean, docsDir: string, files: string[]): DisplayRow[] {
403
+ if (!docsPresent) return [{ cells: [{ label: 'No .harness/knowledge-docs/ directory', tone: 'warn' }] }];
404
+ if (files.length === 0) return [{ cells: [{ label: 'No knowledge docs found', tone: 'muted' }] }];
405
+ return [
406
+ { cells: [
407
+ { label: '📄 RAG sync candidates: ', tone: 'heading' },
408
+ { field: 'files.length', text: String(files.length), tone: 'heading' },
409
+ { label: ' files in ', tone: 'heading' },
410
+ { field: 'directory', text: docsDir, tone: 'heading' },
411
+ ] },
412
+ blankLine(),
413
+ ...files.map((file, i) => ({ cells: [{ field: `files.${i}`, text: ` ${file}`, tone: 'accent' as const }] })),
414
+ blankLine(),
415
+ { cells: [{ label: 'Run `mcp__local-rag__ingest_file` for each to sync to RAG', tone: 'muted' }] },
416
+ ];
417
+ }
276
418
 
277
- log(io, chalk.bold('\n 飞轮指标:'));
278
- log(io, ` 引用覆盖: ${refCoverage}%`);
279
- log(io, ` 平均引用: ${avgRefs}`);
280
- log(io, ` 消费命中率: ${consumptionHitRate}%`);
281
- return { kind: 'ok' };
419
+ export async function knowledgeSyncRag(options: KnowledgeOptions, io: CommandIO = processIO): Promise<CommandResult> {
420
+ return emitKnowledgeView(io, options, knowledgeSyncRagView(options));
282
421
  }
283
422
 
423
+ // ========================================
424
+ // audit(知识库质量审计)
425
+ // ========================================
426
+
427
+ export type KnowledgeAuditOptions = KnowledgeOptions & KnowledgeDirOption & {
428
+ fix?: boolean;
429
+ dryRun?: boolean;
430
+ /**
431
+ * `--threshold <n>` 的 commander 原值。旗帜给到的恒是字符串(缺省 '50' 也是),
432
+ * 声明跟着运行时走(harness#152);窄化在 `knowledgeAudit` 装配点做一次。
433
+ */
434
+ threshold?: string;
435
+ };
436
+
437
+ /** `knowledgeAuditView` 的入参面:threshold 已过装配窄化,引擎槽位要的是数值 */
438
+ export type KnowledgeAuditViewOptions = Omit<KnowledgeAuditOptions, 'threshold'> & {
439
+ threshold?: number;
440
+ };
441
+
442
+ /** 阈值装配结果:`ok: false` = 脏输入,由命令入口 fail-loud(不得往判定槽塞 NaN) */
443
+ type ThresholdAssembly = { ok: true; value?: number } | { ok: false; raw: string };
444
+
284
445
  /**
285
- * RAG 同步:扫描 .harness/knowledge-docs/ 输出需要 ingest 的文件列表
446
+ * `--threshold` 的装配点窄化(harness#152):非负整数字符串 数值,转不出来即脏输入。
447
+ * `undefined` = 未传,落引擎缺省(50);`'0'` 是显式零值,不当「未传」兜掉。
448
+ * 判定规则正本是 `parseNumericFlag`(harness#154 统一解析器),此处保留 #152 的
449
+ * 函数面与报错文案(冻结测试钉死),只做委派。
286
450
  */
287
- export async function knowledgeSyncRag(options: KnowledgeOptions, io: CommandIO = processIO): Promise<CommandResult> {
288
- const projectPath = options.projectPath || process.cwd();
289
- const docsDir = `${projectPath}/.harness/knowledge-docs`;
451
+ function assembleShortContentThreshold(raw: string | undefined): ThresholdAssembly {
452
+ return parseNumericFlag(raw, 'int');
453
+ }
290
454
 
291
- if (!fs.existsSync(docsDir)) {
292
- log(io, chalk.yellow('No .harness/knowledge-docs/ directory'));
293
- return { kind: 'ok' };
455
+ export function knowledgeAuditView(options: KnowledgeAuditViewOptions, io: CommandIO) {
456
+ const audit = new KnowledgeAudit(openKnowledgeStore(options, io), {
457
+ shortContentThreshold: options.threshold,
458
+ });
459
+ const isDryRun = options.dryRun && !options.fix;
460
+ if (!isDryRun) announce(io, options.json, '🔍 知识库质量审计...\n');
461
+ const report = audit.run({ autoFix: options.fix && !isDryRun });
462
+ const showFixHint = !options.fix && report.issues.length > 0;
463
+
464
+ return {
465
+ data: report,
466
+ human: (): DisplayModel => ({ sections: auditSections(report, showFixHint) }),
467
+ };
468
+ }
469
+
470
+ function auditSections(report: AuditReport, showFixHint: boolean): DisplaySection[] {
471
+ const sections: DisplaySection[] = [{
472
+ rows: [
473
+ { cells: [{ label: ' 总条目: ', tone: 'emph' }, { field: 'totalEntries', text: String(report.totalEntries), tone: 'emph' }] },
474
+ { cells: [{ label: ' 健康分: ', tone: 'emph' }, { field: 'healthScore.before', text: `${report.healthScore.before}/100`, tone: 'emph' }] },
475
+ ],
476
+ }];
477
+
478
+ if (report.autoFixed > 0) {
479
+ sections[0].rows.push(
480
+ { cells: [{ label: ' 自动修复: ', tone: 'ok' }, { field: 'autoFixed', text: String(report.autoFixed), tone: 'ok' }] },
481
+ { cells: [{ label: ' 修复后: ', tone: 'emph' }, { field: 'healthScore.after', text: `${report.healthScore.after}/100`, tone: 'emph' }] },
482
+ );
483
+ }
484
+ sections[0].rows.push(blankLine());
485
+
486
+ const dimensions: DisplayRow[] = [];
487
+ for (const [key, dim] of Object.entries(report.dimensions) as Array<[keyof AuditReport['dimensions'], DimensionMetrics]>) {
488
+ dimensions.push({ cells: [
489
+ { field: `dimensions.${key}`, text: ` ${dimensionLabel(key)}: ` },
490
+ { field: `dimensions.${key}.score`, text: `${dim.score}/100`, tone: toneForScore(dim.score) },
491
+ { field: `dimensions.${key}.issues`, text: ` (${dim.issues} 问题)` },
492
+ ] });
493
+ }
494
+ dimensions.push(blankLine());
495
+ sections.push({ title: ' 维度评分:', rows: dimensions });
496
+
497
+ // 规则 label 正本在 audit.ts 的规则定义上(#109 编译期闭环经 ruleLabel 转发)
498
+ const rules: DisplayRow[] = [];
499
+ for (const [rule, count] of Object.entries(report.summary) as Array<[AuditRuleName, number]>) {
500
+ if (count === 0) continue;
501
+ rules.push({ cells: [{ field: `summary.${rule}`, text: ` ${ruleLabel(rule)}: ${count}`, tone: 'error' }] });
502
+ }
503
+ if (report.issues.length > 0) {
504
+ rules.push(blankLine()); // 「问题详情」标题前的空行(原排版是 `\n` 前缀)
505
+ }
506
+ sections.push({ rows: rules });
507
+
508
+ if (report.issues.length > 0) {
509
+ const issues: DisplayRow[] = [blankLine()];
510
+ for (const [i, issue] of report.issues.slice(0, 20).entries()) {
511
+ issues.push({ cells: [
512
+ { field: `issues.${i}.severity`, text: ` [${issue.severity}]`, tone: toneForSeverity(issue.severity) },
513
+ { field: `issues.${i}.entryId`, text: ` ${issue.entryId}: ` },
514
+ { field: `issues.${i}.title`, text: issue.title },
515
+ ] });
516
+ issues.push({ cells: [
517
+ { field: `issues.${i}.detail`, text: ` ${issue.detail}`, tone: 'muted' },
518
+ { label: ' → ' },
519
+ { field: `issues.${i}.action`, text: issue.action },
520
+ ] });
521
+ }
522
+ if (report.issues.length > 20) {
523
+ issues.push(blankLine(), { cells: [
524
+ { label: ' ... 还有 ', tone: 'muted' },
525
+ { field: 'issues.length', text: String(report.issues.length - 20), tone: 'muted' },
526
+ { label: ' 条', tone: 'muted' },
527
+ ] });
528
+ }
529
+ sections.push({ title: ' 问题详情 (前 20 条):', rows: issues });
294
530
  }
295
531
 
296
- const files = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
297
- if (files.length === 0) {
298
- log(io, chalk.gray('No knowledge docs found'));
299
- return { kind: 'ok' };
532
+ if (showFixHint) {
533
+ sections.push({ rows: [blankLine(), { cells: [{ label: ' 使用 --fix 自动修复', tone: 'warn' }] }] });
300
534
  }
535
+ return sections;
536
+ }
301
537
 
302
- if (options.json) {
303
- log(io, JSON.stringify({ directory: docsDir, files }, null, 2));
304
- return { kind: 'ok' };
538
+ export async function knowledgeAudit(options: KnowledgeAuditOptions, io: CommandIO = processIO): Promise<CommandResult> {
539
+ const threshold = assembleShortContentThreshold(options.threshold);
540
+ if (!threshold.ok) {
541
+ logError(io, `错误:--threshold 需要非负整数阈值(字符数),收到 "${threshold.raw}";短内容判定未执行`);
542
+ return { kind: 'usage-error', reason: `knowledge audit --threshold 非法阈值: "${threshold.raw}"` };
305
543
  }
544
+ const result = emitKnowledgeView(io, options, knowledgeAuditView({ ...options, threshold: threshold.value }, io));
545
+ // 审计可能改文件,人读路径收尾重建索引;--json 保持不写盘(现状冻结,#133 不动这条策略)
546
+ if (!options.json) {
547
+ new KnowledgeIndexGenerator(resolveKnowledgeBaseDir(options, io)).regenerate();
548
+ announce(io, options.json, ' 📇 索引已重建', 'muted');
549
+ }
550
+ return result;
551
+ }
552
+
553
+ // ========================================
554
+ // snapshot(KR4 存活率追踪)
555
+ // ========================================
556
+
557
+ export function knowledgeSnapshotView(options: KnowledgeOptions & KnowledgeDirOption, io: CommandIO) {
558
+ const snapshotPath = openKnowledgeStore(options, io).snapshot();
559
+ const data = { snapshotPath };
560
+
561
+ return {
562
+ data,
563
+ human: (): DisplayModel => ({
564
+ sections: [{
565
+ rows: [{
566
+ cells: [
567
+ { label: '✅ 快照已保存: ', tone: 'ok' },
568
+ { field: 'snapshotPath', text: snapshotPath, tone: 'ok' },
569
+ ],
570
+ }],
571
+ }],
572
+ }),
573
+ };
574
+ }
575
+
576
+ export function knowledgeSnapshot(options: KnowledgeOptions & KnowledgeDirOption, io: CommandIO = processIO): CommandResult {
577
+ return emitKnowledgeView(io, options, knowledgeSnapshotView(options, io));
578
+ }
579
+
580
+ // ========================================
581
+ // migrate(为现有条目补 AS-021 新字段)
582
+ // ========================================
583
+
584
+ /** migrateKnowledgeEntries 的返回面 = migrate 的 json 正本(该类型未从 knowledge/migration 导出,此处同构声明) */
585
+ type MigrateResult = { total: number; migrated: number; skipped: number; errors: string[] };
586
+
587
+ export function knowledgeMigrateView(options: KnowledgeOptions & KnowledgeDirOption, io: CommandIO): KnowledgeView<MigrateResult> {
588
+ const result = migrateKnowledgeEntries(resolveKnowledgeBaseDir(options, io));
589
+
590
+ return {
591
+ data: result,
592
+ human: (): DisplayModel => ({ sections: [{ rows: migrateRows(result) }] }),
593
+ };
594
+ }
306
595
 
307
- log(io, chalk.blue(`📄 RAG sync candidates: ${files.length} files in ${docsDir}\n`));
308
- for (const file of files) {
309
- log(io, chalk.cyan(` ${file}`));
596
+ function migrateRows(result: MigrateResult): DisplayRow[] {
597
+ const rows: DisplayRow[] = [
598
+ { cells: [{ label: '🔄 迁移知识条目(添加 consumptionMode/origin 字段)', tone: 'heading' }] },
599
+ blankLine(),
600
+ { cells: [{ label: ' 总计: ', tone: 'ok' }, { field: 'total', text: String(result.total), tone: 'ok' }, { label: ' 条', tone: 'ok' }] },
601
+ { cells: [{ label: ' 已迁移: ', tone: 'ok' }, { field: 'migrated', text: String(result.migrated), tone: 'ok' }, { label: ' 条', tone: 'ok' }] },
602
+ { cells: [{ label: ' 已跳过: ', tone: 'muted' }, { field: 'skipped', text: String(result.skipped), tone: 'muted' }, { label: ' 条', tone: 'muted' }] },
603
+ ];
604
+ if (result.errors.length > 0) {
605
+ rows.push(
606
+ blankLine(),
607
+ { cells: [{ label: ' 错误: ', tone: 'error' }, { field: 'errors.length', text: String(result.errors.length), tone: 'error' }, { label: ' 条', tone: 'error' }] },
608
+ );
609
+ result.errors.slice(0, 5).forEach((err, i) => {
610
+ rows.push({ cells: [{ label: ' ' }, { field: `errors.${i}`, text: err, tone: 'error' }] });
611
+ });
612
+ }
613
+ // 两种收尾语各自带一个前导空行;migrated=0 且有错误时历史上两句都不打
614
+ if (result.migrated === 0 && result.errors.length === 0) {
615
+ rows.push(blankLine(), { cells: [{ label: '✅ 所有条目已是最新,无需迁移', tone: 'ok' }] });
616
+ } else if (result.migrated > 0) {
617
+ rows.push(blankLine(), { cells: [{ label: '✅ 迁移完成', tone: 'ok' }] });
310
618
  }
311
- log(io);
312
- log(io, chalk.gray('Run `mcp__local-rag__ingest_file` for each to sync to RAG'));
313
- return { kind: 'ok' };
619
+ return rows;
314
620
  }
315
621
 
316
- /**
317
- * 飞轮健康检查 零 token 检测知识飞轮数据流状态
318
- */
319
- export async function knowledgeHealth(options: KnowledgeOptions & { dir?: string }, io: CommandIO = processIO): Promise<CommandResult> {
320
- const baseDir = options.dir || getKnowledgeDir(options.projectPath, io);
321
- const store = new KnowledgeStore({ baseDir });
622
+ export function knowledgeMigrate(options: KnowledgeOptions & KnowledgeDirOption, io: CommandIO = processIO): CommandResult {
623
+ return emitKnowledgeView(io, options, knowledgeMigrateView(options, io));
624
+ }
625
+
626
+ // ========================================
627
+ // index(重建 _index.md Agent grep,并同步 index.json)
628
+ // ========================================
629
+
630
+ export function knowledgeIndexView(options: KnowledgeOptions & KnowledgeDirOption, io: CommandIO) {
631
+ const baseDir = resolveKnowledgeBaseDir(options, io);
632
+ const indexPath = path.join(baseDir, '_index.md');
633
+ const beforeSize = fs.existsSync(indexPath) ? fs.statSync(indexPath).size : 0;
634
+
635
+ const output = new KnowledgeIndexGenerator(baseDir).regenerate();
636
+ // Sync index.json from disk files (removes ghost entries from manual mv/rm)
637
+ openKnowledgeStore({ dir: baseDir }, io).rebuildIndex();
638
+
639
+ const afterSize = Buffer.byteLength(output, 'utf-8');
640
+ const lineCount = output.split('\n').filter(l => !l.startsWith('#')).length;
641
+ const data = { path: indexPath, entries: lineCount, size: afterSize, previousSize: beforeSize };
642
+
643
+ return {
644
+ data,
645
+ human: (): DisplayModel => ({ sections: [{ rows: indexRows({ indexPath, afterSize, lineCount, beforeSize }) }] }),
646
+ };
647
+ }
648
+
649
+ function indexRows(shape: { indexPath: string; afterSize: number; lineCount: number; beforeSize: number }): DisplayRow[] {
650
+ const rows: DisplayRow[] = [
651
+ { cells: [{ label: '📇 索引已重建', tone: 'heading' }] },
652
+ { cells: [{ label: ' 路径: ', tone: 'muted' }, { field: 'path', text: shape.indexPath, tone: 'muted' }] },
653
+ { cells: [{ label: ' 条目: ', tone: 'ok' }, { field: 'entries', text: String(shape.lineCount), tone: 'ok' }] },
654
+ { cells: [{ label: ' 大小: ', tone: 'ok' }, { field: 'size', text: `${(shape.afterSize / 1024).toFixed(1)} KB`, tone: 'ok' }] },
655
+ ];
656
+ if (shape.beforeSize > 0) {
657
+ rows.push({ cells: [{ label: ' 旧大小: ', tone: 'muted' }, { field: 'previousSize', text: `${(shape.beforeSize / 1024).toFixed(1)} KB`, tone: 'muted' }] });
658
+ }
659
+ return rows;
660
+ }
661
+
662
+ export function knowledgeIndex(options: KnowledgeOptions & KnowledgeDirOption, io: CommandIO = processIO): CommandResult {
663
+ return emitKnowledgeView(io, options, knowledgeIndexView(options, io));
664
+ }
665
+
666
+ // ========================================
667
+ // health(飞轮健康检查 — 零 token 检测数据流状态)
668
+ // ========================================
669
+
670
+ interface HealthIssue {
671
+ severity: 'error' | 'warn' | 'info';
672
+ entry: string;
673
+ detail: string;
674
+ }
675
+
676
+ /** health 的 json 面正本(字段清单冻结在 __tests__/knowledge-view.test.ts) */
677
+ interface HealthData {
678
+ healthScore: number;
679
+ summary: {
680
+ total: number;
681
+ lowRefEntries: number;
682
+ staleEntries: number;
683
+ consumptionData: boolean;
684
+ refCoverage: number;
685
+ avgRefs: number;
686
+ };
687
+ issues: HealthIssue[];
688
+ }
689
+
690
+ export function knowledgeHealthView(options: KnowledgeOptions & KnowledgeDirOption, io: CommandIO) {
691
+ const store = openKnowledgeStore(options, io);
692
+ const baseDir = store.getBaseDir();
322
693
  const entries = store.list({ excludeArchived: true });
323
694
 
324
- const issues: Array<{ severity: 'error' | 'warn' | 'info'; entry: string; detail: string }> = [];
695
+ const issues: HealthIssue[] = [];
325
696
 
326
697
  // D1: 引用密度检查(低引用 = 可能孤立)
327
698
  let lowRefEntries = 0;
@@ -345,11 +716,9 @@ export async function knowledgeHealth(options: KnowledgeOptions & { dir?: string
345
716
  }
346
717
 
347
718
  // D3: 消费数据检查
348
- let consumptionData = false;
349
719
  const statsPath = path.join(baseDir, '.consumption-stats.json');
350
- if (fs.existsSync(statsPath)) {
351
- consumptionData = true;
352
- } else {
720
+ const consumptionData = fs.existsSync(statsPath);
721
+ if (!consumptionData) {
353
722
  issues.push({ severity: 'info', entry: '-', detail: `消费追踪数据不存在(${statsPath})` });
354
723
  }
355
724
 
@@ -358,268 +727,94 @@ export async function knowledgeHealth(options: KnowledgeOptions & { dir?: string
358
727
  const refCoverage = Math.round(metrics.refCoverage * 100);
359
728
  const avgRefs = Math.round(metrics.avgRefs * 10) / 10;
360
729
 
361
- // 健康分计算
362
730
  const totalIssues = issues.filter(i => i.severity === 'error').length * 3
363
731
  + issues.filter(i => i.severity === 'warn').length * 1;
364
732
  const healthScore = Math.max(0, 100 - totalIssues);
365
733
 
366
- if (options.json) {
367
- log(io, JSON.stringify({
368
- healthScore,
369
- summary: {
370
- total: entries.length,
371
- lowRefEntries,
372
- staleEntries,
373
- consumptionData,
374
- refCoverage,
375
- avgRefs,
376
- },
377
- issues: issues.slice(0, 50),
378
- }, null, 2));
379
- return { kind: 'ok' };
380
- }
381
-
382
- log(io, chalk.blue('🏥 飞轮健康检查\n'));
383
- log(io, chalk.bold(` 健康分: ${healthScore >= 80 ? chalk.green(healthScore) : healthScore >= 60 ? chalk.yellow(healthScore) : chalk.red(healthScore)}/100`));
384
- log(io, chalk.bold(` 活跃条目: ${entries.length}`));
385
- log(io);
386
-
387
- log(io, chalk.bold(' 数据流状态:'));
388
- log(io, ` 引用密度: ${lowRefEntries === 0 ? chalk.green('✓') : chalk.yellow(`⚠ ${lowRefEntries} 个零引用 verified`)}`);
389
- log(io, ` 新鲜度: ${staleEntries === 0 ? chalk.green('✓') : chalk.yellow(`⚠ ${staleEntries} 个过期 draft`)}`);
390
- log(io, ` 消费追踪: ${consumptionData ? chalk.green('✓') : chalk.gray('○ 未启用')}`);
391
- log(io);
392
-
393
- log(io, chalk.bold(' 飞轮指标:'));
394
- log(io, ` 引用覆盖: ${refCoverage}%`);
395
- log(io, ` 平均引用: ${avgRefs}`);
396
- log(io);
397
-
398
- if (issues.length > 0) {
399
- log(io, chalk.bold(` 问题 (前 20):`));
400
- for (const issue of issues.slice(0, 20)) {
401
- const sevColor = issue.severity === 'error' ? chalk.red
402
- : issue.severity === 'warn' ? chalk.yellow
403
- : chalk.gray;
404
- log(io, ` ${sevColor(`[${issue.severity}]`)} ${issue.entry}: ${issue.detail}`);
405
- }
406
- if (issues.length > 20) {
407
- log(io, chalk.gray(` ... 还有 ${issues.length - 20} 条`));
408
- }
409
- } else {
410
- log(io, chalk.green(' ✓ 无问题'));
411
- }
412
- return { kind: 'ok' };
413
- }
414
-
415
- /** 缺省知识库数据根(相对用户 home) */
416
- const KNOWLEDGE_DATA_DIR = path.join('.harness', 'knowledge');
417
- /** 旧缺省数据根(曾寄居 studio home),仍有数据时兼容沿用,免迁移 */
418
- const LEGACY_KNOWLEDGE_DATA_DIR = path.join('.studio', 'knowledge');
419
-
420
- let legacyKnowledgeDirNotified = false;
421
-
422
- function hasKnowledgeData(dir: string): boolean {
423
- try {
424
- return fs.existsSync(dir) && fs.readdirSync(dir).length > 0;
425
- } catch {
426
- return false;
427
- }
428
- }
429
-
430
- function getKnowledgeDir(projectPath: string | undefined, io: CommandIO): string {
431
- if (projectPath) return `${projectPath}/.harness/knowledge`;
432
- if (process.env.KNOWLEDGE_BASE_DIR) return process.env.KNOWLEDGE_BASE_DIR;
433
- const defaultDir = path.join(os.homedir(), KNOWLEDGE_DATA_DIR);
434
- const legacyDir = path.join(os.homedir(), LEGACY_KNOWLEDGE_DATA_DIR);
435
- if (hasKnowledgeData(legacyDir)) {
436
- if (!legacyKnowledgeDirNotified) {
437
- legacyKnowledgeDirNotified = true;
438
- // stderr:不污染 --json 的 stdout 输出
439
- logError(io, chalk.yellow(`⚠ 缺省知识库根已改为 ${defaultDir};旧目录 ${legacyDir} 仍有数据,本次沿用(免迁移,可用 KNOWLEDGE_BASE_DIR 覆盖)`));
440
- }
441
- return legacyDir;
442
- }
443
- return defaultDir;
444
- }
445
-
446
- /**
447
- * 知识库质量审计
448
- */
449
- export async function knowledgeAudit(options: KnowledgeOptions & {
450
- fix?: boolean;
451
- dryRun?: boolean;
452
- threshold?: number;
453
- dir?: string;
454
- }, io: CommandIO = processIO): Promise<CommandResult> {
455
- const audit = new KnowledgeAudit({
456
- baseDir: options.dir || getKnowledgeDir(options.projectPath, io),
457
- shortContentThreshold: options.threshold ? parseInt(options.threshold as any, 10) : undefined,
458
- });
459
-
460
- const isDryRun = options.dryRun && !options.fix;
461
-
462
- if (!options.json && !isDryRun) {
463
- log(io, chalk.blue('🔍 知识库质量审计...\n'));
464
- }
465
-
466
- const report = audit.run({ autoFix: options.fix && !isDryRun });
467
-
468
- if (options.json) {
469
- log(io, JSON.stringify(report, null, 2));
470
- return { kind: 'ok' };
471
- }
472
-
473
- // Summary
474
- log(io, chalk.bold(` 总条目: ${report.totalEntries}`));
475
- log(io, chalk.bold(` 健康分: ${report.healthScore.before}/100`));
476
- if (report.autoFixed > 0) {
477
- log(io, chalk.green(` 自动修复: ${report.autoFixed} 条`));
478
- log(io, chalk.bold(` 修复后: ${report.healthScore.after}/100`));
479
- }
480
- log(io);
481
-
482
- // Dimension scores(Record 全键标注 → 编译期闭环,#109)
483
- const dimLabels: Record<keyof AuditReport['dimensions'], string> = {
484
- structure: 'D1 结构完整性',
485
- content: 'D2 内容质量',
486
- dedup: 'D3 去重有效性',
487
- maturity: 'D4 成熟度健康',
488
- freshness: 'D5 新鲜度',
489
- flywheel: 'D6 飞轮验证',
490
- incremental: 'D7 增量存活',
734
+ const data: HealthData = {
735
+ healthScore,
736
+ summary: { total: entries.length, lowRefEntries, staleEntries, consumptionData, refCoverage, avgRefs },
737
+ issues: issues.slice(0, 50),
491
738
  };
492
- log(io, chalk.bold(' 维度评分:'));
493
- for (const [key, dim] of Object.entries(report.dimensions)) {
494
- const label = dimLabels[key as keyof AuditReport['dimensions']];
495
- const scoreColor = dim.score >= 80 ? chalk.green : dim.score >= 60 ? chalk.yellow : chalk.red;
496
- log(io, ` ${label}: ${scoreColor(`${dim.score}/100`)} (${dim.issues} 问题)`);
497
- }
498
- log(io);
499
-
500
- // Issues by rule(label 正本在 audit.ts 规则定义,AUDIT_RULE_LABELS 编译期闭环,#109)
501
- for (const [rule, count] of Object.entries(report.summary)) {
502
- if (count === 0) continue;
503
- const label = AUDIT_RULE_LABELS[rule as AuditRuleName];
504
- log(io, ` ${chalk.red(`${label}: ${count}`)}`);
505
- }
506
-
507
- // Detail (first 20)
508
- if (report.issues.length > 0) {
509
- log(io, chalk.bold(`\n 问题详情 (前 20 条):\n`));
510
- for (const issue of report.issues.slice(0, 20)) {
511
- const severityColor = issue.severity === 'critical' ? chalk.red
512
- : issue.severity === 'high' ? chalk.yellow
513
- : chalk.gray;
514
- log(io, ` ${severityColor(`[${issue.severity}]`)} ${issue.entryId}: ${issue.title}`);
515
- log(io, ` ${chalk.gray(issue.detail)} → ${issue.action}`);
516
- }
517
- if (report.issues.length > 20) {
518
- log(io, chalk.gray(`\n ... 还有 ${report.issues.length - 20} 条`));
519
- }
520
- }
521
-
522
- if (!options.fix && report.issues.length > 0) {
523
- log(io, chalk.yellow(`\n 使用 --fix 自动修复`));
524
- }
525
739
 
526
- // Auto-rebuild index after audit (files may have changed)
527
- const baseDir = options.dir || getKnowledgeDir(options.projectPath, io);
528
- const idxGen = new KnowledgeIndexGenerator(baseDir);
529
- idxGen.regenerate();
530
- if (!options.json) {
531
- log(io, chalk.gray(` 📇 索引已重建`));
532
- }
533
- return { kind: 'ok' };
740
+ return { data, human: (): DisplayModel => ({ sections: healthSections(data) }) };
534
741
  }
535
742
 
536
- /**
537
- * 知识库快照(KR4 存活率追踪)
538
- */
539
- export function knowledgeSnapshot(options: KnowledgeOptions & { dir?: string }, io: CommandIO = processIO): CommandResult {
540
- const store = new KnowledgeStore({ baseDir: options.dir || getKnowledgeDir(options.projectPath, io) });
541
- const snapPath = store.snapshot();
542
-
543
- if (options.json) {
544
- log(io, JSON.stringify({ snapshotPath: snapPath }));
545
- return { kind: 'ok' };
546
- }
547
-
548
- log(io, chalk.green(`✅ 快照已保存: ${snapPath}`));
549
- return { kind: 'ok' };
743
+ function healthSections(data: HealthData): DisplaySection[] {
744
+ const { healthScore, summary, issues } = data;
745
+ const scoreTone = toneForScore(healthScore);
746
+ return [
747
+ {
748
+ rows: [
749
+ { cells: [{ label: '🏥 飞轮健康检查', tone: 'heading' }] },
750
+ blankLine(),
751
+ { cells: [
752
+ { label: ' 健康分: ', tone: 'emph' },
753
+ { field: 'healthScore', text: String(healthScore), tone: scoreTone },
754
+ { label: '/100', tone: 'emph' },
755
+ ] },
756
+ { cells: [
757
+ { label: ' 活跃条目: ', tone: 'emph' },
758
+ { field: 'summary.total', text: String(summary.total), tone: 'emph' },
759
+ ] },
760
+ blankLine(),
761
+ ],
762
+ },
763
+ {
764
+ title: ' 数据流状态:',
765
+ rows: [
766
+ densityRow(' 引用密度: ', 'summary.lowRefEntries', summary.lowRefEntries, ' 个零引用 verified'),
767
+ densityRow(' 新鲜度: ', 'summary.staleEntries', summary.staleEntries, ' 个过期 draft'),
768
+ { cells: [
769
+ { label: ' 消费追踪: ' },
770
+ summary.consumptionData
771
+ ? { label: '✓', tone: 'ok' }
772
+ : { field: 'summary.consumptionData', text: '○ 未启用', tone: 'muted' },
773
+ ] },
774
+ blankLine(),
775
+ ],
776
+ },
777
+ {
778
+ title: ' 飞轮指标:',
779
+ rows: [
780
+ { cells: [{ label: ' 引用覆盖: ' }, { field: 'summary.refCoverage', text: `${summary.refCoverage}%` }] },
781
+ { cells: [{ label: ' 平均引用: ' }, { field: 'summary.avgRefs', text: String(summary.avgRefs) }] },
782
+ blankLine(),
783
+ ],
784
+ },
785
+ {
786
+ title: ' 问题 (前 20):',
787
+ rows: issues.length === 0
788
+ ? [{ cells: [{ label: ' ✓ 无问题', tone: 'ok' }] }]
789
+ : [
790
+ ...issues.slice(0, 20).map((issue, i) => ({
791
+ cells: [
792
+ { field: `issues.${i}.severity`, text: ` [${issue.severity}]`, tone: toneForSeverity(issue.severity) },
793
+ { field: `issues.${i}.entry`, text: ` ${issue.entry}: ` },
794
+ { field: `issues.${i}.detail`, text: issue.detail },
795
+ ],
796
+ })),
797
+ // 溢出计数取全量数组,而 json 面的 issues 已截 50 条 → 无字段可锚,登记派生量
798
+ ...(issues.length > 20
799
+ ? [{ cells: [{ derived: 'healthIssuesOverflow', text: ` ... 还有 ${issues.length - 20} 条`, tone: 'muted' as const }] }]
800
+ : []),
801
+ ],
802
+ },
803
+ ];
550
804
  }
551
805
 
552
- /**
553
- * 知识库迁移 为现有条目添加 AS-021 新字段
554
- */
555
- export function knowledgeMigrate(options: KnowledgeOptions & { dir?: string }, io: CommandIO = processIO): CommandResult {
556
- const baseDir = options.dir || getKnowledgeDir(options.projectPath, io);
557
-
558
- if (options.json) {
559
- const result = migrateKnowledgeEntries(baseDir);
560
- log(io, JSON.stringify(result, null, 2));
561
- return { kind: 'ok' };
562
- }
563
-
564
- log(io, chalk.blue('🔄 迁移知识条目(添加 consumptionMode/origin 字段)\n'));
565
- const result = migrateKnowledgeEntries(baseDir);
566
-
567
- log(io, chalk.green(` 总计: ${result.total} 条`));
568
- log(io, chalk.green(` 已迁移: ${result.migrated} 条`));
569
- log(io, chalk.gray(` 已跳过: ${result.skipped} 条`));
570
-
571
- if (result.errors.length > 0) {
572
- log(io, chalk.red(`\n 错误: ${result.errors.length} 条`));
573
- for (const err of result.errors.slice(0, 5)) {
574
- log(io, chalk.red(` ${err}`));
575
- }
576
- }
577
-
578
- if (result.migrated === 0 && result.errors.length === 0) {
579
- log(io, chalk.green('\n✅ 所有条目已是最新,无需迁移'));
580
- } else if (result.migrated > 0) {
581
- log(io, chalk.green(`\n✅ 迁移完成`));
582
- }
583
- return { kind: 'ok' };
806
+ function densityRow(prefix: string, field: string, count: number, suffix: string): DisplayRow {
807
+ if (count === 0) return { cells: [{ label: prefix }, { label: '✓', tone: 'ok' }] };
808
+ return {
809
+ cells: [
810
+ { label: prefix },
811
+ { label: '⚠ ', tone: 'warn' },
812
+ { field, text: String(count), tone: 'warn' },
813
+ { label: suffix, tone: 'warn' },
814
+ ],
815
+ };
584
816
  }
585
817
 
586
- /**
587
- * 知识库索引重建 生成 _index.md 供 Agent grep 使用,同时同步 index.json
588
- */
589
- export function knowledgeIndex(options: KnowledgeOptions & { dir?: string }, io: CommandIO = processIO): CommandResult {
590
- const baseDir = options.dir || getKnowledgeDir(options.projectPath, io);
591
- const gen = new KnowledgeIndexGenerator(baseDir);
592
-
593
- const beforeSize = (() => {
594
- const indexPath = path.join(baseDir, '_index.md');
595
- return fs.existsSync(indexPath) ? fs.statSync(indexPath).size : 0;
596
- })();
597
-
598
- const output = gen.regenerate();
599
-
600
- // Sync index.json from disk files (removes ghost entries from manual mv/rm)
601
- const store = new KnowledgeStore({ baseDir });
602
- store.rebuildIndex();
603
-
604
- const afterSize = Buffer.byteLength(output, 'utf-8');
605
- const lineCount = output.split('\n').filter(l => !l.startsWith('#')).length;
606
-
607
- if (options.json) {
608
- log(io, JSON.stringify({
609
- path: path.join(baseDir, '_index.md'),
610
- entries: lineCount,
611
- size: afterSize,
612
- previousSize: beforeSize,
613
- }));
614
- return { kind: 'ok' };
615
- }
616
-
617
- log(io, chalk.blue(`📇 索引已重建`));
618
- log(io, chalk.gray(` 路径: ${path.join(baseDir, '_index.md')}`));
619
- log(io, chalk.green(` 条目: ${lineCount}`));
620
- log(io, chalk.green(` 大小: ${(afterSize / 1024).toFixed(1)} KB`));
621
- if (beforeSize > 0) {
622
- log(io, chalk.gray(` 旧大小: ${(beforeSize / 1024).toFixed(1)} KB`));
623
- }
624
- return { kind: 'ok' };
818
+ export async function knowledgeHealth(options: KnowledgeOptions & KnowledgeDirOption, io: CommandIO = processIO): Promise<CommandResult> {
819
+ return emitKnowledgeView(io, options, knowledgeHealthView(options, io));
625
820
  }