akm-cli 0.9.0-rc.4 → 0.9.0-rc.6

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 (438) hide show
  1. package/CHANGELOG.md +122 -26
  2. package/README.md +7 -7
  3. package/SECURITY.md +11 -10
  4. package/dist/akm +2 -2
  5. package/dist/akm-migrate-storage +2 -2
  6. package/dist/assets/hints/cli-hints-full.md +60 -73
  7. package/dist/assets/hints/cli-hints-short.md +8 -12
  8. package/dist/assets/improve-strategies/default.json +5 -7
  9. package/dist/assets/improve-strategies/frequent.json +2 -2
  10. package/dist/assets/improve-strategies/proactive-maintenance.json +2 -2
  11. package/dist/assets/improve-strategies/quick.json +1 -1
  12. package/dist/assets/improve-strategies/reflect-distill.json +3 -3
  13. package/dist/assets/improve-strategies/thorough.json +1 -1
  14. package/dist/assets/prompts/consolidate-system.md +5 -5
  15. package/dist/assets/prompts/extract-session.md +2 -6
  16. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  17. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  18. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  19. package/dist/assets/stash-skeleton/README.md +10 -11
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +1 -1
  21. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +1 -1
  22. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +1 -1
  23. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +2 -2
  24. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +2 -2
  25. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +1 -1
  26. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +2 -2
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +1 -1
  28. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +18 -18
  29. package/dist/assets/stash-skeleton/facts/conventions/domains.md +8 -9
  30. package/dist/assets/stash-skeleton/facts/conventions/organization.md +13 -15
  31. package/dist/assets/tasks/core/extract.yml +2 -2
  32. package/dist/assets/tasks/core/improve.yml +1 -1
  33. package/dist/assets/templates/html/health.html +2 -3
  34. package/dist/cli/config-migrate.js +1328 -101
  35. package/dist/cli/invocation.js +255 -0
  36. package/dist/cli/parse-args.js +20 -83
  37. package/dist/cli/shared.js +15 -37
  38. package/dist/cli.js +18 -49
  39. package/dist/commands/agent/agent-dispatch.js +2 -2
  40. package/dist/commands/agent/contribute-cli.js +5 -4
  41. package/dist/commands/bundle/bundle-cli.js +68 -0
  42. package/dist/commands/bundle/bundle.js +219 -0
  43. package/dist/commands/completions.js +2 -2
  44. package/dist/commands/config-cli.js +6 -3
  45. package/dist/commands/env/env-binding.js +13 -9
  46. package/dist/commands/env/env-cli.js +49 -48
  47. package/dist/commands/env/secret-cli.js +24 -10
  48. package/dist/commands/events.js +2 -2
  49. package/dist/commands/feedback-cli.js +59 -33
  50. package/dist/commands/graph/graph.js +6 -4
  51. package/dist/commands/health/advisories.js +4 -4
  52. package/dist/commands/health/html-report.js +190 -593
  53. package/dist/commands/health/improve-metrics.js +240 -188
  54. package/dist/commands/health/metrics.js +9 -17
  55. package/dist/commands/health/report-view-model.js +509 -0
  56. package/dist/commands/health/surfaces.js +6 -3
  57. package/dist/commands/health/types-checks.js +4 -0
  58. package/dist/commands/health/types-improve.js +22 -0
  59. package/dist/commands/health/types-metrics.js +4 -0
  60. package/dist/commands/health/types-result.js +7 -0
  61. package/dist/commands/health/types-runs.js +4 -0
  62. package/dist/commands/health/types-session-log.js +4 -0
  63. package/dist/commands/health/types-windows.js +4 -0
  64. package/dist/commands/health/types.js +26 -21
  65. package/dist/commands/health/windows.js +1 -2
  66. package/dist/commands/health.js +219 -161
  67. package/dist/commands/improve/collapse-detector.js +25 -14
  68. package/dist/commands/improve/consolidate/chunking.js +7 -5
  69. package/dist/commands/improve/consolidate.js +640 -740
  70. package/dist/commands/improve/content-hash.js +39 -0
  71. package/dist/commands/improve/distill/content-repair.js +4 -10
  72. package/dist/commands/improve/distill/promote-memory.js +79 -49
  73. package/dist/commands/improve/distill/quality-gate.js +113 -35
  74. package/dist/commands/improve/distill-promotion-policy.js +24 -885
  75. package/dist/commands/improve/distill.js +565 -337
  76. package/dist/commands/improve/eligibility.js +75 -36
  77. package/dist/commands/improve/extract-cli.js +6 -6
  78. package/dist/commands/improve/extract-prompt.js +5 -34
  79. package/dist/commands/improve/extract.js +526 -357
  80. package/dist/commands/improve/feedback-valence.js +2 -12
  81. package/dist/commands/improve/improve-cli.js +11 -20
  82. package/dist/commands/improve/improve-result-file.js +7 -6
  83. package/dist/commands/improve/improve-run-types.js +4 -0
  84. package/dist/commands/improve/improve-strategies.js +3 -9
  85. package/dist/commands/improve/improve.js +822 -727
  86. package/dist/commands/improve/locks.js +27 -95
  87. package/dist/commands/improve/loop-stages.js +941 -963
  88. package/dist/commands/improve/memory/derived-ref.js +122 -0
  89. package/dist/commands/improve/memory/memory-contradiction-detect.js +35 -38
  90. package/dist/commands/improve/memory/memory-improve.js +24 -37
  91. package/dist/commands/improve/outcome-loop.js +13 -33
  92. package/dist/commands/improve/preparation.js +889 -691
  93. package/dist/commands/improve/proactive-maintenance.js +37 -9
  94. package/dist/commands/improve/proposal-envelope.js +31 -0
  95. package/dist/commands/improve/reflect.js +974 -588
  96. package/dist/commands/improve/run-context.js +119 -0
  97. package/dist/commands/improve/salience.js +8 -4
  98. package/dist/commands/improve/session-asset.js +7 -3
  99. package/dist/commands/improve/shared.js +14 -40
  100. package/dist/commands/improve/source-identity.js +39 -26
  101. package/dist/commands/improve/triage.js +20 -17
  102. package/dist/commands/lint/base-linter.js +288 -295
  103. package/dist/commands/lint/index.js +132 -19
  104. package/dist/commands/mv-cli.js +257 -234
  105. package/dist/commands/observability-cli.js +2 -1
  106. package/dist/commands/proposal/diff-format.js +50 -0
  107. package/dist/commands/proposal/drain-policies.js +0 -6
  108. package/dist/commands/proposal/drain.js +17 -16
  109. package/dist/commands/proposal/proposal-cli.js +40 -77
  110. package/dist/commands/proposal/proposal-types.js +56 -0
  111. package/dist/commands/proposal/proposal.js +39 -1
  112. package/dist/commands/proposal/propose.js +41 -19
  113. package/dist/commands/proposal/repository.js +566 -486
  114. package/dist/commands/proposal/validators/proposal-quality-validators.js +22 -6
  115. package/dist/commands/proposal/validators/proposal-validators.js +6 -5
  116. package/dist/commands/proposal/validators/proposals.js +5 -4
  117. package/dist/commands/read/curate.js +50 -28
  118. package/dist/commands/read/knowledge.js +25 -16
  119. package/dist/commands/read/remember-cli.js +12 -4
  120. package/dist/commands/read/search-cli.js +33 -33
  121. package/dist/commands/read/search.js +68 -55
  122. package/dist/commands/read/show.js +54 -135
  123. package/dist/commands/remember.js +7 -5
  124. package/dist/commands/sources/add-cli.js +10 -24
  125. package/dist/commands/sources/bundle-config-ops.js +58 -0
  126. package/dist/commands/sources/history.js +13 -8
  127. package/dist/commands/sources/info.js +10 -11
  128. package/dist/commands/sources/init.js +12 -21
  129. package/dist/commands/sources/installed-stashes.js +151 -138
  130. package/dist/commands/sources/schema-repair.js +5 -3
  131. package/dist/commands/sources/self-update.js +9 -7
  132. package/dist/commands/sources/source-add.js +116 -154
  133. package/dist/commands/sources/source-clone.js +33 -14
  134. package/dist/commands/sources/source-manage.js +43 -32
  135. package/dist/commands/sources/sources-cli.js +12 -7
  136. package/dist/commands/sources/stash-cli.js +12 -5
  137. package/dist/commands/sources/stash-skeleton.js +1 -1
  138. package/dist/commands/tasks/default-tasks.js +13 -13
  139. package/dist/commands/tasks/tasks-cli.js +3 -3
  140. package/dist/commands/tasks/tasks.js +71 -31
  141. package/dist/commands/workflow-cli.js +22 -26
  142. package/dist/core/action-contributors.js +1 -1
  143. package/dist/core/activation-policy.js +49 -0
  144. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  145. package/dist/core/adapter/adapters/akm-adapter.js +509 -0
  146. package/dist/core/adapter/adapters/akm-lint.js +381 -0
  147. package/dist/core/adapter/adapters/akm-metadata.js +416 -0
  148. package/dist/core/adapter/adapters/akm-task-adapter.js +150 -0
  149. package/dist/core/adapter/adapters/akm-workflow-adapter.js +196 -0
  150. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  151. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  152. package/dist/core/adapter/adapters/generic-files-adapter.js +117 -0
  153. package/dist/core/adapter/adapters/index.js +80 -0
  154. package/dist/core/adapter/adapters/llm-wiki-adapter.js +413 -0
  155. package/dist/core/adapter/adapters/okf-adapter.js +223 -0
  156. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  157. package/dist/core/adapter/adapters/shared.js +286 -0
  158. package/dist/core/adapter/adapters/tool-dir-shared.js +212 -0
  159. package/dist/core/adapter/adapters/website-snapshot-adapter.js +154 -0
  160. package/dist/core/adapter/bundle-adapter.js +4 -0
  161. package/dist/core/adapter/recognize-match.js +42 -0
  162. package/dist/core/adapter/registry.js +56 -0
  163. package/dist/core/adapter/types.js +4 -0
  164. package/dist/core/asset/asset-placement.js +229 -0
  165. package/dist/core/asset/asset-ref.js +107 -78
  166. package/dist/core/asset/asset-serialize.js +20 -0
  167. package/dist/core/asset/frontmatter.js +1 -1
  168. package/dist/core/asset/resolve-ref.js +223 -0
  169. package/dist/core/asset/stash-meta.js +1 -1
  170. package/dist/core/common.js +141 -49
  171. package/dist/core/config/config-io.js +0 -108
  172. package/dist/core/config/config-schema.js +94 -1039
  173. package/dist/core/config/config-sources.js +115 -36
  174. package/dist/core/config/config-types.js +16 -11
  175. package/dist/core/config/config-walker.js +4 -2
  176. package/dist/core/config/config.js +21 -26
  177. package/dist/core/config/engine-semantics.js +0 -4
  178. package/dist/core/config/schema/embedding.js +38 -0
  179. package/dist/core/config/schema/engines.js +116 -0
  180. package/dist/core/config/schema/feedback.js +31 -0
  181. package/dist/core/config/schema/improve-processes.js +395 -0
  182. package/dist/core/config/schema/improve.js +76 -0
  183. package/dist/core/config/schema/index-config.js +183 -0
  184. package/dist/core/config/schema/output.js +15 -0
  185. package/dist/core/config/schema/primitives.js +94 -0
  186. package/dist/core/config/schema/search.js +30 -0
  187. package/dist/core/config/schema/setup.js +27 -0
  188. package/dist/core/config/schema/sources-bundles.js +164 -0
  189. package/dist/core/config/schema/workflow.js +29 -0
  190. package/dist/core/env-secret-ref.js +113 -8
  191. package/dist/core/errors.js +1 -1
  192. package/dist/core/events-types.js +4 -0
  193. package/dist/core/extra-params.js +1 -0
  194. package/dist/core/file-change.js +16 -0
  195. package/dist/core/fs-txn.js +316 -0
  196. package/dist/core/git-message.js +59 -0
  197. package/dist/core/improve-result.js +9 -1
  198. package/dist/core/logs-db.js +1 -1
  199. package/dist/core/maintenance-barrier.js +16 -0
  200. package/dist/core/migration-backup.js +332 -160
  201. package/dist/core/paths.js +3 -6
  202. package/dist/core/platform.js +10 -0
  203. package/dist/core/recognition-util.js +130 -0
  204. package/dist/core/redaction.js +2 -2
  205. package/dist/core/standards/resolve-standards-context.js +47 -64
  206. package/dist/core/standards/resolve-type-conventions.js +3 -3
  207. package/dist/core/state/migrations.js +201 -8
  208. package/dist/core/state-db.js +38 -2
  209. package/dist/core/subprocess.js +303 -0
  210. package/dist/core/time.js +20 -0
  211. package/dist/core/type-presentation.js +130 -0
  212. package/dist/core/write-source.js +39 -67
  213. package/dist/indexer/bundle-identity-guard.js +91 -0
  214. package/dist/indexer/db/graph-db.js +1 -1
  215. package/dist/indexer/db/llm-cache.js +1 -1
  216. package/dist/indexer/ensure-index.js +29 -9
  217. package/dist/indexer/graph/graph-boost.js +28 -24
  218. package/dist/indexer/graph/graph-extraction.js +5 -6
  219. package/dist/indexer/graph/graph-types.js +4 -0
  220. package/dist/indexer/index-written-assets.js +44 -17
  221. package/dist/indexer/indexer.js +289 -153
  222. package/dist/indexer/init.js +18 -25
  223. package/dist/indexer/installations.js +224 -0
  224. package/dist/indexer/passes/dir-staleness.js +2 -1
  225. package/dist/indexer/passes/memory-inference.js +8 -4
  226. package/dist/indexer/passes/metadata.js +96 -243
  227. package/dist/indexer/scan/doc-to-entry.js +123 -0
  228. package/dist/indexer/scan/drain-dir.js +144 -0
  229. package/dist/indexer/search/db-search.js +171 -99
  230. package/dist/indexer/search/fts-query.js +1 -1
  231. package/dist/indexer/search/ranking-contributors.js +57 -7
  232. package/dist/indexer/search/ranking-types.js +4 -0
  233. package/dist/indexer/search/ranking.js +31 -6
  234. package/dist/indexer/search/search-attribution.js +67 -0
  235. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  236. package/dist/indexer/search/search-source.js +109 -52
  237. package/dist/indexer/search/semantic-status.js +4 -1
  238. package/dist/indexer/usage/unmigrated-vaults-guard.js +2 -1
  239. package/dist/indexer/usage/usage-events.js +72 -9
  240. package/dist/indexer/walk/file-context.js +1 -44
  241. package/dist/indexer/walk/matchers.js +16 -32
  242. package/dist/indexer/walk/path-resolver.js +6 -5
  243. package/dist/indexer/walk/walker.js +4 -2
  244. package/dist/integrations/agent/engine-resolution.js +22 -0
  245. package/dist/integrations/agent/model-aliases.js +1 -1
  246. package/dist/integrations/agent/prompts.js +35 -11
  247. package/dist/integrations/agent/spawn.js +41 -270
  248. package/dist/integrations/harnesses/aider/agent-builder.js +2 -2
  249. package/dist/integrations/harnesses/aider/index.js +2 -11
  250. package/dist/integrations/harnesses/amazonq/agent-builder.js +1 -9
  251. package/dist/integrations/harnesses/amazonq/index.js +3 -16
  252. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  253. package/dist/integrations/harnesses/claude/index.js +1 -12
  254. package/dist/integrations/harnesses/claude/session-log.js +27 -65
  255. package/dist/integrations/harnesses/codex/agent-builder.js +1 -2
  256. package/dist/integrations/harnesses/codex/index.js +2 -12
  257. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -3
  258. package/dist/integrations/harnesses/copilot/index.js +1 -12
  259. package/dist/integrations/harnesses/gemini/agent-builder.js +1 -3
  260. package/dist/integrations/harnesses/gemini/index.js +1 -12
  261. package/dist/integrations/harnesses/ids.js +24 -0
  262. package/dist/integrations/harnesses/index.js +27 -2
  263. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  264. package/dist/integrations/harnesses/opencode/index.js +1 -12
  265. package/dist/integrations/harnesses/opencode/session-log.js +67 -110
  266. package/dist/integrations/harnesses/opencode-sdk/harness.js +2 -11
  267. package/dist/integrations/harnesses/openhands/agent-builder.js +3 -3
  268. package/dist/integrations/harnesses/openhands/index.js +2 -11
  269. package/dist/integrations/harnesses/pi/agent-builder.js +3 -11
  270. package/dist/integrations/harnesses/pi/index.js +3 -15
  271. package/dist/integrations/harnesses/shared.js +17 -0
  272. package/dist/integrations/harnesses/types.js +37 -2
  273. package/dist/integrations/lockfile.js +36 -0
  274. package/dist/integrations/session-logs/index.js +21 -12
  275. package/dist/integrations/session-logs/provider-base.js +113 -0
  276. package/dist/llm/client.js +7 -5
  277. package/dist/llm/embedders/deterministic.js +2 -2
  278. package/dist/llm/embedders/remote.js +8 -4
  279. package/dist/llm/graph-extract.js +7 -2
  280. package/dist/llm/metadata-enhance.js +43 -26
  281. package/dist/llm/structured-call.js +15 -7
  282. package/dist/migrate/legacy/config-source-migration.js +223 -0
  283. package/dist/migrate/legacy/content-migration.js +305 -0
  284. package/dist/migrate/legacy/legacy-layout.js +779 -0
  285. package/dist/migrate/legacy/legacy-paths.js +25 -0
  286. package/dist/migrate/legacy/legacy-stash-json.js +72 -0
  287. package/dist/migrate/legacy/proposal-fs-import.js +168 -0
  288. package/dist/migrate/legacy/task-target-ref-migration.js +272 -0
  289. package/dist/migrate/legacy/three-db-cutover.js +840 -0
  290. package/dist/migrate/legacy/workflow-migrations-bodies.js +52 -0
  291. package/dist/migrate/legacy/workflow-migrations-frozen.js +21 -0
  292. package/dist/migrate/legacy-ref-grammar.js +209 -0
  293. package/dist/output/command-registry.js +27 -0
  294. package/dist/output/html-render.js +11 -16
  295. package/dist/output/renderers.js +32 -276
  296. package/dist/output/shapes/passthrough.js +3 -9
  297. package/dist/output/shapes/registry.js +12 -6
  298. package/dist/output/text/command-format.js +547 -0
  299. package/dist/output/text/helpers.js +15 -1377
  300. package/dist/output/text/proposal-format.js +230 -0
  301. package/dist/output/text/registry.js +12 -6
  302. package/dist/output/text/show-directives.js +107 -0
  303. package/dist/output/text/show-format.js +103 -0
  304. package/dist/output/text/workflow-format.js +345 -0
  305. package/dist/output/text.js +1 -3
  306. package/dist/registry/build-index.js +13 -17
  307. package/dist/registry/providers/static-index.js +2 -2
  308. package/dist/registry/resolve.js +4 -90
  309. package/dist/registry/semver.js +93 -0
  310. package/dist/runtime.js +90 -0
  311. package/dist/schemas/akm-config.json +2956 -14890
  312. package/dist/schemas/akm-task.json +2 -2
  313. package/dist/schemas/akm-workflow.json +2 -2
  314. package/dist/scripts/migrate-storage.js +2312 -19642
  315. package/dist/setup/detect.js +5 -7
  316. package/dist/setup/engine-config.js +2 -1
  317. package/dist/setup/registry-stash-loader.js +1 -1
  318. package/dist/setup/semantic-assets.js +12 -9
  319. package/dist/setup/setup.js +109 -39
  320. package/dist/setup/steps/connection-shared.js +120 -0
  321. package/dist/setup/steps/connection.js +50 -274
  322. package/dist/setup/steps/platforms.js +1 -0
  323. package/dist/setup/steps/sources.js +13 -6
  324. package/dist/setup/steps/stashdir.js +5 -2
  325. package/dist/sources/freshness.js +39 -0
  326. package/dist/sources/providers/git-provider.js +29 -33
  327. package/dist/sources/providers/git-stash.js +10 -5
  328. package/dist/sources/providers/provider-utils.js +40 -18
  329. package/dist/sources/providers/website.js +1 -1
  330. package/dist/sources/resolve.js +5 -5
  331. package/dist/sources/snapshot-fetchers/types.js +4 -0
  332. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +105 -39
  333. package/dist/storage/database.js +47 -3
  334. package/dist/storage/engines/sqlite-migrations.js +34 -16
  335. package/dist/storage/locations.js +1 -2
  336. package/dist/storage/repositories/improve-runs-repository.js +0 -20
  337. package/dist/storage/repositories/index-connection.js +80 -0
  338. package/dist/storage/repositories/index-db.js +4 -3
  339. package/dist/storage/repositories/index-entries-repository.js +952 -0
  340. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +15 -2
  341. package/dist/storage/repositories/index-entry-types.js +4 -0
  342. package/dist/storage/repositories/index-fts-repository.js +164 -0
  343. package/dist/storage/repositories/index-llm-cache-repository.js +109 -0
  344. package/dist/storage/repositories/index-meta-repository.js +50 -0
  345. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +241 -100
  346. package/dist/storage/repositories/index-sql.js +12 -0
  347. package/dist/storage/repositories/index-utility-repository.js +369 -0
  348. package/dist/storage/repositories/index-vec-repository.js +245 -0
  349. package/dist/storage/repositories/proposals-repository.js +41 -50
  350. package/dist/storage/repositories/registry-cache.js +1 -1
  351. package/dist/storage/repositories/workflow-runs-repository.js +17 -11
  352. package/dist/tasks/backends/cron.js +5 -5
  353. package/dist/tasks/backends/exec-utils.js +21 -0
  354. package/dist/tasks/backends/index.js +6 -9
  355. package/dist/tasks/backends/launchd.js +16 -17
  356. package/dist/tasks/backends/schtasks.js +33 -41
  357. package/dist/tasks/backends/types.js +4 -0
  358. package/dist/tasks/parser.js +25 -5
  359. package/dist/tasks/runner.js +33 -39
  360. package/dist/tasks/validator.js +5 -6
  361. package/dist/workflows/authoring/authoring.js +8 -11
  362. package/dist/workflows/exec/brief.js +10 -7
  363. package/dist/workflows/exec/report.js +261 -156
  364. package/dist/workflows/exec/run-workflow.js +2 -1
  365. package/dist/workflows/exec/step-work.js +3 -9
  366. package/dist/workflows/program/parser.js +4 -60
  367. package/dist/workflows/renderer.js +11 -60
  368. package/dist/workflows/runtime/checkin.js +1 -1
  369. package/dist/workflows/runtime/plan-classifier.js +7 -4
  370. package/dist/workflows/runtime/runs.js +11 -10
  371. package/dist/workflows/runtime/unit-checkin.js +1 -1
  372. package/dist/workflows/runtime/unit-phases.js +20 -0
  373. package/dist/workflows/runtime/workflow-asset-loader.js +27 -16
  374. package/dist/workflows/validate-summary.js +2 -2
  375. package/dist/workflows/validator.js +18 -2
  376. package/docs/migration/release-notes/0.9.0.md +45 -3
  377. package/docs/migration/v0.8-to-v0.9.md +466 -262
  378. package/docs/{data-and-telemetry.md → reference/data-and-telemetry.md} +61 -17
  379. package/package.json +10 -9
  380. package/schemas/akm-config.json +2956 -14890
  381. package/schemas/akm-task.json +2 -2
  382. package/schemas/akm-workflow.json +2 -2
  383. package/dist/assets/help/help-accept.md +0 -12
  384. package/dist/assets/help/help-improve.md +0 -84
  385. package/dist/assets/help/help-proposals.md +0 -17
  386. package/dist/assets/help/help-propose.md +0 -17
  387. package/dist/assets/help/help-reject.md +0 -11
  388. package/dist/assets/improve-strategies/recombine-only.json +0 -23
  389. package/dist/assets/improve-strategies/synthesize.json +0 -17
  390. package/dist/assets/prompts/procedural-system.md +0 -44
  391. package/dist/assets/prompts/recombine-system.md +0 -40
  392. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  393. package/dist/assets/templates/html/default.html +0 -78
  394. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  395. package/dist/assets/wiki/index-template.md +0 -12
  396. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  397. package/dist/assets/wiki/log-template.md +0 -8
  398. package/dist/assets/wiki/schema-template.md +0 -61
  399. package/dist/commands/improve/calibration.js +0 -161
  400. package/dist/commands/improve/dedup.js +0 -482
  401. package/dist/commands/improve/hot-probation.js +0 -45
  402. package/dist/commands/improve/improve-auto-accept.js +0 -262
  403. package/dist/commands/improve/procedural.js +0 -403
  404. package/dist/commands/improve/recombine.js +0 -843
  405. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  406. package/dist/commands/lint/agent-linter.js +0 -44
  407. package/dist/commands/lint/command-linter.js +0 -44
  408. package/dist/commands/lint/default-linter.js +0 -16
  409. package/dist/commands/lint/fact-linter.js +0 -39
  410. package/dist/commands/lint/knowledge-linter.js +0 -16
  411. package/dist/commands/lint/memory-linter.js +0 -61
  412. package/dist/commands/lint/registry.js +0 -41
  413. package/dist/commands/lint/skill-linter.js +0 -45
  414. package/dist/commands/lint/task-linter.js +0 -50
  415. package/dist/commands/lint/workflow-linter.js +0 -81
  416. package/dist/commands/proposal/legacy-import.js +0 -115
  417. package/dist/commands/wiki-cli.js +0 -291
  418. package/dist/core/asset/asset-registry.js +0 -76
  419. package/dist/core/asset/asset-spec.js +0 -316
  420. package/dist/core/eval/rank-metrics.js +0 -113
  421. package/dist/core/ripgrep/install.js +0 -163
  422. package/dist/core/ripgrep/resolve.js +0 -81
  423. package/dist/indexer/db/db.js +0 -1585
  424. package/dist/indexer/manifest.js +0 -170
  425. package/dist/indexer/passes/metadata-contributors.js +0 -31
  426. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -25
  427. package/dist/output/text/wiki.js +0 -16
  428. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -18228
  429. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  430. package/dist/storage/repositories/consolidation-repository.js +0 -38
  431. package/dist/storage/repositories/recombine-repository.js +0 -213
  432. package/dist/wiki/wiki-templates.js +0 -15
  433. package/dist/wiki/wiki.js +0 -1013
  434. package/dist/workflows/db.js +0 -413
  435. package/docs/README.md +0 -103
  436. package/docs/migration/release-notes/0.9.0-beta.60.md +0 -19
  437. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  438. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -9,163 +9,40 @@
9
9
  * strict superset of the skill's report.html; this module computes the 17
10
10
  * `%%TOKEN%%` replacements it consumes.
11
11
  *
12
+ * Structure (chunk-9 WI-9.5a / H2, anchors C.6): `./report-view-model.ts`
13
+ * holds the pure `AkmHealthResult`→`HealthReportViewModel` extractor — every
14
+ * computed number/string/series the report shows, with NO HTML. This module
15
+ * holds the thin VM→HTML fragment renderers (one per template section) plus
16
+ * `buildHealthHtmlReplacements`, the extract-then-render glue that produces
17
+ * the 17-token replacement map.
18
+ *
12
19
  * Determinism: nothing here depends on Date.now()/Math.random(). Runs are
13
20
  * sorted by startedAt; `%%GENERATED_AT%%` derives from the latest run (or the
14
21
  * window anchor), so output is byte-identical for identical inputs.
22
+ *
23
+ * ECharts delivery (chunk-9 WI-9.4d, anchors C.1): CDN-only. The vendored
24
+ * `~1MB` `echarts.min.js` self-contained/"inline" mode was removed along with
25
+ * the asset itself — `buildEchartsTag` always emits a `<script src>` tag
26
+ * pointed at the jsDelivr CDN, so viewing the report now requires network
27
+ * access. BEHAVIOR CHANGE (plan-accepted, ledgered): previously
28
+ * `AKM_ECHARTS=cdn` opted IN to the CDN with "inline" as the default; there is
29
+ * no more env var or option to opt back into an offline report.
15
30
  */
16
- import fs from "node:fs";
17
- import path from "node:path";
18
31
  import { escapeHtml } from "../../output/html-render.js";
19
- import { getDirname } from "../../runtime.js";
20
32
  import { pkgVersion } from "../../version.js";
21
- import { TASK_FAIL_RATE_WARN, } from "./types.js";
22
- /**
23
- * Distill skip-reasons hidden from the breakdown chart. `no new signal since
24
- * last proposal` is the steady-state "nothing changed, nothing to do" outcome —
25
- * it dominates the histogram and drowns out the actionable reasons, so it is
26
- * intentionally excluded from the chart (the count still lives in the data).
27
- */
28
- const DISTILL_REASONS_HIDDEN = new Set(["no new signal since last proposal"]);
29
- const ECHARTS_CDN = "https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";
30
- const ECHARTS_VENDOR_PATH = path.join(getDirname(import.meta.url), "../../assets/templates/html/vendor/echarts.min.js");
31
- // ── Small formatters (ports of render.py helpers) ───────────────────────────
33
+ import { buildHealthReportViewModel, compact, fmtMs, humanize, num, } from "./report-view-model.js";
32
34
  const esc = escapeHtml;
33
35
  /** Emit a <time> element that the browser's JS will reformat to the viewer's local timezone. */
34
36
  function isoTimeTag(iso) {
35
37
  const fallback = iso.slice(0, 16).replace("T", " ");
36
38
  return `<time data-iso="${esc(iso)}">${esc(fallback)}</time>`;
37
39
  }
38
- function num(value) {
39
- return Math.round(value).toLocaleString("en-US");
40
- }
41
- /** Compact token/count formatter, e.g. 127345 → "127K", 1_500_000 → "1.5M". */
42
- function compact(value) {
43
- const v = Math.round(value);
44
- if (Math.abs(v) >= 1_000_000)
45
- return `${(v / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
46
- if (Math.abs(v) >= 1_000)
47
- return `${(v / 1_000).toFixed(1).replace(/\.0$/, "")}K`;
48
- return String(v);
49
- }
50
- /** Humanize a camelCase / kebab / snake enum into reader-facing text. */
51
- function humanize(raw) {
52
- return raw
53
- .replace(/[-_]/g, " ")
54
- .replace(/([a-z])([A-Z])/g, "$1 $2")
55
- .replace(/\s+/g, " ")
56
- .trim()
57
- .replace(/^./, (c) => c.toUpperCase());
58
- }
59
- function fmtMs(ms) {
60
- return ms ? `${(ms / 60000).toFixed(1)}m` : "—";
61
- }
62
- function pct(rate, digits) {
63
- return `${(rate * 100).toFixed(digits)}%`;
64
- }
65
40
  function trendClass(direction) {
66
41
  return direction === "up" ? "trend-up" : direction === "down" ? "trend-down" : "trend-flat";
67
42
  }
68
43
  function trendLabel(direction) {
69
44
  return direction === "up" ? "▲ up" : direction === "down" ? "▼ watch" : "— flat";
70
45
  }
71
- /** pctChange may be a number or an "+inf"/"-inf" sentinel (prior window was 0). */
72
- function coercePct(raw) {
73
- if (typeof raw === "number")
74
- return raw;
75
- if (typeof raw !== "string")
76
- return undefined;
77
- const s = raw.trim().toLowerCase();
78
- if (s === "+inf" || s === "inf" || s === "infinity" || s === "+infinity")
79
- return 1e9;
80
- if (s === "-inf" || s === "-infinity")
81
- return -1e9;
82
- const parsed = Number.parseFloat(s.replace(/%$/, ""));
83
- return Number.isFinite(parsed) ? parsed : undefined;
84
- }
85
- function reshapeRun(r) {
86
- const cons = r.consolidation;
87
- const mi = r.memoryInference;
88
- const ge = r.graphExtraction;
89
- const wall = r.wallTimeMs || 0;
90
- const consMs = cons.durationMs || 0;
91
- const miMs = mi.durationMs || 0;
92
- const geMs = ge.durationMs || 0;
93
- return {
94
- id: r.id,
95
- resultStatus: r.resultStatus ?? "valid",
96
- resultComplete: r.resultComplete ?? (r.resultStatus === undefined || r.resultStatus === "valid"),
97
- taskId: r.taskId ?? "manual",
98
- strategy: r.strategy,
99
- legacyProfile: r.legacyProfile,
100
- startedAt: r.startedAt,
101
- completedAt: r.completedAt,
102
- wallTimeMs: wall,
103
- ok: r.ok,
104
- mode: r.scope.mode,
105
- consDurationMs: consMs,
106
- miDurationMs: miMs,
107
- geDurationMs: geMs,
108
- otherMs: Math.max(0, wall - consMs - miMs - geMs),
109
- consRan: cons.ran,
110
- promoted: cons.promoted,
111
- merged: cons.merged,
112
- deleted: cons.deleted,
113
- contradicted: cons.contradicted,
114
- judgedNoAction: cons.judgedNoAction,
115
- processed: cons.processed,
116
- failedChunks: cons.failedChunks,
117
- totalChunks: cons.totalChunks,
118
- miWritten: mi.written || mi.writes || 0,
119
- miConsidered: mi.considered,
120
- miYieldRate: mi.yieldRate,
121
- miCacheHits: mi.cacheHits,
122
- geEntities: ge.entities,
123
- geRelations: ge.relations,
124
- geCacheHitRate: ge.cacheHitRate,
125
- geFailures: ge.failures,
126
- distillSkipped: r.actions.distill.skipped,
127
- distillQueued: r.actions.distill.queued,
128
- distillLlmFailed: r.actions.distill.llmFailed,
129
- distillByReason: r.actions.distill.skippedByReason,
130
- reflectOk: r.actions.reflect.ok,
131
- reflectFailed: r.actions.reflect.failed,
132
- derived: r.memorySummary.derived,
133
- eligible: r.memorySummary.eligible,
134
- lintFlagged: r.lintFlagged,
135
- lintFixed: r.lintFixed,
136
- reflectsWithErrorContext: r.reflectsWithErrorContext,
137
- orphansPurged: r.orphansPurged,
138
- evalCasesWritten: r.evalCasesWritten,
139
- };
140
- }
141
- // ── Trend classification (port of collect.py classify) ──────────────────────
142
- function classify(deltas, metricKeys, lowerIsBetter = false) {
143
- const changes = metricKeys
144
- .map((key) => coercePct(deltas[key]?.pctChange))
145
- .filter((c) => c !== undefined);
146
- if (changes.length === 0)
147
- return "flat";
148
- const avg = changes.reduce((acc, c) => acc + c, 0) / changes.length;
149
- if (Math.abs(avg) < 5)
150
- return "flat";
151
- const direction = avg > 0 ? "up" : "down";
152
- if (lowerIsBetter)
153
- return avg > 0 ? "down" : "up";
154
- return direction;
155
- }
156
- function buildTrend(deltas) {
157
- const decisionQuality = classify(deltas, ["improve.memoryInference.yieldRate", "improve.consolidation.promoted"]);
158
- const outputVolume = classify(deltas, [
159
- "improve.consolidation.promoted",
160
- "improve.memoryInference.written",
161
- "improve.sessionExtraction.proposalsCreated",
162
- ]);
163
- const failures = classify(deltas, ["improve.graphExtraction.failures"], true);
164
- const latency = classify(deltas, ["improve.wallTime.medianMs", "improve.wallTime.p95Ms"], true);
165
- const score = [decisionQuality, outputVolume, failures, latency].reduce((acc, d) => acc + (d === "up" ? 1 : d === "down" ? -1 : 0), 0);
166
- const overall = score >= 1 ? "improving" : score <= -1 ? "degrading" : "mixed";
167
- return { decisionQuality, outputVolume, failures, latency, overall };
168
- }
169
46
  function deltaPill(deltas, key, lowerIsBetter = false) {
170
47
  const raw = deltas[key]?.pctChange;
171
48
  if (raw === undefined)
@@ -196,249 +73,107 @@ function passCard(title, desc) {
196
73
  `<div class="title">${esc(title)}</div>` +
197
74
  `<div class="desc">${esc(desc)}</div></div></div>`);
198
75
  }
199
- function readSemSearch(advisories) {
200
- const check = advisories.find((a) => a.name === "semantic-search-runtime");
201
- if (!check)
202
- return { blocked: false, detail: "" };
203
- const evidence = check.evidence ?? {};
204
- const status = String(evidence.status ?? "unknown");
205
- const blocked = check.status !== "pass" || status.toLowerCase().includes("block");
206
- const entries = typeof evidence.entryCount === "number" ? evidence.entryCount : 0;
207
- const embeddings = typeof evidence.embeddingCount === "number" ? evidence.embeddingCount : 0;
208
- return { blocked, detail: `${num(entries)} entries, ${num(embeddings)} embeddings` };
209
- }
76
+ // ── Status badges (presentation constants) ───────────────────────────────────
77
+ const badgeByStatus = {
78
+ pass: { badge: "badge-pass", dot: "dot-pass", label: "PASS" },
79
+ warn: { badge: "badge-warn", dot: "dot-warn", label: "WARN" },
80
+ fail: { badge: "badge-fail", dot: "dot-fail", label: "FAIL" },
81
+ };
210
82
  // ── ECharts delivery ─────────────────────────────────────────────────────────
211
- /** Parse an akm window string (`24h`, `7d`, `30m`, `2w`) to milliseconds; 0 if unparseable. */
212
- function windowToMs(window) {
213
- const m = /^(\d+)\s*([mhdw])$/i.exec(window.trim());
214
- if (!m)
215
- return 0;
216
- const n = Number(m[1]);
217
- const mult = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 }[m[2].toLowerCase()] ?? 0;
218
- return n * mult;
83
+ const ECHARTS_CDN = "https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";
84
+ /** Always CDN — see the module docstring's chunk-9 WI-9.4d note. */
85
+ function buildEchartsTag() {
86
+ return `<script src="${ECHARTS_CDN}"></script>`;
219
87
  }
220
88
  /**
221
- * Build the time-slice `<option>`s for the report's filter bar, DERIVED from the
222
- * actual report window so the choices always make sense (the old hard-coded
223
- * 1d–21d list was useless on a 24h or 7d report). "All" is the default; the
224
- * sub-window options carry their cutoff in milliseconds (consumed by
225
- * filteredRuns), largest first, only those strictly shorter than the window.
89
+ * Build the time-slice `<option>`s for the report's filter bar, DERIVED from
90
+ * the actual report window (`vm.sliceCandidates`) so the choices always make
91
+ * sense (the old hard-coded 1d–21d list was useless on a 24h or 7d report).
92
+ * "All" is the default; the sub-window options carry their cutoff in
93
+ * milliseconds (consumed by filteredRuns), largest first.
226
94
  */
227
- function buildSliceOptions(window) {
228
- const windowMs = windowToMs(window);
229
- const HOUR = 3_600_000;
230
- const DAY = 86_400_000;
231
- const candidates = [
232
- [6 * HOUR, "6h"],
233
- [12 * HOUR, "12h"],
234
- [DAY, "1d"],
235
- [3 * DAY, "3d"],
236
- [7 * DAY, "7d"],
237
- [14 * DAY, "14d"],
238
- ];
239
- const subs = candidates
240
- .filter(([ms]) => windowMs > 0 && ms < windowMs)
241
- .sort((a, b) => b[0] - a[0])
242
- .slice(0, 4);
243
- const opts = [`<option value="all" selected>All (${esc(window)})</option>`];
244
- for (const [ms, label] of subs)
95
+ function buildSliceOptions(vm) {
96
+ const opts = [`<option value="all" selected>All (${esc(vm.window)})</option>`];
97
+ for (const [ms, label] of vm.sliceCandidates)
245
98
  opts.push(`<option value="${ms}">Last ${label}</option>`);
246
99
  return opts.join("\n ");
247
100
  }
248
- function buildEchartsTag(opts) {
249
- const mode = opts.echarts ?? (process.env.AKM_ECHARTS === "cdn" ? "cdn" : "inline");
250
- if (mode === "cdn")
251
- return `<script src="${ECHARTS_CDN}"></script>`;
252
- const libPath = opts.echartsLibPath ?? ECHARTS_VENDOR_PATH;
253
- // Guard against an accidental </script> in the minified payload.
254
- const lib = fs.readFileSync(libPath, "utf8").replaceAll("</script>", "<\\/script>");
255
- return `<script>\n${lib}\n</script>`;
101
+ // ── Fragment renderers (VM → HTML; one per template section) ────────────────
102
+ function renderSinceHuman(vm) {
103
+ return vm.sinceIso ? `${isoTimeTag(vm.sinceIso)} now` : `last ${esc(vm.window)}`;
104
+ }
105
+ function renderStatusBadgeHtml(vm) {
106
+ const badge = badgeByStatus[vm.status];
107
+ const statusBadge = `<span class="badge-pill ${badge.badge}"><span class="dot ${badge.dot}"></span>${badge.label}</span>`;
108
+ const failBadge = `<span class="badge-pill ${vm.failOk ? "badge-pass" : "badge-warn"}">` +
109
+ `<span class="dot ${vm.failOk ? "dot-pass" : "dot-warn"}"></span>${vm.taskFailRate} Fail Rate</span>`;
110
+ return `${statusBadge}\n ${failBadge}`;
256
111
  }
257
- // ── Main builder ─────────────────────────────────────────────────────────────
258
112
  /**
259
- * Compute all 17 `%%TOKEN%%` replacements for the health HTML template.
260
- * There is deliberately NO standalone `%%OVERALL_STATUS%%` token the
261
- * overall status is embedded in the pre-rendered badge / exec-summary
262
- * fragments, matching the skill template. Advisories and "what to watch" are
263
- * merged + de-duplicated into a single prioritized `%%ACTION_ITEMS_HTML%%`.
113
+ * Executive-summary block: quick numbers, trend pills, period-over-period
114
+ * deltas, the current-run snapshot, the window facts, the synthesized verdict
115
+ * sentence, and the freshness line.
264
116
  */
265
- export function buildHealthHtmlReplacements(result, opts) {
266
- const deltas = opts.deltas ?? {};
267
- const compareRuns = (a, b) => a.startedAt.localeCompare(b.startedAt) || a.completedAt.localeCompare(b.completedAt) || a.id.localeCompare(b.id);
268
- const allRuns = (result.runs ?? []).map(reshapeRun).sort(compareRuns);
269
- const invalidRuns = allRuns.filter((run) => run.resultStatus === "invalid");
270
- const runs = allRuns.filter((run) => run.resultStatus !== "invalid");
271
- const improve = result.improve;
272
- const proposals = opts.proposals;
273
- const sem = readSemSearch(result.advisories);
274
- const trend = buildTrend(deltas);
275
- // ── Aggregates (collect.py step 6) ─────────────────────────────────────────
276
- const cons = improve.consolidation;
277
- const mi = improve.memoryInference;
278
- const ge = improve.graphExtraction;
279
- const wallTime = improve.wallTime;
280
- // WS-5: perf telemetry, coverage, and degradation metrics.
281
- const perf = improve.perfTelemetry ?? {
282
- dedupPoolSize: 0,
283
- llmPoolSize: 0,
284
- judgedCacheSkipped: 0,
285
- embedMs: 0,
286
- embedCacheHits: 0,
287
- embedCacheMisses: 0,
288
- overBudgetRuns: 0,
289
- runsWithTelemetry: 0,
290
- };
291
- const coverage = improve.coverage;
292
- const degradation = improve.degradation;
293
- const minting = improve.enrichmentMinting;
294
- // #576: real per-stage LLM token/time accounting (replaces the GPU-time
295
- // proxy). Optional-guarded so reports built from older health JSON without
296
- // the aggregate still render.
297
- const llm = result.metrics.llmUsage ?? {
298
- calls: 0,
299
- totalDurationMs: 0,
300
- promptTokens: 0,
301
- completionTokens: 0,
302
- totalTokens: 0,
303
- reasoningTokens: 0,
304
- byStage: {},
305
- byProcess: {},
306
- byEngine: {},
307
- };
308
- const totalRuns = runs.length;
309
- const failedRuns = runs.filter((r) => !r.ok).length;
310
- const invoked = improve.invoked || totalRuns;
311
- const completed = improve.completed || totalRuns - failedRuns;
312
- const miWritten = mi.written || mi.writes || 0;
313
- const completionRate = invoked ? `${Math.round((100 * completed) / invoked)}%` : "0%";
314
- const taskFailRate = pct(result.metrics.taskFailRate, 1);
315
- const agentFailRate = pct(result.metrics.agentFailureRate, 2);
316
- const miYieldRate = pct(mi.yieldRate, 1);
317
- const medianDurMin = (wallTime.medianMs / 60000).toFixed(1);
318
- const p95DurMin = (wallTime.p95Ms / 60000).toFixed(1);
319
- const avgPromoted = totalRuns ? String(Math.round(cons.promoted / totalRuns)) : "0";
320
- const chunkFail = `${num(cons.failedChunks)} / ${num(cons.totalChunks)}`;
321
- // ── Meta (collect.py steps 10-11) ──────────────────────────────────────────
322
- const sinceIso = result.since;
323
- const reportDate = sinceIso.slice(0, 10);
324
- const sinceHuman = sinceIso ? `${isoTimeTag(sinceIso)} → now` : `last ${esc(opts.window)}`;
325
- const reportTitle = reportDate ? `AKM Health Report — ${reportDate}` : "AKM Health Report";
326
- const lastRun = runs[runs.length - 1];
327
- const generatedAt = lastRun ? lastRun.completedAt || lastRun.startedAt || sinceIso : sinceIso;
328
- const latest = [...runs]
329
- .filter((run) => run.resultComplete)
330
- .sort(compareRuns)
331
- .at(-1);
332
- // Freshness: surface the newest run's timestamp + the generated-at anchor in
333
- // the exec card. Staleness is computed deterministically (no Date.now()) from
334
- // the gap between the window start (`since`) and the newest run we have: if no
335
- // run landed in the window, or the newest run sits in the first 25% of a
336
- // window wider than the 6h threshold (i.e. a long idle tail), we flag stale.
337
- const STALE_MS = 6 * 60 * 60 * 1000;
338
- const latestRunMs = lastRun ? Date.parse(lastRun.completedAt || lastRun.startedAt) : NaN;
339
- const sinceMs = Date.parse(sinceIso);
340
- const generatedMs = Date.parse(generatedAt);
341
- const idleTailMs = Number.isFinite(latestRunMs) && Number.isFinite(generatedMs) ? Math.max(0, generatedMs - latestRunMs) : 0;
342
- const isStale = totalRuns === 0 || (Number.isFinite(latestRunMs) && Number.isFinite(sinceMs) && idleTailMs > STALE_MS);
343
- const latestRunHuman = lastRun ? isoTimeTag(lastRun.completedAt || lastRun.startedAt) : "—";
344
- // ── Status badges ──────────────────────────────────────────────────────────
345
- const badgeByStatus = {
346
- pass: { badge: "badge-pass", dot: "dot-pass", label: "PASS" },
347
- warn: { badge: "badge-warn", dot: "dot-warn", label: "WARN" },
348
- fail: { badge: "badge-fail", dot: "dot-fail", label: "FAIL" },
349
- };
350
- const badge = badgeByStatus[result.status];
351
- const statusBadge = `<span class="badge-pill ${badge.badge}"><span class="dot ${badge.dot}"></span>${badge.label}</span>`;
352
- const failOk = result.metrics.taskFailRate < TASK_FAIL_RATE_WARN;
353
- const failBadge = `<span class="badge-pill ${failOk ? "badge-pass" : "badge-warn"}">` +
354
- `<span class="dot ${failOk ? "dot-pass" : "dot-warn"}"></span>${taskFailRate} Fail Rate</span>`;
355
- // ── Executive summary ──────────────────────────────────────────────────────
117
+ function renderExecSummary(vm) {
118
+ const badge = badgeByStatus[vm.status];
356
119
  const li = (k, vHtml) => `<li><span class="k">${esc(k)}</span><span class="v">${vHtml}</span></li>`;
357
120
  const trendLi = (k, d) => li(k, `<span class="trend-pill ${d === "flat" ? "flat" : d}">${d}</span>`);
358
121
  const quickNumbers = [
359
- li("Task fail rate", taskFailRate),
360
- li("Agent fail rate", agentFailRate),
361
- li("Improve completion", `${num(completed)} / ${num(invoked)} (${completionRate})`),
362
- li("MI yield rate", miYieldRate),
363
- li("MI written", num(miWritten)),
364
- li("Consolidation promoted", num(cons.promoted)),
365
- li("Consolidation judged: no action", `<abbr title="Candidates the consolidator reviewed but intentionally left unchanged (the 'judgedNoAction' field).">${num(cons.judgedNoAction)}</abbr>`),
366
- li("Chunk failure", chunkFail),
367
- li("Median wall time", fmtMs(wallTime.medianMs)),
368
- li("P95 wall time", fmtMs(wallTime.p95Ms)),
122
+ li("Task fail rate", vm.taskFailRate),
123
+ li("Agent fail rate", vm.agentFailRate),
124
+ li("Improve completion", `${num(vm.completed)} / ${num(vm.invoked)} (${vm.completionRate})`),
125
+ li("MI yield rate", vm.miYieldRate),
126
+ li("MI written", num(vm.miWritten)),
127
+ li("Consolidation promoted", num(vm.consolidation.promoted)),
128
+ li("Consolidation judged: no action", `<abbr title="Candidates the consolidator reviewed but intentionally left unchanged (the 'judgedNoAction' field).">${num(vm.consolidation.judgedNoAction)}</abbr>`),
129
+ li("Chunk failure", vm.chunkFail),
130
+ li("Median wall time", fmtMs(vm.wallTime.medianMs)),
131
+ li("P95 wall time", fmtMs(vm.wallTime.p95Ms)),
369
132
  ].join("");
370
133
  const trendRows = [
371
- trendLi("Decision quality", trend.decisionQuality),
372
- trendLi("Output volume", trend.outputVolume),
373
- trendLi("Failures", trend.failures),
374
- trendLi("Latency", trend.latency),
134
+ trendLi("Decision quality", vm.trend.decisionQuality),
135
+ trendLi("Output volume", vm.trend.outputVolume),
136
+ trendLi("Failures", vm.trend.failures),
137
+ trendLi("Latency", vm.trend.latency),
375
138
  ].join("");
376
139
  const deltaRows = [
377
- li("Promoted", deltaPill(deltas, "improve.consolidation.promoted")),
378
- li("MI written", deltaPill(deltas, "improve.memoryInference.written")),
379
- li("MI yield", deltaPill(deltas, "improve.memoryInference.yieldRate")),
380
- li("Median wall", deltaPill(deltas, "improve.wallTime.medianMs", true)),
381
- li("P95 wall", deltaPill(deltas, "improve.wallTime.p95Ms", true)),
140
+ li("Promoted", deltaPill(vm.deltas, "improve.consolidation.promoted")),
141
+ li("MI written", deltaPill(vm.deltas, "improve.memoryInference.written")),
142
+ li("MI yield", deltaPill(vm.deltas, "improve.memoryInference.yieldRate")),
143
+ li("Median wall", deltaPill(vm.deltas, "improve.wallTime.medianMs", true)),
144
+ li("P95 wall", deltaPill(vm.deltas, "improve.wallTime.p95Ms", true)),
382
145
  ].join("");
383
- const snapRows = latest
146
+ const snapRows = vm.latest
384
147
  ? [
385
- li("Run id", `<code>${esc(latest.id.slice(0, 28))}</code>`),
386
- li("Completed", isoTimeTag(latest.completedAt || latest.startedAt)),
387
- li("Status", latest.ok ? "✅ ok" : "❌ failed"),
388
- li("Wall time", fmtMs(latest.wallTimeMs)),
389
- li("Reflect ok/fail", `${latest.reflectOk} / ${latest.reflectFailed}`),
390
- li("Promoted", String(latest.promoted)),
391
- li("Judged: no action", `<abbr title="Candidates reviewed but intentionally left unchanged on this run.">${latest.judgedNoAction}</abbr>`),
392
- li("MI written", String(latest.miWritten)),
393
- li("Graph entities/relations", `${latest.geEntities} / ${latest.geRelations}`),
148
+ li("Run id", `<code>${esc(vm.latest.id.slice(0, 28))}</code>`),
149
+ li("Completed", isoTimeTag(vm.latest.completedAt || vm.latest.startedAt)),
150
+ li("Status", vm.latest.ok ? "✅ ok" : "❌ failed"),
151
+ li("Wall time", fmtMs(vm.latest.wallTimeMs)),
152
+ li("Reflect ok/fail", `${vm.latest.reflectOk} / ${vm.latest.reflectFailed}`),
153
+ li("Promoted", String(vm.latest.promoted)),
154
+ li("Judged: no action", `<abbr title="Candidates reviewed but intentionally left unchanged on this run.">${vm.latest.judgedNoAction}</abbr>`),
155
+ li("MI written", String(vm.latest.miWritten)),
156
+ li("Graph entities/relations", `${vm.latest.geEntities} / ${vm.latest.geRelations}`),
394
157
  ].join("")
395
158
  : '<li><span class="k">No runs in window</span><span class="v">—</span></li>';
396
159
  const windowRows = [
397
- li("Report window", esc(opts.window)),
398
- li("Compare window", esc(opts.compare)),
399
- li("Runs", `${num(totalRuns)} (${failedRuns} failed)`),
400
- li("Included result rows", num(improve.resultRows?.included ?? totalRuns)),
401
- li("Normalized result rows", num(improve.resultRows?.normalized ?? 0)),
402
- li("Invalid result rows skipped", num(improve.resultRows?.skipped.invalid ?? invalidRuns.length)),
403
- li("Stash derived", `<abbr title="Whole-stash recount of derived assets at report time — not a per-run sum.">${num(improve.memorySummary.derived)}</abbr>`),
404
- li("Stash eligible", `<abbr title="Whole-stash recount of eligible assets at report time — not a per-run sum.">${num(improve.memorySummary.eligible)}</abbr>`),
405
- li("Pending proposals", String(proposals.length)),
406
- li("Semantic search", sem.blocked ? "BLOCKED" : "OK"),
160
+ li("Report window", esc(vm.window)),
161
+ li("Compare window", esc(vm.compare)),
162
+ li("Runs", `${num(vm.totalRuns)} (${vm.failedRuns} failed)`),
163
+ li("Included result rows", num(vm.includedResultRows)),
164
+ li("Normalized result rows", num(vm.normalizedResultRows)),
165
+ li("Invalid result rows skipped", num(vm.skippedInvalidResultRows)),
166
+ li("Stash derived", `<abbr title="Whole-stash recount of derived assets at report time — not a per-run sum.">${num(vm.memorySummary.derived)}</abbr>`),
167
+ li("Stash eligible", `<abbr title="Whole-stash recount of eligible assets at report time — not a per-run sum.">${num(vm.memorySummary.eligible)}</abbr>`),
168
+ li("Pending proposals", String(vm.proposals.length)),
169
+ li("Semantic search", vm.sem.blocked ? "BLOCKED" : "OK"),
407
170
  ].join("");
408
- const overallEmoji = trend.overall === "improving" ? "📈" : trend.overall === "degrading" ? "📉" : "↔️";
409
- // ── Synthesized verdict (one sentence + 2-3 drivers) ───────────────────────
410
- // The verdict WORD is the authoritative health status (hard checks). Concerns
411
- // (failed improve runs, blocked search, pending proposals) are advisory and do
412
- // NOT gate that word, so we must not pair a green PASS with red "drivers" as
413
- // if they were failures. When the status is PASS we frame concerns as "watch:"
414
- // alongside the trend; only a degraded status (WARN/FAIL) leads with them.
415
- const concerns = [];
416
- if (failedRuns > 0)
417
- concerns.push(`${failedRuns} failed run${failedRuns === 1 ? "" : "s"}`);
418
- if (sem.blocked)
419
- concerns.push("semantic search blocked");
420
- if (proposals.length > 0)
421
- concerns.push(`${proposals.length} pending proposal${proposals.length === 1 ? "" : "s"}`);
422
- const trendClause = trend.overall === "improving"
423
- ? "throughput and latency improving"
424
- : trend.overall === "degrading"
425
- ? "throughput or latency degrading"
426
- : "throughput and latency steady";
427
- const verdictWord = badge.label;
428
- let verdictRest;
429
- if (result.status === "pass") {
430
- const watch = concerns.length > 0 ? `; watch: ${concerns.slice(0, 3).join(", ")}` : "";
431
- verdictRest = `healthy, ${trendClause}${watch}`;
432
- }
433
- else {
434
- const lead = concerns.length > 0 ? concerns.slice(0, 3).join("; ") : trendClause;
435
- verdictRest = trend.overall === "degrading" && concerns.length > 0 ? `${lead}; ${trendClause}` : lead;
436
- }
437
- const verdictSentence = `${verdictWord} — ${esc(verdictRest)}.`;
438
- const verdictHtml = `<div class="verdict ${result.status}"><b>Verdict:</b> ${verdictSentence}</div>`;
439
- // ── Freshness line ─────────────────────────────────────────────────────────
440
- const freshnessHtml = `<div class="freshness${isStale ? " stale" : ""}">${isStale ? "⚠️ Stale: " : ""}Latest run ${latestRunHuman} &nbsp;·&nbsp; generated ${isoTimeTag(generatedAt)}${isStale ? " — no recent activity (newest run older than the 6h freshness threshold)." : "."}</div>`;
441
- const execSummary = `
171
+ const overallEmoji = vm.trend.overall === "improving" ? "📈" : vm.trend.overall === "degrading" ? "📉" : "↔️";
172
+ const verdictSentence = `${vm.verdictWord} — ${esc(vm.verdictRest)}.`;
173
+ const verdictHtml = `<div class="verdict ${vm.status}"><b>Verdict:</b> ${verdictSentence}</div>`;
174
+ const latestRunHuman = vm.lastRun ? isoTimeTag(vm.lastRun.completedAt || vm.lastRun.startedAt) : "—";
175
+ const freshnessHtml = `<div class="freshness${vm.isStale ? " stale" : ""}">${vm.isStale ? "⚠️ Stale: " : ""}Latest run ${latestRunHuman} &nbsp;·&nbsp; generated ${isoTimeTag(vm.generatedAt)}${vm.isStale ? " — no recent activity (newest run older than the 6h freshness threshold)." : "."}</div>`;
176
+ return `
442
177
  <h2>${overallEmoji} Executive Summary
443
178
  <span class="badge-pill ${badge.badge}" style="font-size:11px;">
444
179
  <span class="dot ${badge.dot}"></span>${badge.label}</span></h2>
@@ -450,7 +185,7 @@ export function buildHealthHtmlReplacements(result, opts) {
450
185
  <ul>${quickNumbers}</ul>
451
186
  </div>
452
187
  <div>
453
- <h4>Trend vs prior ${esc(opts.compare)}</h4>
188
+ <h4>Trend vs prior ${esc(vm.compare)}</h4>
454
189
  <ul>${trendRows}</ul>
455
190
  <h4 style="margin-top:14px;">Period-over-period deltas</h4>
456
191
  <ul>${deltaRows}</ul>
@@ -464,200 +199,55 @@ export function buildHealthHtmlReplacements(result, opts) {
464
199
  <ul>${windowRows}</ul>
465
200
  </div>
466
201
  </div>
467
- <div class="overall">Overall trend: <b>${esc(trend.overall)}</b> ${overallEmoji}
202
+ <div class="overall">Overall trend: <b>${esc(vm.trend.overall)}</b> ${overallEmoji}
468
203
  &nbsp;·&nbsp; based on decision quality, output volume, failures, and latency vs the prior window.</div>`.trim();
469
- // ── KPI cards ──────────────────────────────────────────────────────────────
470
- // Color is a health SIGNAL, not decoration: green/yellow/red where a card has
471
- // a meaningful threshold; "neutral" for purely-informational counts. Cards are
472
- // ordered by operator priority (failures first, informational counts last).
473
- const semValue = sem.blocked ? "BLOCKED" : "OK";
474
- const semColor = sem.blocked ? "red" : "green";
475
- const semStyle = sem.blocked ? "font-size:18px;" : "";
476
- const completionPct = invoked ? (100 * completed) / invoked : 100;
477
- const completionColor = completionPct >= 99 ? "green" : completionPct >= 90 ? "yellow" : "red";
478
- const llmTokensCompact = compact(llm.totalTokens);
204
+ }
205
+ /**
206
+ * Color is a health SIGNAL, not decoration: green/yellow/red where a card has
207
+ * a meaningful threshold; "neutral" for purely-informational counts. Cards
208
+ * are ordered by operator priority (failures first, informational counts
209
+ * last).
210
+ */
211
+ function renderKpiCards(vm) {
212
+ const semValue = vm.sem.blocked ? "BLOCKED" : "OK";
213
+ const semColor = vm.sem.blocked ? "red" : "green";
214
+ const semStyle = vm.sem.blocked ? "font-size:18px;" : "";
215
+ const completionColor = vm.completionPct >= 99 ? "green" : vm.completionPct >= 90 ? "yellow" : "red";
479
216
  const kpiCard = (color, label, value, sub, valueStyle = "") => `<div class="kpi-card ${color}">
480
217
  <div class="label">${label}</div>
481
218
  <div class="value"${valueStyle ? ` style="${valueStyle}"` : ""}>${value}</div>
482
219
  <div class="sub">${sub}</div>
483
220
  </div>`;
484
- const kpiCards = [
485
- kpiCard(failedRuns === 0 ? "green" : "red", "Failed Runs", String(failedRuns), `of ${num(totalRuns)} runs · ${taskFailRate} task fail`),
486
- kpiCard(completionColor, "Completion Rate", completionRate, `${num(completed)} / ${num(invoked)} invoked`),
487
- kpiCard("neutral", "Median Duration", `${medianDurMin}m`, `p95 = ${p95DurMin}m`),
488
- kpiCard("blue", "Total Promoted", num(cons.promoted), `avg ${avgPromoted} / run`),
489
- kpiCard("blue", "MI Written", num(miWritten), `${miYieldRate} yield rate`),
490
- kpiCard("purple", "Graph Entities", num(ge.entities), `+${num(ge.relations)} relations`),
491
- kpiCard("neutral", "Stash Derived", num(improve.memorySummary.derived), `of ${num(improve.memorySummary.eligible)} eligible (whole-stash)`),
221
+ return [
222
+ kpiCard(vm.failedRuns === 0 ? "green" : "red", "Failed Runs", String(vm.failedRuns), `of ${num(vm.totalRuns)} runs · ${vm.taskFailRate} task fail`),
223
+ kpiCard(completionColor, "Completion Rate", vm.completionRate, `${num(vm.completed)} / ${num(vm.invoked)} invoked`),
224
+ kpiCard("neutral", "Median Duration", `${vm.medianDurMin}m`, `p95 = ${vm.p95DurMin}m`),
225
+ kpiCard("blue", "Total Promoted", num(vm.consolidation.promoted), `avg ${vm.avgPromoted} / run`),
226
+ kpiCard("blue", "MI Written", num(vm.miWritten), `${vm.miYieldRate} yield rate`),
227
+ kpiCard("purple", "Graph Entities", num(vm.graphExtraction.entities), `+${num(vm.graphExtraction.relations)} relations`),
228
+ kpiCard("neutral", "Stash Derived", num(vm.memorySummary.derived), `of ${num(vm.memorySummary.eligible)} eligible (whole-stash)`),
492
229
  // #576: real LLM work — duration leads, tokens compact, not a GPU proxy.
493
- kpiCard(llm.calls > 0 ? "purple" : "neutral", "🧠 LLM Work", `${llmTokensCompact} tok`, `${fmtMs(llm.totalDurationMs)} · ${num(llm.calls)} calls · ${compact(llm.reasoningTokens)} reasoning`),
494
- kpiCard(semColor, "Semantic Search", semValue, esc(sem.detail), semStyle),
495
- kpiCard(proposals.length > 0 ? "yellow" : "neutral", "Pending Proposals", String(proposals.length), `from ${esc(opts.window)} batch`),
230
+ kpiCard(vm.llm.calls > 0 ? "purple" : "neutral", "🧠 LLM Work", `${vm.llmTokensCompact} tok`, `${fmtMs(vm.llm.totalDurationMs)} · ${num(vm.llm.calls)} calls · ${compact(vm.llm.reasoningTokens)} reasoning`),
231
+ kpiCard(semColor, "Semantic Search", semValue, esc(vm.sem.detail), semStyle),
232
+ kpiCard(vm.proposals.length > 0 ? "yellow" : "neutral", "Pending Proposals", String(vm.proposals.length), `from ${esc(vm.window)} batch`),
496
233
  ].join("\n");
497
- // ── Chart payload ──────────────────────────────────────────────────────────
498
- const distillReasons = [...new Set(runs.flatMap((r) => Object.keys(r.distillByReason)))]
499
- .filter((reason) => !DISTILL_REASONS_HIDDEN.has(reason))
500
- .sort();
501
- const runsJsConst = `const RUNS = ${JSON.stringify(runs)};`;
502
- // ── Summary table rows ─────────────────────────────────────────────────────
503
- // Optional 4th element = a glossary tooltip rendered as <abbr title>.
504
- const summaryRows = [
505
- ["Task fail rate", taskFailRate, "flat"],
506
- ["Agent fail rate", agentFailRate, "flat"],
507
- ["Improve completion", `${num(completed)} / ${num(invoked)}`, "flat"],
508
- [
509
- "MI yield rate",
510
- miYieldRate,
511
- trend.decisionQuality,
512
- "Memory-inference yield: share of considered candidates that produced a written fact.",
513
- ],
514
- ["MI written", num(miWritten), trend.outputVolume, "Memory-inference: facts written this window."],
515
- ["Consolidation promoted", num(cons.promoted), trend.outputVolume],
516
- ["Consolidation merged", num(cons.merged), "flat"],
517
- ["Consolidation deleted", num(cons.deleted), "flat"],
518
- ["Consolidation contradicted", num(cons.contradicted), "flat"],
519
- [
520
- "Consolidation judged: no action",
521
- num(cons.judgedNoAction),
522
- "flat",
523
- "Candidates reviewed but intentionally left unchanged (the 'judgedNoAction' field).",
524
- ],
525
- ["Chunk failure", chunkFail, "flat"],
526
- ["Graph entities", num(ge.entities), "up"],
527
- ["Graph relations", num(ge.relations), "up"],
528
- [
529
- "Stash derived",
530
- num(improve.memorySummary.derived),
531
- "up",
532
- "Whole-stash recount of derived assets at report time — not a per-run sum.",
533
- ],
534
- ["Median wall time", fmtMs(wallTime.medianMs), trend.latency],
535
- ["P95 wall time", fmtMs(wallTime.p95Ms), trend.latency],
536
- // #576: real LLM accounting (replaces the GPU-time proxy).
537
- ["LLM calls", num(llm.calls), "flat"],
538
- ["LLM total tokens", num(llm.totalTokens), "flat"],
539
- ["LLM prompt tokens", num(llm.promptTokens), "flat"],
540
- ["LLM completion tokens", num(llm.completionTokens), "flat"],
541
- [
542
- "LLM reasoning tokens",
543
- num(llm.reasoningTokens),
544
- "flat",
545
- "Tokens spent on model reasoning/thinking, billed separately from prompt and completion.",
546
- ],
547
- ["LLM wall time", fmtMs(llm.totalDurationMs), trend.latency],
548
- ];
549
- // #612 — auto-accept gate calibration. Only surface when the gate actually
550
- // acted on proposals in the window (samples > 0); a default ungated install
551
- // reports an empty summary and we omit the rows to keep the table parity-clean.
552
- const calibration = improve.calibration;
553
- if (calibration && calibration.samples > 0) {
554
- summaryRows.push([
555
- "Calibration samples",
556
- num(calibration.samples),
557
- "flat",
558
- "Auto-accept gate decisions (auto-accepted + auto-rejected) the calibration join measured this window.",
559
- ], [
560
- "Calibration accept rate",
561
- String(calibration.overallAcceptRate),
562
- "flat",
563
- "Realized accept rate of acted-on gate decisions (auto-accepted / total acted-on).",
564
- ], [
565
- "Calibration gap",
566
- String(calibration.calibrationGap),
567
- "flat",
568
- "Mean predicted confidence minus realized accept rate. Positive = the gate is over-confident.",
569
- ]);
570
- }
571
- // WS-5: denominator-fixed coverage rows (only when we have real data).
572
- if (coverage && !Number.isNaN(coverage.rate)) {
573
- summaryRows.push([
574
- "Coverage rate",
575
- pct(coverage.rate, 1),
576
- "flat",
577
- "Distinct accepted refs / total stash assets (denominator-fixed). Shows what fraction of the corpus has been touched.",
578
- ], [
579
- "Eligible fraction",
580
- pct(coverage.eligibleFraction, 1),
581
- "flat",
582
- "Eligible assets / total stash assets. Fraction the improve pipeline actively considers.",
583
- ], [
584
- "Coverage accepted",
585
- num(coverage.acceptedProposals),
586
- "flat",
587
- "Total accepted proposals in the window (raw volume — includes repeated rewrites of the same asset).",
588
- ], [
589
- "Churn ratio",
590
- Number.isFinite(coverage.churnRatio) ? num(coverage.churnRatio) : "—",
591
- Number.isFinite(coverage.churnRatio) && coverage.churnRatio > 1.5 ? "down" : "flat",
592
- "Accepted proposals / distinct refs touched. >1.5 = the loop is repeatedly rewriting the same assets (churn, not coverage).",
593
- ]);
594
- }
595
- // Enrichment-vs-minting policy rollup (reporting-only).
596
- if (minting && Number.isFinite(minting.share)) {
597
- summaryRows.push([
598
- "Enrichment-lane minted share",
599
- pct(minting.share, 1),
600
- minting.share > 0.05 ? "down" : "flat",
601
- `New assets minted by enrichment lanes / their accepted total (${minting.minted} minted vs ${minting.updated} updated). Enrichment lanes are ratified to edit existing assets only; WARN >5%, FAIL >15%.`,
602
- ]);
603
- }
604
- // WS-5: perf telemetry rows (only when at least one run reported telemetry).
605
- if (perf.runsWithTelemetry > 0) {
606
- const embedCacheTotal = perf.embedCacheHits + perf.embedCacheMisses;
607
- const embedCacheHitRate = embedCacheTotal > 0 ? pct(perf.embedCacheHits / embedCacheTotal, 1) : "—";
608
- summaryRows.push([
609
- "Embed cache hit rate",
610
- embedCacheHitRate,
611
- "flat",
612
- "Fraction of embedding lookups served from cache (>95% is healthy). Aggregated across WS-5 runs.",
613
- ], [
614
- "Embed wall time",
615
- fmtMs(perf.embedMs),
616
- "flat",
617
- "Cumulative embedding wall-clock time across consolidation runs in the window.",
618
- ], [
619
- "Judged-cache skipped",
620
- num(perf.judgedCacheSkipped),
621
- "flat",
622
- "Candidates skipped by the judged-cache (not sent to LLM). Higher = more efficient reuse of prior judgments.",
623
- ], [
624
- "Dedup pool size",
625
- num(perf.dedupPoolSize),
626
- "flat",
627
- "Average memory pool size after deduplication (before judged-cache narrowing). WS-5 perf telemetry.",
628
- ], [
629
- "Over-budget consolidation runs",
630
- String(perf.overBudgetRuns),
631
- perf.overBudgetRuns > 0 ? "down" : "flat",
632
- "Runs where consolidation alone exceeded the total run budget (estimatedBudgetFractionUsed > 1.0).",
633
- ]);
634
- }
635
- // WS-5: degradation metrics rows.
636
- if (degradation) {
637
- summaryRows.push([
638
- "Corpus diversity (Gini)",
639
- num(degradation.corpusCentroidDistance),
640
- degradation.entrenchmentFlagged || degradation.salienceUniformityFlagged ? "down" : "flat",
641
- "Gini coefficient of retrieval_salience for top-100 ranked assets. Two-tailed: >0.35 = entrenchment risk; <0.08 = collapsed toward uniform (ranking no longer discriminates).",
642
- ], [
643
- "Merge fidelity contradiction rate",
644
- pct(degradation.mergeFidelityContradictionRate, 1),
645
- "flat",
646
- "Fraction of consolidated proposals that involved a contradiction, from consolidation result envelopes.",
647
- ]);
648
- }
649
- const summaryRowsHtml = summaryRows
234
+ }
235
+ function renderSummaryRowsHtml(vm) {
236
+ return vm.summaryRows
650
237
  .map(([label, value, t, tip]) => {
651
238
  const labelHtml = tip ? `<abbr title="${esc(tip)}">${esc(label)}</abbr>` : esc(label);
652
239
  return (` <tr><td>${labelHtml}</td><td>${esc(value)}</td>` +
653
240
  `<td class="trend ${trendClass(t)}">${trendLabel(t)}</td></tr>`);
654
241
  })
655
242
  .join("\n");
656
- // ── Action Items (merged + de-duplicated advisories + what-to-watch) ────────
657
- // Advisories and the old "what to watch" cards were built from the same data
658
- // (result.advisories, sem-blocked, proposals, tail-latency, failed-runs). We
659
- // collapse them into ONE prioritized, de-duplicated list (P1/P2/P3 + a
660
- // remediation command per item), keyed so each concern appears exactly once.
243
+ }
244
+ /**
245
+ * Merge + de-duplicate advisories and the old "what to watch" cards (built
246
+ * from the same VM data — invalid rows, sem-blocked, proposals, tail-latency,
247
+ * failed-runs, ) into ONE prioritized list (P1/P2/P3 + a remediation command
248
+ * per item), keyed so each concern appears exactly once.
249
+ */
250
+ function renderActionItems(vm) {
661
251
  const items = [];
662
252
  const seen = new Set();
663
253
  const pushItem = (item) => {
@@ -666,18 +256,18 @@ export function buildHealthHtmlReplacements(result, opts) {
666
256
  seen.add(item.key);
667
257
  items.push(item);
668
258
  };
669
- if (invalidRuns.length > 0) {
259
+ if (vm.invalidRuns.length > 0) {
670
260
  pushItem({
671
261
  key: "invalid-improve-result-rows",
672
262
  prio: "P2",
673
263
  cls: "warn",
674
- title: `${invalidRuns.length} invalid improve result row${invalidRuns.length === 1 ? "" : "s"} skipped`,
675
- descHtml: `Excluded from result-derived metrics: ${invalidRuns.map((run) => `<code>${esc(run.id)}</code>`).join(", ")}.`,
676
- remedy: `akm health --since=${opts.window} --group-by run`,
264
+ title: `${vm.invalidRuns.length} invalid improve result row${vm.invalidRuns.length === 1 ? "" : "s"} skipped`,
265
+ descHtml: `Excluded from result-derived metrics: ${vm.invalidRuns.map((run) => `<code>${esc(run.id)}</code>`).join(", ")}.`,
266
+ remedy: `akm health --since=${vm.window} --group-by run`,
677
267
  });
678
268
  }
679
269
  // Hard advisories from the health check engine (own remediation in message).
680
- for (const a of result.advisories) {
270
+ for (const a of vm.advisories) {
681
271
  if (a.status !== "warn" && a.status !== "fail")
682
272
  continue;
683
273
  pushItem({
@@ -689,58 +279,53 @@ export function buildHealthHtmlReplacements(result, opts) {
689
279
  });
690
280
  }
691
281
  // Failed runs in window.
692
- if (failedRuns > 0) {
282
+ if (vm.failedRuns > 0) {
693
283
  pushItem({
694
284
  key: "failed-runs",
695
285
  prio: "P1",
696
286
  cls: "fail",
697
- title: `${failedRuns} failed run${failedRuns === 1 ? "" : "s"} in window`,
698
- descHtml: `Task fail rate ${esc(taskFailRate)}. Inspect failed runs (ok=false) for early-exit or harness errors.`,
699
- remedy: `akm health --since=${opts.window} --group-by run`,
287
+ title: `${vm.failedRuns} failed run${vm.failedRuns === 1 ? "" : "s"} in window`,
288
+ descHtml: `Task fail rate ${esc(vm.taskFailRate)}. Inspect failed runs (ok=false) for early-exit or harness errors.`,
289
+ remedy: `akm health --since=${vm.window} --group-by run`,
700
290
  });
701
291
  }
702
292
  // Semantic search blocked.
703
- if (sem.blocked) {
293
+ if (vm.sem.blocked) {
704
294
  pushItem({
705
295
  key: "semantic-search-blocked",
706
296
  prio: "P2",
707
297
  cls: "warn",
708
298
  title: "Semantic search blocked",
709
- descHtml: `Embedding provider unreachable. ${esc(sem.detail)}. Curate falls back to keyword search — relevance scoring degraded.`,
299
+ descHtml: `Embedding provider unreachable. ${esc(vm.sem.detail)}. Curate falls back to keyword search — relevance scoring degraded.`,
710
300
  remedy: "akm config show",
711
301
  });
712
302
  }
713
303
  // Pending proposals to drain.
714
- if (proposals.length > 0) {
715
- const bySource = new Map();
716
- for (const p of proposals)
717
- bySource.set(p.source, (bySource.get(p.source) ?? 0) + 1);
718
- const srcSummary = [...bySource.entries()]
719
- .sort(([a], [b]) => a.localeCompare(b))
720
- .map(([source, count]) => `${count} via ${esc(source)}`)
721
- .join(", ");
304
+ if (vm.proposals.length > 0) {
305
+ const srcSummary = vm.proposalsBySource.map(([source, count]) => `${count} via ${esc(source)}`).join(", ");
722
306
  pushItem({
723
307
  key: "drain-proposals",
724
308
  prio: "P2",
725
309
  cls: "warn",
726
- title: `Drain ${proposals.length} pending proposal${proposals.length === 1 ? "" : "s"}`,
310
+ title: `Drain ${vm.proposals.length} pending proposal${vm.proposals.length === 1 ? "" : "s"}`,
727
311
  descHtml: `Proposals generated this batch (${srcSummary}). Review before the queue grows further.`,
728
312
  remedy: "akm proposal list",
729
313
  });
730
314
  }
731
315
  // High tail latency.
732
- if (wallTime.p95Ms && wallTime.medianMs && wallTime.p95Ms / wallTime.medianMs > 2.5) {
316
+ if (vm.wallTime.p95Ms && vm.wallTime.medianMs && vm.wallTime.p95Ms / vm.wallTime.medianMs > 2.5) {
733
317
  pushItem({
734
318
  key: "tail-latency",
735
319
  prio: "P3",
736
320
  cls: "warn",
737
- title: `High tail latency: p95=${fmtMs(wallTime.p95Ms)}, median=${fmtMs(wallTime.medianMs)}`,
321
+ title: `High tail latency: p95=${fmtMs(vm.wallTime.p95Ms)}, median=${fmtMs(vm.wallTime.medianMs)}`,
738
322
  descHtml: "P95 is well above median. Consolidation/LLM phase dominates wall time on slow runs. " +
739
323
  "Check for slow chunks or LLM rate limiting.",
740
324
  });
741
325
  }
742
326
  // Stale freshness.
743
- if (isStale && totalRuns > 0) {
327
+ if (vm.isStale && vm.totalRuns > 0) {
328
+ const latestRunHuman = vm.lastRun ? isoTimeTag(vm.lastRun.completedAt || vm.lastRun.startedAt) : "—";
744
329
  pushItem({
745
330
  key: "stale",
746
331
  prio: "P3",
@@ -750,7 +335,7 @@ export function buildHealthHtmlReplacements(result, opts) {
750
335
  });
751
336
  }
752
337
  // WS-5: corpus entrenchment flag.
753
- if (degradation?.entrenchmentFlagged) {
338
+ if (vm.degradation?.entrenchmentFlagged) {
754
339
  pushItem({
755
340
  key: "corpus-entrenchment",
756
341
  prio: "P2",
@@ -758,12 +343,12 @@ export function buildHealthHtmlReplacements(result, opts) {
758
343
  title: "Corpus entrenchment risk: retrieval_salience Gini > 0.35",
759
344
  descHtml: "A small set of assets dominates retrieval — retrieval diversity is low. " +
760
345
  "Review top-ranked assets for stale or over-represented content. " +
761
- `Corpus diversity proxy: ${esc(String(degradation.corpusCentroidDistance))}.`,
346
+ `Corpus diversity proxy: ${esc(String(vm.degradation.corpusCentroidDistance))}.`,
762
347
  remedy: "akm health --format json | jq '.improve.degradation'",
763
348
  });
764
349
  }
765
350
  // Low-tail companion: salience distribution collapsed toward uniform.
766
- if (degradation?.salienceUniformityFlagged) {
351
+ if (vm.degradation?.salienceUniformityFlagged) {
767
352
  pushItem({
768
353
  key: "salience-uniformity-collapse",
769
354
  prio: "P2",
@@ -771,25 +356,25 @@ export function buildHealthHtmlReplacements(result, opts) {
771
356
  title: "Salience distribution collapsed: retrieval_salience Gini < 0.08",
772
357
  descHtml: "The top-100 salience scores are near-uniform (uniform baseline ≈ 0.1) — " +
773
358
  "ranking currently carries little to no discrimination between assets. " +
774
- `Corpus diversity proxy: ${esc(String(degradation.corpusCentroidDistance))}.`,
359
+ `Corpus diversity proxy: ${esc(String(vm.degradation.corpusCentroidDistance))}.`,
775
360
  remedy: "akm health --format json | jq '.improve.degradation'",
776
361
  });
777
362
  }
778
363
  // WS-5: over-budget consolidation advisory.
779
- if (perf.overBudgetRuns > 0) {
364
+ if (vm.perf.overBudgetRuns > 0) {
780
365
  pushItem({
781
366
  key: "over-budget-consolidation",
782
367
  prio: "P2",
783
368
  cls: "warn",
784
- title: `${perf.overBudgetRuns} consolidation run${perf.overBudgetRuns === 1 ? "" : "s"} exceeded budget`,
369
+ title: `${vm.perf.overBudgetRuns} consolidation run${vm.perf.overBudgetRuns === 1 ? "" : "s"} exceeded budget`,
785
370
  descHtml: "Consolidation phase wall time exceeded the total run budget on these runs. " +
786
371
  "Consider increasing the timeout or reducing the consolidation pool via profile config.",
787
372
  remedy: "akm config show",
788
373
  });
789
374
  }
790
375
  // Session-log notes (informational, lowest priority).
791
- if (result.sessionLogAdvisories.length > 0) {
792
- const patterns = result.sessionLogAdvisories
376
+ if (vm.sessionLogAdvisories.length > 0) {
377
+ const patterns = vm.sessionLogAdvisories
793
378
  .slice(0, 6)
794
379
  .map((p) => `<li>${esc(p.topic)}</li>`)
795
380
  .join("");
@@ -797,17 +382,18 @@ export function buildHealthHtmlReplacements(result, opts) {
797
382
  key: "session-log-notes",
798
383
  prio: "P3",
799
384
  cls: "warn",
800
- title: `${result.sessionLogAdvisories.length} session-log note(s) (informational)`,
385
+ title: `${vm.sessionLogAdvisories.length} session-log note(s) (informational)`,
801
386
  descHtml: `<ul style="margin:4px 0 0 16px;padding:0;">${patterns}</ul>`,
802
387
  });
803
388
  }
804
389
  items.sort((a, b) => PRIO_RANK[a.prio] - PRIO_RANK[b.prio]);
805
- const actionItemsHtml = items.length > 0
390
+ return items.length > 0
806
391
  ? items.map(actionItemCard).join("\n")
807
392
  : passCard("No action items", "All checks passed and nothing needs attention for this window.");
808
- // ── Proposal rows ──────────────────────────────────────────────────────────
809
- const proposalRowsHtml = proposals.length > 0
810
- ? proposals
393
+ }
394
+ function renderProposalRowsHtml(vm) {
395
+ return vm.proposals.length > 0
396
+ ? vm.proposals
811
397
  .map((p, i) => {
812
398
  const tagCls = p.source === "extract" ? "tag-extract" : "tag-consolidate";
813
399
  return (`<tr><td>${i + 1}</td><td><code>${esc(p.ref)}</code></td>` +
@@ -816,24 +402,35 @@ export function buildHealthHtmlReplacements(result, opts) {
816
402
  })
817
403
  .join("\n")
818
404
  : '<tr><td colspan="4" style="text-align:center;color:var(--muted);">No pending proposals</td></tr>';
405
+ }
406
+ // ── Main builder: extract-then-render glue ──────────────────────────────────
407
+ /**
408
+ * Compute all 17 `%%TOKEN%%` replacements for the health HTML template.
409
+ * There is deliberately NO standalone `%%OVERALL_STATUS%%` token — the
410
+ * overall status is embedded in the pre-rendered badge / exec-summary
411
+ * fragments, matching the skill template. Advisories and "what to watch" are
412
+ * merged + de-duplicated into a single prioritized `%%ACTION_ITEMS_HTML%%`.
413
+ */
414
+ export function buildHealthHtmlReplacements(result, opts) {
415
+ const vm = buildHealthReportViewModel(result, opts);
819
416
  return {
820
- "%%ECHARTS_TAG%%": buildEchartsTag(opts),
821
- "%%REPORT_TITLE%%": esc(reportTitle),
822
- "%%WINDOW%%": esc(opts.window),
823
- "%%SINCE_HUMAN%%": sinceHuman,
824
- "%%RUN_COUNT%%": num(totalRuns),
825
- "%%STATUS_BADGE_HTML%%": `${statusBadge}\n ${failBadge}`,
826
- "%%EXEC_SUMMARY_HTML%%": execSummary,
827
- "%%KPI_CARDS_HTML%%": kpiCards,
828
- "%%RUNS_JS_CONST%%": runsJsConst,
829
- "%%DISTILL_REASONS_JSON%%": JSON.stringify(distillReasons),
830
- "%%SLICE_OPTIONS_HTML%%": buildSliceOptions(opts.window),
831
- "%%LLM_BY_STAGE_JSON%%": JSON.stringify(llm.byStage ?? {}),
832
- "%%SUMMARY_ROWS_HTML%%": summaryRowsHtml,
833
- "%%ACTION_ITEMS_HTML%%": actionItemsHtml,
834
- "%%PROPOSAL_ROWS_HTML%%": proposalRowsHtml,
835
- "%%PROPOSAL_COUNT%%": String(proposals.length),
836
- "%%GENERATED_AT%%": esc(generatedAt),
417
+ "%%ECHARTS_TAG%%": buildEchartsTag(),
418
+ "%%REPORT_TITLE%%": esc(vm.reportTitle),
419
+ "%%WINDOW%%": esc(vm.window),
420
+ "%%SINCE_HUMAN%%": renderSinceHuman(vm),
421
+ "%%RUN_COUNT%%": num(vm.totalRuns),
422
+ "%%STATUS_BADGE_HTML%%": renderStatusBadgeHtml(vm),
423
+ "%%EXEC_SUMMARY_HTML%%": renderExecSummary(vm),
424
+ "%%KPI_CARDS_HTML%%": renderKpiCards(vm),
425
+ "%%RUNS_JS_CONST%%": `const RUNS = ${JSON.stringify(vm.runs)};`,
426
+ "%%DISTILL_REASONS_JSON%%": JSON.stringify(vm.distillReasons),
427
+ "%%SLICE_OPTIONS_HTML%%": buildSliceOptions(vm),
428
+ "%%LLM_BY_STAGE_JSON%%": JSON.stringify(vm.llm.byStage ?? {}),
429
+ "%%SUMMARY_ROWS_HTML%%": renderSummaryRowsHtml(vm),
430
+ "%%ACTION_ITEMS_HTML%%": renderActionItems(vm),
431
+ "%%PROPOSAL_ROWS_HTML%%": renderProposalRowsHtml(vm),
432
+ "%%PROPOSAL_COUNT%%": String(vm.proposals.length),
433
+ "%%GENERATED_AT%%": esc(vm.generatedAt),
837
434
  "%%AKM_VERSION%%": esc(pkgVersion),
838
435
  };
839
436
  }