akm-cli 0.9.0-beta.9 → 0.9.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/CHANGELOG.md +592 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/help/help-improve.md +9 -6
  6. package/dist/assets/hints/cli-hints-full.md +6 -5
  7. package/dist/assets/profiles/default.json +9 -4
  8. package/dist/assets/profiles/frequent.json +1 -1
  9. package/dist/assets/profiles/memory-focus.json +1 -1
  10. package/dist/assets/profiles/proactive-maintenance.json +25 -0
  11. package/dist/assets/profiles/quick.json +1 -1
  12. package/dist/assets/profiles/recombine-only.json +21 -0
  13. package/dist/assets/profiles/reflect-distill.json +30 -0
  14. package/dist/assets/profiles/synthesize.json +15 -0
  15. package/dist/assets/profiles/thorough.json +1 -1
  16. package/dist/assets/prompts/consolidate-system.md +23 -0
  17. package/dist/assets/prompts/contradiction-judge.md +33 -0
  18. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  19. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  20. package/dist/assets/prompts/extract-session.md +11 -3
  21. package/dist/assets/prompts/graph-extract-system.md +1 -0
  22. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  23. package/dist/assets/prompts/memory-infer-system.md +1 -0
  24. package/dist/assets/prompts/memory-infer-user.md +5 -0
  25. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  26. package/dist/assets/prompts/procedural-system.md +44 -0
  27. package/dist/assets/prompts/recombine-system.md +40 -0
  28. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  29. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  39. package/dist/assets/templates/html/health.html +281 -111
  40. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  41. package/dist/assets/wiki/schema-template.md +4 -4
  42. package/dist/cli/clack.js +56 -0
  43. package/dist/cli/config-migrate.js +7 -1
  44. package/dist/cli/confirm.js +1 -1
  45. package/dist/cli/parse-args.js +46 -1
  46. package/dist/cli/shared.js +28 -0
  47. package/dist/cli.js +25 -21
  48. package/dist/commands/agent/agent-dispatch.js +3 -2
  49. package/dist/commands/agent/agent-support.js +0 -7
  50. package/dist/commands/agent/contribute-cli.js +26 -7
  51. package/dist/commands/config-cli.js +26 -13
  52. package/dist/commands/env/child-env.js +47 -0
  53. package/dist/commands/env/env-cli.js +220 -227
  54. package/dist/commands/env/env.js +14 -67
  55. package/dist/commands/env/secret-cli.js +140 -138
  56. package/dist/commands/feedback-cli.js +156 -155
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +3 -3
  59. package/dist/commands/health/advisories.js +151 -0
  60. package/dist/commands/health/checks.js +103 -16
  61. package/dist/commands/health/html-report.js +447 -81
  62. package/dist/commands/health/improve-metrics.js +771 -0
  63. package/dist/commands/health/llm-usage.js +65 -0
  64. package/dist/commands/health/md-report.js +103 -0
  65. package/dist/commands/health/metrics.js +278 -0
  66. package/dist/commands/health/stash-exposure.js +46 -0
  67. package/dist/commands/health/surfaces.js +216 -0
  68. package/dist/commands/health/task-runs.js +135 -0
  69. package/dist/commands/health/types.js +26 -0
  70. package/dist/commands/health/windows.js +195 -0
  71. package/dist/commands/health.js +91 -1091
  72. package/dist/commands/improve/anti-collapse.js +170 -0
  73. package/dist/commands/improve/calibration.js +161 -0
  74. package/dist/commands/improve/collapse-detector.js +421 -0
  75. package/dist/commands/improve/consolidate/chunking.js +141 -0
  76. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  77. package/dist/commands/improve/consolidate/merge.js +145 -0
  78. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  79. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  80. package/dist/commands/improve/consolidate.js +1295 -1277
  81. package/dist/commands/improve/dedup.js +482 -0
  82. package/dist/commands/improve/distill/content-repair.js +202 -0
  83. package/dist/commands/improve/distill/promote-memory.js +229 -0
  84. package/dist/commands/improve/distill/quality-gate.js +236 -0
  85. package/dist/commands/improve/distill-guards.js +127 -0
  86. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  87. package/dist/commands/improve/distill.js +228 -605
  88. package/dist/commands/improve/eligibility.js +434 -0
  89. package/dist/commands/improve/encoding-salience.js +205 -0
  90. package/dist/commands/improve/extract-cli.js +179 -59
  91. package/dist/commands/improve/extract-prompt.js +54 -3
  92. package/dist/commands/improve/extract-watch.js +140 -0
  93. package/dist/commands/improve/extract.js +409 -43
  94. package/dist/commands/improve/feedback-valence.js +54 -0
  95. package/dist/commands/improve/hot-probation.js +45 -0
  96. package/dist/commands/improve/improve-auto-accept.js +157 -10
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +28 -8
  99. package/dist/commands/improve/improve-result-file.js +15 -25
  100. package/dist/commands/improve/improve-session.js +58 -0
  101. package/dist/commands/improve/improve.js +485 -2764
  102. package/dist/commands/improve/locks.js +154 -0
  103. package/dist/commands/improve/loop-stages.js +1100 -0
  104. package/dist/commands/improve/memory/memory-belief.js +14 -15
  105. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  106. package/dist/commands/improve/memory/memory-improve.js +27 -27
  107. package/dist/commands/improve/outcome-loop.js +270 -0
  108. package/dist/commands/improve/preparation.js +2002 -0
  109. package/dist/commands/improve/proactive-maintenance.js +37 -35
  110. package/dist/commands/improve/procedural.js +398 -0
  111. package/dist/commands/improve/recombine.js +818 -0
  112. package/dist/commands/improve/reflect-noise.js +0 -0
  113. package/dist/commands/improve/reflect.js +206 -45
  114. package/dist/commands/improve/salience.js +455 -0
  115. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  116. package/dist/commands/improve/shared.js +51 -0
  117. package/dist/commands/improve/triage.js +93 -0
  118. package/dist/commands/lint/agent-linter.js +19 -24
  119. package/dist/commands/lint/base-linter.js +173 -60
  120. package/dist/commands/lint/command-linter.js +19 -24
  121. package/dist/commands/lint/env-key-rules.js +38 -1
  122. package/dist/commands/lint/fact-linter.js +39 -0
  123. package/dist/commands/lint/index.js +31 -13
  124. package/dist/commands/lint/memory-linter.js +1 -1
  125. package/dist/commands/lint/registry.js +7 -2
  126. package/dist/commands/lint/task-linter.js +3 -3
  127. package/dist/commands/lint/workflow-linter.js +26 -1
  128. package/dist/commands/observability-cli.js +4 -4
  129. package/dist/commands/proposal/drain-policies.js +13 -4
  130. package/dist/commands/proposal/drain.js +45 -51
  131. package/dist/commands/proposal/legacy-import.js +115 -0
  132. package/dist/commands/proposal/proposal-cli.js +24 -34
  133. package/dist/commands/proposal/proposal.js +2 -1
  134. package/dist/commands/proposal/propose.js +8 -3
  135. package/dist/commands/proposal/repository.js +829 -0
  136. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  137. package/dist/commands/proposal/validators/proposals.js +93 -895
  138. package/dist/commands/read/curate.js +410 -111
  139. package/dist/commands/read/knowledge.js +10 -3
  140. package/dist/commands/read/remember-cli.js +133 -138
  141. package/dist/commands/read/search-cli.js +15 -8
  142. package/dist/commands/read/search.js +22 -11
  143. package/dist/commands/read/show.js +106 -14
  144. package/dist/commands/registry-cli.js +76 -87
  145. package/dist/commands/remember.js +11 -12
  146. package/dist/commands/sources/add-cli.js +91 -95
  147. package/dist/commands/sources/history.js +1 -1
  148. package/dist/commands/sources/init.js +66 -18
  149. package/dist/commands/sources/installed-stashes.js +11 -3
  150. package/dist/commands/sources/schema-repair.js +44 -46
  151. package/dist/commands/sources/self-update.js +2 -2
  152. package/dist/commands/sources/source-add.js +7 -3
  153. package/dist/commands/sources/sources-cli.js +3 -3
  154. package/dist/commands/sources/stash-cli.js +19 -39
  155. package/dist/commands/sources/stash-skeleton.js +57 -8
  156. package/dist/commands/tasks/default-tasks.js +15 -2
  157. package/dist/commands/tasks/tasks-cli.js +20 -29
  158. package/dist/commands/tasks/tasks.js +39 -11
  159. package/dist/commands/wiki-cli.js +23 -38
  160. package/dist/commands/workflow-cli.js +15 -1
  161. package/dist/core/asset/asset-registry.js +3 -1
  162. package/dist/core/asset/asset-spec.js +21 -4
  163. package/dist/core/asset/frontmatter.js +188 -167
  164. package/dist/core/asset/markdown.js +8 -0
  165. package/dist/core/authoring-rules.js +92 -0
  166. package/dist/core/common.js +4 -23
  167. package/dist/core/concurrent.js +10 -1
  168. package/dist/core/config/config-io.js +10 -1
  169. package/dist/core/config/config-migration.js +18 -40
  170. package/dist/core/config/config-schema.js +382 -62
  171. package/dist/core/config/config-types.js +3 -3
  172. package/dist/core/config/config.js +67 -22
  173. package/dist/core/deep-merge.js +38 -0
  174. package/dist/core/errors.js +1 -0
  175. package/dist/core/eval/rank-metrics.js +113 -0
  176. package/dist/core/events.js +4 -7
  177. package/dist/core/improve-types.js +47 -8
  178. package/dist/core/logs-db.js +14 -75
  179. package/dist/core/parse.js +36 -16
  180. package/dist/core/paths.js +18 -18
  181. package/dist/core/standards/resolve-standards-context.js +87 -0
  182. package/dist/core/standards/resolve-stash-standards.js +99 -0
  183. package/dist/core/standards/resolve-type-conventions.js +66 -0
  184. package/dist/core/state/migrations.js +770 -0
  185. package/dist/core/state-db.js +132 -1126
  186. package/dist/core/structured.js +69 -0
  187. package/dist/core/time.js +53 -0
  188. package/dist/core/warn.js +21 -0
  189. package/dist/core/write-source.js +37 -0
  190. package/dist/indexer/db/db.js +259 -769
  191. package/dist/indexer/db/entry-mapper.js +41 -0
  192. package/dist/indexer/db/graph-db.js +129 -86
  193. package/dist/indexer/db/llm-cache.js +2 -2
  194. package/dist/indexer/db/schema.js +516 -0
  195. package/dist/indexer/ensure-index.js +36 -92
  196. package/dist/indexer/feedback/utility-policy.js +75 -0
  197. package/dist/indexer/graph/graph-boost.js +51 -41
  198. package/dist/indexer/graph/graph-extraction.js +207 -4
  199. package/dist/indexer/index-writer-lock.js +18 -11
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +182 -204
  202. package/dist/indexer/passes/dir-staleness.js +114 -0
  203. package/dist/indexer/passes/memory-inference.js +13 -5
  204. package/dist/indexer/passes/metadata.js +20 -0
  205. package/dist/indexer/read-preflight.js +23 -0
  206. package/dist/indexer/search/db-search.js +89 -13
  207. package/dist/indexer/search/fts-query.js +51 -0
  208. package/dist/indexer/search/ranking-contributors.js +95 -9
  209. package/dist/indexer/search/ranking.js +79 -3
  210. package/dist/indexer/search/search-fields.js +6 -0
  211. package/dist/indexer/search/search-source.js +32 -21
  212. package/dist/indexer/search/semantic-status.js +4 -0
  213. package/dist/indexer/walk/matchers.js +9 -0
  214. package/dist/indexer/walk/walker.js +21 -13
  215. package/dist/integrations/agent/builders.js +39 -13
  216. package/dist/integrations/agent/config.js +20 -59
  217. package/dist/integrations/agent/detect.js +9 -0
  218. package/dist/integrations/agent/index.js +3 -19
  219. package/dist/integrations/agent/model-aliases.js +7 -2
  220. package/dist/integrations/agent/profiles.js +7 -1
  221. package/dist/integrations/agent/prompts.js +75 -9
  222. package/dist/integrations/agent/runner-dispatch.js +59 -0
  223. package/dist/integrations/agent/runner.js +13 -9
  224. package/dist/integrations/agent/spawn.js +69 -67
  225. package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
  226. package/dist/integrations/harnesses/claude/index.js +2 -0
  227. package/dist/integrations/harnesses/claude/session-log.js +10 -0
  228. package/dist/integrations/harnesses/index.js +2 -3
  229. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  230. package/dist/integrations/harnesses/opencode/index.js +2 -0
  231. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  232. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  233. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
  234. package/dist/integrations/harnesses/types.js +1 -0
  235. package/dist/integrations/session-logs/index.js +16 -0
  236. package/dist/llm/call-ai.js +2 -2
  237. package/dist/llm/client.js +34 -11
  238. package/dist/llm/embedder.js +67 -4
  239. package/dist/llm/embedders/cache.js +3 -1
  240. package/dist/llm/embedders/deterministic.js +66 -0
  241. package/dist/llm/embedders/local.js +73 -3
  242. package/dist/llm/feature-gate.js +16 -15
  243. package/dist/llm/graph-extract.js +67 -44
  244. package/dist/llm/memory-infer-impl.js +138 -0
  245. package/dist/llm/memory-infer.js +1 -127
  246. package/dist/llm/metadata-enhance.js +44 -31
  247. package/dist/llm/structured-call.js +49 -0
  248. package/dist/migrate-storage-node.mjs +8 -0
  249. package/dist/output/context.js +5 -5
  250. package/dist/output/renderers.js +85 -14
  251. package/dist/output/shapes/curate.js +14 -2
  252. package/dist/output/shapes/helpers.js +0 -3
  253. package/dist/output/shapes/passthrough.js +2 -1
  254. package/dist/output/text/helpers.js +29 -1
  255. package/dist/output/text/workflow.js +1 -0
  256. package/dist/registry/providers/skills-sh.js +21 -147
  257. package/dist/registry/providers/static-index.js +15 -157
  258. package/dist/registry/resolve.js +27 -9
  259. package/dist/runtime.js +25 -1
  260. package/dist/scripts/migrate-storage.js +2661 -2369
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
  262. package/dist/setup/detect.js +9 -0
  263. package/dist/setup/legacy-config.js +106 -0
  264. package/dist/setup/prompt.js +57 -0
  265. package/dist/setup/providers.js +14 -0
  266. package/dist/setup/registry-stash-loader.js +12 -0
  267. package/dist/setup/semantic-assets.js +124 -0
  268. package/dist/setup/setup.js +52 -1614
  269. package/dist/setup/steps/connection.js +734 -0
  270. package/dist/setup/steps/output.js +31 -0
  271. package/dist/setup/steps/platforms.js +124 -0
  272. package/dist/setup/steps/semantic.js +27 -0
  273. package/dist/setup/steps/sources.js +222 -0
  274. package/dist/setup/steps/stashdir.js +42 -0
  275. package/dist/setup/steps/tasks.js +152 -0
  276. package/dist/sources/include.js +6 -2
  277. package/dist/sources/providers/filesystem.js +0 -1
  278. package/dist/sources/providers/git-install.js +210 -0
  279. package/dist/sources/providers/git-provider.js +234 -0
  280. package/dist/sources/providers/git-stash.js +248 -0
  281. package/dist/sources/providers/git.js +10 -661
  282. package/dist/sources/providers/npm.js +2 -6
  283. package/dist/sources/providers/provider-utils.js +13 -7
  284. package/dist/sources/providers/sync-from-ref.js +9 -1
  285. package/dist/sources/providers/website.js +9 -5
  286. package/dist/sources/website-ingest.js +187 -29
  287. package/dist/sources/wiki-fetchers/registry.js +53 -0
  288. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  289. package/dist/storage/database.js +45 -10
  290. package/dist/storage/managed-db.js +82 -0
  291. package/dist/storage/repositories/canaries-repository.js +107 -0
  292. package/dist/storage/repositories/consolidation-repository.js +38 -0
  293. package/dist/storage/repositories/embeddings-repository.js +72 -0
  294. package/dist/storage/repositories/events-repository.js +187 -0
  295. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  296. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  297. package/dist/storage/repositories/index-db.js +14 -8
  298. package/dist/storage/repositories/proposals-repository.js +220 -0
  299. package/dist/storage/repositories/recombine-repository.js +213 -0
  300. package/dist/storage/repositories/registry-cache.js +93 -0
  301. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  302. package/dist/storage/repositories/task-history-repository.js +93 -0
  303. package/dist/storage/sqlite-pragmas.js +146 -0
  304. package/dist/tasks/backends/cron.js +1 -1
  305. package/dist/tasks/backends/index.js +9 -0
  306. package/dist/tasks/backends/launchd.js +1 -1
  307. package/dist/tasks/backends/schtasks.js +1 -1
  308. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  309. package/dist/tasks/runner.js +15 -13
  310. package/dist/text-import-hook.mjs +0 -0
  311. package/dist/wiki/wiki.js +52 -11
  312. package/dist/workflows/cli.js +1 -0
  313. package/dist/workflows/db.js +3 -4
  314. package/dist/workflows/runtime/runs.js +43 -118
  315. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  316. package/dist/workflows/validate-summary.js +2 -7
  317. package/docs/README.md +69 -18
  318. package/docs/data-and-telemetry.md +5 -4
  319. package/docs/migration/release-notes/0.7.0.md +1 -1
  320. package/docs/migration/release-notes/0.9.0.md +39 -0
  321. package/package.json +10 -10
  322. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  323. package/dist/commands/db-cli.js +0 -23
  324. package/dist/indexer/db/db-backup.js +0 -376
  325. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -51,23 +51,38 @@
51
51
  * be invoked from CI / automation without spinning up an agent harness.
52
52
  */
53
53
  import fs from "node:fs";
54
- import path from "node:path";
54
+ import distillKnowledgeSystemPrompt from "../../assets/prompts/distill-knowledge-system.md" with { type: "text" };
55
+ import distillLessonSystemPrompt from "../../assets/prompts/distill-lesson-system.md" with { type: "text" };
55
56
  import { parseAssetRef } from "../../core/asset/asset-ref.js";
56
57
  import { assembleAssetFromString } from "../../core/asset/asset-serialize.js";
57
- import { parseFrontmatter } from "../../core/asset/frontmatter.js";
58
+ import { parseFrontmatter, writeSalienceToFrontmatter } from "../../core/asset/frontmatter.js";
58
59
  import { stripMarkdownFences } from "../../core/asset/markdown.js";
59
- import { resolveStashDir, timestampForFilename } from "../../core/common.js";
60
- import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
60
+ import { authoringRulesForType } from "../../core/authoring-rules.js";
61
+ import { resolveStashDir } from "../../core/common.js";
62
+ import { getDefaultLlmConfig, getImproveProcessConfig, loadConfig } from "../../core/config/config.js";
61
63
  import { ConfigError, UsageError } from "../../core/errors.js";
62
64
  import { appendEvent, readEvents } from "../../core/events.js";
63
65
  import { lintLessonContent } from "../../core/lesson-lint.js";
66
+ import { getDbPath } from "../../core/paths.js";
67
+ import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
68
+ import { withStateDb } from "../../core/state-db.js";
64
69
  import { warnVerbose } from "../../core/warn.js";
70
+ import { closeDatabase, getAllEntries, openIndexDatabase } from "../../indexer/db/db.js";
65
71
  import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
66
72
  import { chatCompletion, parseEmbeddedJsonResponse } from "../../llm/client.js";
67
73
  import { isLlmFeatureEnabled, tryLlmFeature } from "../../llm/feature-gate.js";
68
- import { createProposal, isProposalSkipped, listProposals, } from "../proposal/validators/proposals.js";
69
- import { akmSearch } from "../read/search.js";
70
- import { assessMemoryKnowledgePromotionCandidate, deriveKnowledgeRef } from "./distill-promotion-policy.js";
74
+ import { createProposal, isProposalSkipped, listProposals, } from "../proposal/repository.js";
75
+ import { stripFrontmatterBody as stripBodyForFidelity } from "./dedup.js";
76
+ import { autoRepairLessonFrontmatter, autoSwapDescriptionWhenToUse, collectLessonQualityFindings, repairLessonDescriptionTruncation, } from "./distill/content-repair.js";
77
+ import { promoteMemoryToKnowledge } from "./distill/promote-memory.js";
78
+ import { fetchTopSimilarLessons, persistOutputEncodingSalience, runLessonQualityJudge, writeQualityRejection, } from "./distill/quality-gate.js";
79
+ import { buildClsContext, checkDistillFidelity } from "./distill-guards.js";
80
+ import { deriveKnowledgeRef } from "./distill-promotion-policy.js";
81
+ import { buildRefVocabulary, scoreEncodingSalience } from "./encoding-salience.js";
82
+ import { computeSalience, upsertAssetSalience } from "./salience.js";
83
+ // Re-exported for `reflect.ts`, which applies the same LLM-as-judge gate to
84
+ // reflect proposals (R-5 / #374).
85
+ export { runLessonQualityJudge };
71
86
  /**
72
87
  * Asset-ref types that `akm distill` structurally refuses as inputs.
73
88
  *
@@ -76,6 +91,11 @@ import { assessMemoryKnowledgePromotionCandidate, deriveKnowledgeRef } from "./d
76
91
  * `lesson:lesson-<name>-lesson-lesson` (double `-lesson` suffix) — the
77
92
  * recursive-ref defect observed across 323 archived rejected proposals.
78
93
  *
94
+ * 08-F2: `env` and `secret` are refused as a STRUCTURAL floor — distill reads
95
+ * the input asset's bytes via `readFileSync` and hands them to the LLM, so
96
+ * secret material must never be a distill input. This gate is code, not config:
97
+ * it holds even when `allowedTypes` config is mis-set in unattended cron.
98
+ *
79
99
  * The runtime gate inside {@link akmDistill} still refuses these inputs
80
100
  * defensively (returning an `outcome: "skipped"` envelope with `skipReason:
81
101
  * "recursive_lesson_input"`). This exported set is the planner-side companion:
@@ -87,7 +107,7 @@ import { assessMemoryKnowledgePromotionCandidate, deriveKnowledgeRef } from "./d
87
107
  * type means updating this constant — the planner picks the change up for
88
108
  * free.
89
109
  */
90
- export const DISTILL_REFUSED_INPUT_TYPES = new Set(["lesson"]);
110
+ export const DISTILL_REFUSED_INPUT_TYPES = new Set(["lesson", "env", "secret"]);
91
111
  /**
92
112
  * Returns true when `type` is structurally refused as an input by
93
113
  * {@link akmDistill}. See {@link DISTILL_REFUSED_INPUT_TYPES}.
@@ -112,7 +132,6 @@ export function deriveLessonRef(inputRef) {
112
132
  .replace(/^-|-$/g, "");
113
133
  return `lesson:${safe}-lesson`;
114
134
  }
115
- import { repairTruncatedDescription } from "../../core/text-truncation.js";
116
135
  // ── Content quality validators ──────────────────────────────────────────────
117
136
  //
118
137
  // The actual implementations now live in `core/proposal-quality-validators.ts`
@@ -122,68 +141,8 @@ import { repairTruncatedDescription } from "../../core/text-truncation.js";
122
141
  import { detectDoubleFrontmatter, isValidDescription, isValidWhenToUse, } from "../proposal/validators/proposal-quality-validators.js";
123
142
  export { detectDoubleFrontmatter, isValidDescription, isValidWhenToUse };
124
143
  // ── Prompt assembly ─────────────────────────────────────────────────────────
125
- const LESSON_SYSTEM_PROMPT = [
126
- "You are the akm `distill` distiller.",
127
- "Given an asset and recent feedback events about it, produce a single",
128
- "concise *lesson* an agent should remember next time it works on this",
129
- "asset's domain.",
130
- "",
131
- "YOUR RESPONSE MUST START EXACTLY WITH `---` ON THE VERY FIRST LINE.",
132
- "DO NOT output any prose, explanation, or code fences before or after.",
133
- "",
134
- "Required output format — copy this structure exactly:",
135
- "---",
136
- "description: <one complete sentence (ending with `.`) summarising what the lesson teaches>",
137
- "when_to_use: <one complete sentence describing the concrete trigger condition>",
138
- "---",
139
- "",
140
- "<lesson body — plain markdown, 1–3 short paragraphs of practical guidance>",
141
- "",
142
- "## description field (MANDATORY)",
143
- "- A single complete sentence in present tense, 80-200 chars, NO markdown.",
144
- "- Self-contained: a reviewer must understand the lesson from this field alone.",
145
- '- DO NOT start with "When ", "If ", or a connector word — that belongs in when_to_use.',
146
- '- DO NOT copy a section heading ("Key takeaways", "For example", "Key pitfalls").',
147
- "- DO NOT begin with a numbered list marker, code fence, or markdown heading.",
148
- "",
149
- 'GOOD: "Always validate ref existence before promoting a memory to knowledge; missing refs surface as silent 404s during accept."',
150
- 'BAD: "Key pitfalls"',
151
- 'BAD: "When working with the akm CLI"',
152
- 'BAD: "For example, you might..."',
153
- 'BAD: "1. Check the file"',
154
- "",
155
- "RULES:",
156
- "- `when_to_use` MUST be a complete sentence describing a concrete trigger. Never write `When working with <asset-name>` — that is circular and useless.",
157
- "- `description` and `when_to_use` MUST differ from each other.",
158
- "- The lesson body MUST be non-empty markdown prose. Do NOT restate `description:` or `when_to_use:` inside the body (no `**description:** ...` or `**when_to_use:** ...` lines — the frontmatter is the only place those keys belong).",
159
- "- Do NOT emit a second `---` fence after the opening frontmatter — there are exactly two `---` lines in the output, both belonging to the single frontmatter block at the top.",
160
- "- Do NOT reproduce the source asset verbatim — distil what a caller needs to know.",
161
- "- Output ONLY the lesson file. No preamble, no code fences, no trailing prose.",
162
- ].join("\n");
163
- const KNOWLEDGE_SYSTEM_PROMPT = [
164
- "You are the akm `distill` distiller.",
165
- "Given an asset and recent feedback events about it, produce a concise",
166
- "*knowledge* markdown document capturing the durable, reusable facts.",
167
- "Prefer stable guidance over narrative recap.",
168
- "",
169
- "YOUR RESPONSE MUST START EXACTLY WITH `---` ON THE VERY FIRST LINE.",
170
- "DO NOT output any prose, explanation, or code fences before or after.",
171
- "",
172
- "Required output format:",
173
- "---",
174
- "description: <one-line summary of the knowledge asset>",
175
- "tags: [<tag1>, <tag2>]",
176
- "---",
177
- "",
178
- "# <Title>",
179
- "",
180
- "<body — structured markdown, durable facts only>",
181
- "",
182
- "RULES:",
183
- "- `description` MUST be a non-empty single-line string.",
184
- "- Include a meaningful markdown body with a `# Title` heading.",
185
- "- Output ONLY the knowledge file. No preamble, no code fences, no trailing prose.",
186
- ].join("\n");
144
+ const LESSON_SYSTEM_PROMPT = distillLessonSystemPrompt;
145
+ const KNOWLEDGE_SYSTEM_PROMPT = distillKnowledgeSystemPrompt;
187
146
  // ── Structured-output schemas (responseSchema lift) ─────────────────────────
188
147
  //
189
148
  // PR 1 of the asset-writers decision (see knowledge:projects/akm/
@@ -358,6 +317,18 @@ export function buildDistillPrompt(input) {
358
317
  const lines = [];
359
318
  lines.push(`Asset ref: ${input.inputRef}`);
360
319
  lines.push("");
320
+ if (input.standardsContext?.trim()) {
321
+ lines.push("Standards to follow (the rulebook for this target):");
322
+ lines.push(input.standardsContext.trim());
323
+ lines.push("");
324
+ }
325
+ {
326
+ const authoringRules = authoringRulesForType(input.proposalKind ?? "lesson");
327
+ if (authoringRules) {
328
+ lines.push(authoringRules);
329
+ lines.push("");
330
+ }
331
+ }
361
332
  lines.push("Asset content:");
362
333
  if (input.assetContent) {
363
334
  const body = input.assetContent.trim().slice(0, 3000);
@@ -443,181 +414,6 @@ export function buildDistillPrompt(input) {
443
414
  }
444
415
  return lines.join("\n");
445
416
  }
446
- // ── D-4 / #390: Top-3 similar lessons retrieval ──────────────────────────────
447
- /**
448
- * Default implementation: use akmSearch to find top-N similar lesson assets.
449
- * Returns empty array when search fails or returns no results.
450
- * Requires embedding configured for semantic similarity; degrades gracefully.
451
- */
452
- async function fetchTopSimilarLessons(query, n, _stashDir) {
453
- try {
454
- const result = await akmSearch({
455
- query,
456
- type: "lesson",
457
- limit: n,
458
- skipLogging: true,
459
- eventSource: "improve",
460
- });
461
- const hits = result?.hits ?? [];
462
- return hits
463
- .filter((h) => "path" in h && typeof h.path === "string")
464
- .slice(0, n)
465
- .map((h) => {
466
- let content = "";
467
- try {
468
- if (h.path && fs.existsSync(h.path)) {
469
- content = fs.readFileSync(h.path, "utf8");
470
- }
471
- }
472
- catch {
473
- /* best-effort */
474
- }
475
- return { ref: h.ref, content };
476
- });
477
- }
478
- catch {
479
- return [];
480
- }
481
- }
482
- // ── LLM-as-judge quality gate (P2-B) ────────────────────────────────────────
483
- /**
484
- * D-4 / #390: Build the LLM-as-judge prompt.
485
- *
486
- * When similarLessons are provided (top-3 by embedding similarity), they are
487
- * included in the context so the judge can lower the score for near-duplicates.
488
- * Voyager arXiv:2305.16291 — skill library admission requires similarity check
489
- * against the existing library. A-MEM arXiv:2502.12110 — new notes are checked
490
- * against existing notes before linking.
491
- */
492
- function buildJudgePrompt(lessonContent, sourceContent, similarLessons) {
493
- const lines = [
494
- "You are evaluating a proposed lesson asset for an akm knowledge base.",
495
- "",
496
- "Score this lesson on each criterion from 1 (poor) to 5 (excellent):",
497
- "1. NOVELTY: Does the lesson add information not already present in the source asset?",
498
- "2. ACTIONABILITY: Can an agent follow this lesson without additional context?",
499
- "3. NON-REDUNDANCY: Is this lesson meaningfully different from what the source already says?",
500
- "",
501
- "Source asset content:",
502
- "```",
503
- sourceContent.slice(0, 2000),
504
- "```",
505
- ];
506
- if (similarLessons && similarLessons.length > 0) {
507
- lines.push("");
508
- lines.push("Existing similar lessons (top-3 by similarity). Rate lower if the proposed lesson is substantially similar to any of these:");
509
- for (const sl of similarLessons) {
510
- lines.push(`\nExisting lesson ref: ${sl.ref}`);
511
- lines.push("```");
512
- lines.push(sl.content.slice(0, 500));
513
- lines.push("```");
514
- }
515
- }
516
- lines.push("");
517
- lines.push("Proposed lesson content:");
518
- lines.push("```");
519
- lines.push(lessonContent.slice(0, 1000));
520
- lines.push("```");
521
- lines.push("");
522
- lines.push('Return ONLY valid JSON, no prose: {"score": <average score 1-5 as float>, "reason": "<one sentence>"}');
523
- return lines.join("\n");
524
- }
525
- /**
526
- * Run the LLM-as-judge quality gate on a proposal's content.
527
- *
528
- * Exported so reflect.ts can apply the same gate to reflect proposals (R-5 / #374).
529
- * Gated by the flag name `lesson_quality_gate` (or its alias
530
- * `proposal_quality_gate`) via {@link isLlmFeatureEnabled} — which reads
531
- * `profiles.improve.default.processes.distill.qualityGate.enabled` (and the
532
- * corresponding `.reflect.qualityGate.enabled` for proposals).
533
- *
534
- * Fail-open: returns `pass: true` on timeout, parse failure, or missing LLM.
535
- */
536
- export async function runLessonQualityJudge(config, lessonContent, sourceContent, chat,
537
- /** D-4 / #390: top-3 similar existing lessons for dedup check. */
538
- similarLessons) {
539
- const llmConfig = getDefaultLlmConfig(config);
540
- if (!llmConfig) {
541
- return { pass: true, score: -1, reason: "no LLM configured — passing through" };
542
- }
543
- const judgeLlmConfig = llmConfig.judgeModel ? { ...llmConfig, model: llmConfig.judgeModel } : llmConfig;
544
- const JUDGE_TIMEOUT_MS = 8_000;
545
- try {
546
- const raw = await Promise.race([
547
- chat(judgeLlmConfig, [
548
- { role: "system", content: "Return only valid JSON. No prose." },
549
- { role: "user", content: buildJudgePrompt(lessonContent, sourceContent, similarLessons) },
550
- ]),
551
- new Promise((_, reject) => setTimeout(() => reject(new Error("judge timeout")), JUDGE_TIMEOUT_MS)),
552
- ]);
553
- const parsed = parseEmbeddedJsonResponse(raw);
554
- if (!parsed || typeof parsed.score !== "number") {
555
- return { pass: true, score: -1, reason: "judge parse failed — passing through" };
556
- }
557
- // D-5 / #388: Three-band system (MT-Bench arXiv:2306.05685 — ~±0.5 judge variance).
558
- // >= 3.5: auto-queue as pending (pass: true)
559
- // 2.5–3.5: review-needed band — uncertain, escalate to human (reviewNeeded: true)
560
- // < 2.5: auto-reject (pass: false)
561
- const score = parsed.score;
562
- const reason = parsed.reason ?? "";
563
- if (score >= 3.5) {
564
- return { pass: true, score, reason };
565
- }
566
- if (score >= 2.5) {
567
- // Uncertainty band: treat as failed for auto-queuing but flag for review.
568
- return { pass: false, score, reason, reviewNeeded: true };
569
- }
570
- return { pass: false, score, reason };
571
- }
572
- catch {
573
- return { pass: true, score: -1, reason: "judge failed — passing through" };
574
- }
575
- }
576
- // ── Quality-rejection helper ─────────────────────────────────────────────────
577
- /**
578
- * Write a rejected lesson to `.akm/distill-rejected/`, append a `distill_invoked`
579
- * quality-rejected event, and return the `quality_rejected` envelope.
580
- *
581
- * @param stash - Root stash directory.
582
- * @param inputRef - The original input ref (for the event).
583
- * @param lessonRef - The proposed lesson/knowledge ref.
584
- * @param content - The raw content that failed the quality gate.
585
- * @param score - Quality score from the judge.
586
- * @param reason - Human-readable rejection reason.
587
- * @param extraMeta - Optional additional metadata for the event.
588
- */
589
- function writeQualityRejection(stash, inputRef, lessonRef, content, score, reason, extraMeta = {}, eligibilitySource) {
590
- // D-5 / #388: reviewNeeded flag selects "review_needed" vs "quality_rejected" outcome.
591
- const outcome = extraMeta.reviewNeeded ? "review_needed" : "quality_rejected";
592
- const rejectDir = path.join(stash, ".akm", "distill-rejected");
593
- fs.mkdirSync(rejectDir, { recursive: true });
594
- const ts = timestampForFilename();
595
- fs.writeFileSync(path.join(rejectDir, `${ts}-${lessonRef}.md`), `---\nscore: ${score}\nreason: ${reason}\noutcome: ${outcome}\n---\n\n${content}`, "utf8");
596
- appendEvent({
597
- eventType: "distill_invoked",
598
- ref: inputRef,
599
- metadata: {
600
- outcome,
601
- lessonRef,
602
- score,
603
- reason,
604
- ...extraMeta,
605
- // Attribution tagging: stamp the eligibility lane so distill_invoked can be
606
- // sliced by lane downstream. See EligibilitySource.
607
- ...(eligibilitySource ? { eligibilitySource } : {}),
608
- },
609
- });
610
- return {
611
- schemaVersion: 1,
612
- ok: true,
613
- outcome,
614
- inputRef,
615
- lessonRef,
616
- score,
617
- reason,
618
- ...extraMeta,
619
- };
620
- }
621
417
  // ── Main entry point ────────────────────────────────────────────────────────
622
418
  /**
623
419
  * Run a single bounded distillation pass for `ref`. Always emits exactly one
@@ -650,15 +446,21 @@ export async function akmDistill(options) {
650
446
  // the improve planner can skip these refs before queuing distill attempts;
651
447
  // this runtime check stays as a defensive backstop for direct callers.
652
448
  if (isDistillRefusedInputType(parsedInputRef.type)) {
653
- const skippedRef = `lesson:${parsedInputRef.name}`;
449
+ // 08-F2: env/secret are a secret-material refusal (never read the bytes);
450
+ // lesson is the recursive-form refusal. Both skip BEFORE any readFileSync.
451
+ const isSecretInput = parsedInputRef.type === "env" || parsedInputRef.type === "secret";
452
+ const skippedRef = isSecretInput ? inputRef : `lesson:${parsedInputRef.name}`;
453
+ const message = isSecretInput
454
+ ? `Distill refuses ${parsedInputRef.type} inputs — secret material must never be sent to the LLM.`
455
+ : "Distill refuses lesson inputs — lessons are the distilled form, not a source.";
654
456
  appendEvent({
655
457
  eventType: "distill_invoked",
656
458
  ref: inputRef,
657
459
  metadata: {
658
460
  outcome: "skipped",
659
461
  lessonRef: skippedRef,
660
- message: "distill refuses lesson inputs — lessons are the distilled form, not a source",
661
- skipReason: "recursive_lesson_input",
462
+ message,
463
+ skipReason: isSecretInput ? "refused_secret_input" : "recursive_lesson_input",
662
464
  ...eligMeta,
663
465
  },
664
466
  });
@@ -668,7 +470,7 @@ export async function akmDistill(options) {
668
470
  outcome: "skipped",
669
471
  inputRef,
670
472
  lessonRef: skippedRef,
671
- message: "Distill refuses lesson inputs — lessons are the distilled form, not a source.",
473
+ message,
672
474
  };
673
475
  }
674
476
  const config = options.config ?? loadConfig();
@@ -676,20 +478,95 @@ export async function akmDistill(options) {
676
478
  const chat = options.chat ?? chatCompletion;
677
479
  const lookup = options.lookupFn ?? defaultLookup;
678
480
  const readEventsImpl = options.readEventsFn ?? readEvents;
481
+ // R1 opt-out must flow into every computeSalience call this command makes so
482
+ // distill-written rank_score rows use the same weights as preparation's.
483
+ const outcomeWeightEnabled = config.improve?.salience?.outcomeWeightEnabled !== false;
679
484
  // D-4 / #390: similar-lessons retrieval seam (test-injectable).
680
485
  const fetchSimilarLessonsFn = options.fetchSimilarLessonsFn ?? ((query, n) => fetchTopSimilarLessons(query, n, options.stashDir));
681
486
  // Best-effort load: when the asset is not yet indexed we still proceed —
682
487
  // the LLM is asked to distil from "available signal" (feedback alone).
683
488
  let assetContent = null;
489
+ let assetFilePath = null;
684
490
  try {
685
491
  const filePath = await lookup(inputRef);
686
492
  if (filePath && fs.existsSync(filePath)) {
493
+ assetFilePath = filePath;
687
494
  assetContent = fs.readFileSync(filePath, "utf8");
688
495
  }
689
496
  }
690
497
  catch {
691
498
  assetContent = null;
692
499
  }
500
+ // ── #608: Encoding-time salience scoring ────────────────────────────────
501
+ // Score the source asset with the three-signal model (novelty × 0.40 +
502
+ // magnitude × 0.35 + predictionError × 0.25) and persist the result to:
503
+ // 1. The asset's frontmatter (human-readable mirror; idempotent delta gate).
504
+ // 2. state.db :: asset_salience (canonical; feeds improve's high-salience gate).
505
+ // Both writes are best-effort — a DB error never blocks distillation.
506
+ //
507
+ // The bigram ref vocabulary is built ONCE per invocation — the novelty signal
508
+ // reuses it when scoring the distilled OUTPUT at proposal creation (G4).
509
+ let existingRefVocabulary = new Set();
510
+ try {
511
+ const embCfg = config?.embedding;
512
+ const indexDb = openIndexDatabase(getDbPath(), embCfg?.dimension ? { embeddingDim: embCfg.dimension } : undefined);
513
+ try {
514
+ const allRefs = getAllEntries(indexDb).map((e) => e.entryKey);
515
+ existingRefVocabulary = buildRefVocabulary(allRefs);
516
+ }
517
+ finally {
518
+ closeDatabase(indexDb);
519
+ }
520
+ }
521
+ catch {
522
+ // Index not available — novelty defaults to type-floor.
523
+ }
524
+ if (assetContent && assetFilePath) {
525
+ try {
526
+ const parsedRef = parseAssetRef(inputRef);
527
+ // G4: predictionError decays with revision count — the prior hardcoded
528
+ // `revisionCount: 0` made it a dead constant 1.0. Use the number of
529
+ // proposals ever raised against this ref as the revision proxy.
530
+ let revisionCount = 0;
531
+ try {
532
+ revisionCount = listProposals(stash, { ref: inputRef, includeArchive: true }).length;
533
+ }
534
+ catch {
535
+ // best-effort: unknown history scores as a first encounter
536
+ }
537
+ const salienceResult = scoreEncodingSalience({
538
+ body: assetContent,
539
+ type: parsedRef.type,
540
+ existingRefVocabulary,
541
+ revisionCount,
542
+ });
543
+ // 1. Write salience to the source asset frontmatter (idempotent).
544
+ const updatedContent = writeSalienceToFrontmatter(assetContent, salienceResult.score, salienceResult);
545
+ if (updatedContent !== assetContent) {
546
+ fs.writeFileSync(assetFilePath, updatedContent, "utf8");
547
+ assetContent = updatedContent;
548
+ }
549
+ // 2. Persist encoding_salience to state.db.
550
+ try {
551
+ withStateDb((stateDb) => {
552
+ const vector = computeSalience({
553
+ ref: inputRef,
554
+ type: parsedRef.type,
555
+ retrievalFreq: 0,
556
+ encodingSalience: salienceResult.score,
557
+ outcomeWeightEnabled,
558
+ });
559
+ upsertAssetSalience(stateDb, inputRef, vector);
560
+ });
561
+ }
562
+ catch {
563
+ // State DB unavailable — frontmatter mirror is the only persistence.
564
+ }
565
+ }
566
+ catch {
567
+ // Scoring errors never block distillation.
568
+ }
569
+ }
693
570
  const { events } = readEventsImpl({
694
571
  ref: inputRef,
695
572
  type: "feedback",
@@ -713,190 +590,34 @@ export async function akmDistill(options) {
713
590
  eventType: e.eventType,
714
591
  ...(e.metadata !== undefined ? { metadata: e.metadata } : {}),
715
592
  }));
716
- const promotion = targetKind === "lesson"
717
- ? null
718
- : assessMemoryKnowledgePromotionCandidate({
719
- inputRef,
720
- assetContent,
721
- feedbackEvents: filteredEvents.map((event) => ({
722
- ...(event.metadata !== undefined ? { metadata: event.metadata } : {}),
723
- })),
724
- });
725
- if (promotion?.promote && promotion.content && (targetKind === "knowledge" || targetKind === "auto")) {
726
- // D-1 / #369: When the destination knowledge file already exists, route
727
- // through the LLM for contradiction resolution instead of silently
728
- // overwriting. Follows mem0 ADD/UPDATE/DELETE/NOOP pattern (arXiv:2504.19413 §3.2)
729
- // and A-MEM dynamic linking (arXiv:2502.12110).
730
- let resolvedPromotionContent = promotion.content;
731
- const existingKnowledgePath = await lookup(promotion.knowledgeRef);
732
- const existingKnowledgeContent = existingKnowledgePath && fs.existsSync(existingKnowledgePath)
733
- ? (() => {
734
- try {
735
- return fs.readFileSync(existingKnowledgePath, "utf8");
736
- }
737
- catch {
738
- return null;
739
- }
740
- })()
741
- : null;
742
- if (existingKnowledgeContent && config && getDefaultLlmConfig(config)) {
743
- // Existing content found: call LLM for contradiction-resolution merge.
744
- const mergePrompt = [
745
- "You are merging two versions of a knowledge document.",
746
- "Existing content is already committed; new content comes from a memory distillation run.",
747
- "Choose one of: ADD (combine both), UPDATE (replace existing with new), NOOP (keep existing unchanged).",
748
- 'Return ONLY valid JSON: {"action": "ADD"|"UPDATE"|"NOOP", "content": "<merged markdown if ADD/UPDATE, empty string if NOOP>"}',
749
- "",
750
- "## Existing knowledge content",
751
- "```",
752
- existingKnowledgeContent.slice(0, 3000),
753
- "```",
754
- "",
755
- "## New content from distillation",
756
- "```",
757
- promotion.content.slice(0, 3000),
758
- "```",
759
- ].join("\n");
760
- try {
761
- const mergeLlm = getDefaultLlmConfig(config);
762
- if (!mergeLlm) {
763
- throw new ConfigError("LLM is not configured for distillation merge.", "LLM_NOT_CONFIGURED");
764
- }
765
- const mergeResponse = await chat(mergeLlm, [
766
- { role: "system", content: "Return only valid JSON. No prose." },
767
- { role: "user", content: mergePrompt },
768
- ]);
769
- const mergeResult = parseEmbeddedJsonResponse(mergeResponse);
770
- if (mergeResult?.action === "NOOP") {
771
- // Existing content is authoritative — no update needed.
772
- appendEvent({
773
- eventType: "distill_invoked",
774
- ref: inputRef,
775
- metadata: {
776
- outcome: "skipped",
777
- lessonRef: promotion.knowledgeRef,
778
- message: "D-1: LLM resolved destination conflict as NOOP — existing content kept",
779
- ...eligMeta,
780
- },
781
- });
782
- return {
783
- schemaVersion: 1,
784
- ok: true,
785
- outcome: "skipped",
786
- inputRef,
787
- lessonRef: promotion.knowledgeRef,
788
- message: "Existing knowledge content unchanged (contradiction resolution: NOOP)",
789
- };
790
- }
791
- if (mergeResult?.action && (mergeResult.action === "ADD" || mergeResult.action === "UPDATE")) {
792
- if (mergeResult.content?.trim()) {
793
- resolvedPromotionContent = mergeResult.content;
794
- }
795
- }
796
- }
797
- catch {
798
- // LLM merge failed — fall through with the original promotion content.
799
- // The reviewer will see both versions in the proposal diff.
800
- }
801
- }
802
- else if (existingKnowledgeContent && config && !getDefaultLlmConfig(config)) {
803
- // No LLM configured: include existing content as context in the proposal
804
- // so the reviewer can do the contradiction resolution manually.
805
- resolvedPromotionContent = [
806
- promotion.content,
807
- "",
808
- "---",
809
- "<!-- D-1 / #369: Existing knowledge content is shown below for reviewer reference. -->",
810
- "<!-- Review: decide whether to ADD (merge), UPDATE (replace), or NOOP (keep existing). -->",
811
- "",
812
- "## Existing content (for reviewer reference)",
813
- "",
814
- existingKnowledgeContent,
815
- ].join("\n");
816
- }
817
- // Apply quality gate to fast-path knowledge promotion (Risk 4 fix).
818
- // D-5 / #388: Three-band system — review_needed band queues to proposal
819
- // queue with review_needed outcome rather than auto-rejecting.
820
- let knowledgeJudgeConfidence;
821
- if (isLlmFeatureEnabled(config, "lesson_quality_gate")) {
822
- // D-4 / #390: retrieve top-3 similar lessons for dedup check in judge.
823
- const similarLessons = await fetchSimilarLessonsFn(resolvedPromotionContent.slice(0, 500), 3);
824
- const judgeResult = await runLessonQualityJudge(config, resolvedPromotionContent, assetContent ?? "", chat, similarLessons.length > 0 ? similarLessons : undefined);
825
- if (!judgeResult.pass) {
826
- if (judgeResult.reviewNeeded) {
827
- // Uncertainty band (2.5–3.5): queue as review_needed instead of rejecting.
828
- return writeQualityRejection(stash, inputRef, promotion.knowledgeRef, resolvedPromotionContent, judgeResult.score, judgeResult.reason, { reviewNeeded: true }, options.eligibilitySource);
829
- }
830
- return writeQualityRejection(stash, inputRef, promotion.knowledgeRef, resolvedPromotionContent, judgeResult.score, judgeResult.reason, {}, options.eligibilitySource);
831
- }
832
- // Normalize 1-5 judge score to [0, 1]. Score of -1 means pass-through
833
- // (no LLM / timeout / parse failure) — leave confidence undefined so
834
- // the auto-accept gate treats the proposal as unscored and skips it.
835
- if (judgeResult.score > 0)
836
- knowledgeJudgeConfidence = judgeResult.score / 5;
837
- }
838
- const knowledgeParsed = parseFrontmatter(resolvedPromotionContent);
839
- const proposalResult = createProposal(stash, {
840
- ref: promotion.knowledgeRef,
841
- source: "distill",
842
- ...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
843
- payload: {
844
- content: resolvedPromotionContent,
845
- ...(Object.keys(knowledgeParsed.data).length > 0 ? { frontmatter: knowledgeParsed.data } : {}),
846
- },
847
- ...(knowledgeJudgeConfidence !== undefined ? { confidence: knowledgeJudgeConfidence } : {}),
848
- // Attribution tagging: persist the eligibility lane on the proposal.
849
- ...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
850
- }, options.ctx);
851
- if (isProposalSkipped(proposalResult)) {
852
- appendEvent({
853
- eventType: "distill_invoked",
854
- ref: inputRef,
855
- metadata: {
856
- outcome: "skipped",
857
- lessonRef: promotion.knowledgeRef,
858
- message: proposalResult.message,
859
- skipReason: proposalResult.reason,
860
- ...eligMeta,
861
- },
862
- });
863
- return {
864
- schemaVersion: 1,
865
- ok: true,
866
- outcome: "skipped",
867
- inputRef,
868
- lessonRef: promotion.knowledgeRef,
869
- message: proposalResult.message,
870
- };
871
- }
872
- const proposal = proposalResult;
873
- appendEvent({
874
- eventType: "distill_invoked",
875
- ref: inputRef,
876
- metadata: {
877
- outcome: "queued",
878
- lessonRef: promotion.knowledgeRef,
879
- proposalRef: promotion.knowledgeRef,
880
- proposalKind: "knowledge",
881
- proposalId: proposal.id,
882
- ...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
883
- ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
884
- ...eligMeta,
885
- },
886
- });
887
- return {
888
- schemaVersion: 1,
889
- ok: true,
890
- outcome: "queued",
891
- inputRef,
892
- lessonRef: promotion.knowledgeRef,
893
- proposalRef: promotion.knowledgeRef,
894
- proposalKind: "knowledge",
895
- proposalId: proposal.id,
896
- proposal,
897
- ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
898
- };
899
- }
593
+ // Memory→knowledge promotion branch (D-1/#369). When the target ref is a
594
+ // reinforced memory, distill graduates it into a knowledge proposal instead
595
+ // of a lesson — the whole branch (LLM contradiction-merge, quality gate,
596
+ // proposal creation, event emit) lives in `promoteMemoryToKnowledge` and is
597
+ // terminal when it fires. A `null` return means "not a promotion candidate";
598
+ // fall through to the ordinary lesson/knowledge distillation path.
599
+ const promotionResult = await promoteMemoryToKnowledge({
600
+ targetKind,
601
+ inputRef,
602
+ assetContent,
603
+ filteredEvents,
604
+ config,
605
+ chat,
606
+ stash,
607
+ lookup,
608
+ fetchSimilarLessonsFn,
609
+ existingRefVocabulary,
610
+ outcomeWeightEnabled,
611
+ eligMeta,
612
+ eligibilitySource: options.eligibilitySource,
613
+ sourceRun: options.sourceRun,
614
+ proposalsCtx: options.ctx,
615
+ exclusionSetSize: exclusionSet.size,
616
+ filteredFeedbackCount,
617
+ feedbackFullyFiltered,
618
+ });
619
+ if (promotionResult)
620
+ return promotionResult;
900
621
  const effectiveProposalKind = targetKind === "knowledge" ? "knowledge" : "lesson";
901
622
  const effectiveLessonRef = effectiveProposalKind === "knowledge" ? deriveKnowledgeRef(inputRef) : deriveLessonRef(inputRef);
902
623
  // Inject last 1–3 rejected proposals for this ref as Reflexion-style
@@ -909,13 +630,36 @@ export async function akmDistill(options) {
909
630
  reason: p.review?.reason ?? "no reason given",
910
631
  contentPreview: p.payload.content.slice(0, 500),
911
632
  }));
912
- const userPrompt = buildDistillPrompt({
633
+ // WS-3b CLS interleaving (step 9).
634
+ // When cls.enabled, inject embedding-retrieved adjacent lessons/knowledge
635
+ // into the distill prompt so the LLM avoids overwriting prior generalizations
636
+ // (catastrophic interference). DEFAULT OFF.
637
+ const clsConfig = getImproveProcessConfig(config, "distill", options.improveProfile)?.cls ?? {};
638
+ let clsContext = "";
639
+ if (clsConfig.enabled) {
640
+ try {
641
+ const adjacentCount = clsConfig.adjacentCount ?? 3;
642
+ // Use the asset content or input ref as the query for adjacent retrieval.
643
+ const clsQuery = assetContent ? assetContent.slice(0, 500) : inputRef;
644
+ const adjacentItems = await fetchSimilarLessonsFn(clsQuery, adjacentCount);
645
+ clsContext = buildClsContext(adjacentItems, clsConfig);
646
+ }
647
+ catch {
648
+ // Fail open — CLS is supplemental, never required.
649
+ }
650
+ }
651
+ // Distill output is a lesson/knowledge (non-wiki) → stash authoring
652
+ // standards. Resolved once for this single call.
653
+ const standardsContext = resolveStashStandards(stash);
654
+ const baseUserPrompt = buildDistillPrompt({
913
655
  inputRef,
914
656
  assetContent,
915
657
  feedback,
916
658
  proposalKind: effectiveProposalKind,
917
659
  ...(rejectedForRef.length > 0 ? { rejectedProposals: rejectedForRef } : {}),
660
+ ...(standardsContext.trim() ? { standardsContext } : {}),
918
661
  });
662
+ const userPrompt = clsContext ? `${baseUserPrompt}${clsContext}` : baseUserPrompt;
919
663
  const messages = [
920
664
  { role: "system", content: effectiveProposalKind === "knowledge" ? KNOWLEDGE_SYSTEM_PROMPT : LESSON_SYSTEM_PROMPT },
921
665
  { role: "user", content: userPrompt },
@@ -1027,136 +771,20 @@ export async function akmDistill(options) {
1027
771
  // Strip any stray fence the LLM might have added around the markdown.
1028
772
  content = stripMarkdownFences(raw);
1029
773
  }
1030
- // Auto-repair missing frontmatter fields before hard-failing. Small models
1031
- // frequently produce a good lesson body but omit the YAML header entirely.
1032
- // Rather than discarding valid content, we extract description/when_to_use
1033
- // from the body and prepend the required frontmatter block.
1034
- //
1035
- // IMPORTANT: We do NOT synthesise placeholder strings here. If the body
1036
- // does not contain text that passes the post-LLM validators
1037
- // (`isValidDescription` / `isValidWhenToUse`), we leave the field missing
1038
- // and let the lesson lint reject the proposal as `validation_failed`.
1039
- // Emitting placeholders like `"Lesson distilled from <ref>"` or
1040
- // `"When working with <slug>"` is what produced the systematic broken
1041
- // proposals observed across 323 archived rejections.
774
+ // Lesson-path content normalization (see distill/content-repair): auto-repair
775
+ // missing frontmatter, description↔when_to_use auto-swap, and truncation
776
+ // repair. Knowledge output skips all three (no lesson frontmatter contract).
1042
777
  if (effectiveProposalKind !== "knowledge") {
1043
- const parsed = parseFrontmatter(content);
1044
- const fm = (parsed.data ?? {});
1045
- const missingDesc = typeof fm.description !== "string" || !fm.description.trim();
1046
- const missingWtu = typeof fm.when_to_use !== "string" || !fm.when_to_use.trim();
1047
- if (missingDesc || missingWtu) {
1048
- const body = parsed.content.trim();
1049
- // Strip markdown formatting tokens from a line so extracted text is clean.
1050
- const stripMd = (l) => l
1051
- .replace(/\*\*([^*]+)\*\*/g, "$1")
1052
- .replace(/\*([^*]+)\*/g, "$1")
1053
- .replace(/`([^`]+)`/g, "$1")
1054
- .replace(/^[#*\->_]+\s*/, "")
1055
- .replace(/:\s*$/, "")
1056
- .trim();
1057
- // Skip lines that look like YAML field assignments (key: value) or frontmatter delimiters.
1058
- // These appear when the LLM leaks frontmatter content into the body, causing
1059
- // auto-repair to produce description: "description: Key Takeaways".
1060
- const isYamlLike = (l) => /^---/.test(l) || /^[a-z_]+:\s/i.test(l);
1061
- const bodyLines = body.split("\n").map(stripMd);
1062
- // Extract description: first body line that BOTH looks like prose AND
1063
- // passes isValidDescription. If nothing qualifies, leave the field
1064
- // missing — the lint pass will reject the proposal cleanly.
1065
- let descLine;
1066
- for (const l of bodyLines) {
1067
- if (isYamlLike(l))
1068
- continue;
1069
- if (l.length <= 10 || l.length >= 400)
1070
- continue;
1071
- if (isValidDescription(l, inputRef).ok) {
1072
- descLine = l;
1073
- break;
1074
- }
1075
- }
1076
- // Extract when_to_use: a line starting with "When" / "Use when" / "Apply when"
1077
- // that ALSO passes isValidWhenToUse (rejects circular fallbacks).
1078
- let wtuLine;
1079
- for (const l of bodyLines) {
1080
- if (!/^(when |use when|apply when)/i.test(l))
1081
- continue;
1082
- if (l.length >= 400)
1083
- continue;
1084
- if (isValidWhenToUse(l, inputRef).ok) {
1085
- wtuLine = l;
1086
- break;
1087
- }
1088
- }
1089
- const repairedFm = {
1090
- ...fm,
1091
- ...(missingDesc && descLine ? { description: descLine } : {}),
1092
- ...(missingWtu && wtuLine ? { when_to_use: wtuLine } : {}),
1093
- };
1094
- const fmLines = Object.entries(repairedFm)
1095
- .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
1096
- .join("\n");
1097
- // Only rewrite content if we actually have at least one field to write.
1098
- // Otherwise leave the original content for the lint pass to reject.
1099
- if (Object.keys(repairedFm).length > 0) {
1100
- content = assembleAssetFromString(fmLines, body);
1101
- }
1102
- }
778
+ content = autoRepairLessonFrontmatter(content, inputRef);
1103
779
  }
1104
- // Description ↔ when_to_use auto-swap normalization (recover ~93% of
1105
- // qwen-9b's `^when\b/i` rejections at zero LLM cost). When the LLM emits
1106
- // a conditional-framed description ("When X happens, do Y") and the
1107
- // when_to_use field looks like a declarative description (or is empty),
1108
- // the two fields are mis-fielded — exactly what `isValidDescription`'s
1109
- // error message says ("that pattern belongs in when_to_use"). We swap
1110
- // them and revalidate; the swap is committed only if BOTH fields pass
1111
- // their respective validators afterwards. If revalidation still fails,
1112
- // we fall through to the existing reject path.
1113
780
  let descriptionSwapped = 0;
1114
781
  if (effectiveProposalKind !== "knowledge") {
1115
- const parsedSwap = parseFrontmatter(content);
1116
- const fmSwap = (parsedSwap.data ?? {});
1117
- const descRaw = typeof fmSwap.description === "string" ? fmSwap.description.trim() : "";
1118
- const wtuRaw = typeof fmSwap.when_to_use === "string" ? fmSwap.when_to_use.trim() : "";
1119
- const descStartsConditional = /^(when|if)\b/i.test(descRaw);
1120
- const wtuStartsConditional = /^(when|if)\b/i.test(wtuRaw);
1121
- if (descStartsConditional && !wtuStartsConditional && wtuRaw.length > 0) {
1122
- // Try the swap and revalidate. The when_to_use validator requires the
1123
- // value not match `/^when working with\b/i` (the circular fallback) —
1124
- // a real description rarely does, so this usually passes.
1125
- const swappedDescCheck = isValidDescription(wtuRaw, inputRef);
1126
- const swappedWtuCheck = isValidWhenToUse(descRaw, inputRef);
1127
- if (swappedDescCheck.ok && swappedWtuCheck.ok) {
1128
- const swappedFm = {
1129
- ...fmSwap,
1130
- description: wtuRaw,
1131
- when_to_use: descRaw,
1132
- };
1133
- const swappedFmLines = Object.entries(swappedFm)
1134
- .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
1135
- .join("\n");
1136
- content = assembleAssetFromString(swappedFmLines, parsedSwap.content);
1137
- descriptionSwapped = 1;
1138
- }
1139
- }
782
+ const swapResult = autoSwapDescriptionWhenToUse(content, inputRef);
783
+ content = swapResult.content;
784
+ descriptionSwapped = swapResult.swapped;
1140
785
  }
1141
- // Post-generation truncation repair (#556): if the LLM sliced the
1142
- // description mid-sentence, deterministically complete it from its own text
1143
- // / the lesson body BEFORE the lint + quality validators run. No-op
1144
- // (byte-identical) for already-complete descriptions, so this never alters
1145
- // a valid proposal. Runs on the lesson path only (knowledge has no
1146
- // description field gate here).
1147
786
  if (effectiveProposalKind !== "knowledge") {
1148
- const parsedRepair = parseFrontmatter(content);
1149
- const fmRepair = (parsedRepair.data ?? {});
1150
- const descRepairRaw = typeof fmRepair.description === "string" ? fmRepair.description : "";
1151
- if (descRepairRaw) {
1152
- const repaired = repairTruncatedDescription(descRepairRaw, parsedRepair.content);
1153
- if (repaired !== descRepairRaw) {
1154
- const repairedFmLines = Object.entries({ ...fmRepair, description: repaired })
1155
- .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
1156
- .join("\n");
1157
- content = assembleAssetFromString(repairedFmLines, parsedRepair.content);
1158
- }
1159
- }
787
+ content = repairLessonDescriptionTruncation(content);
1160
788
  }
1161
789
  // Parse + lint the lesson before creating the proposal. The lint is the
1162
790
  // canonical gate for required frontmatter (v1 spec §13). On failure we
@@ -1165,49 +793,10 @@ export async function akmDistill(options) {
1165
793
  const findings = effectiveProposalKind === "knowledge"
1166
794
  ? validateKnowledgeContent(content, inputRef)
1167
795
  : lintLessonContent(content, `distill:${inputRef}`).findings;
1168
- // Additional quality validators run only on lessons. lesson-lint checks
1169
- // "field is present and non-empty"; these reject the systematic failure
1170
- // modes observed across 323 archived rejected proposals:
1171
- // - description is a body fragment, section heading, or placeholder
1172
- // - when_to_use is the circular "When working with <ref>" fallback
1173
- // - description == when_to_use (LLM duplicated a single sentence)
1174
- // - body contains a second pseudo-frontmatter block
796
+ // Additional lesson-only quality validators reject the systematic failure
797
+ // modes seen across 323 archived rejected proposals (see distill/content-repair).
1175
798
  if (effectiveProposalKind !== "knowledge" && findings.length === 0) {
1176
- const parsedQC = parseFrontmatter(content);
1177
- const fmQC = (parsedQC.data ?? {});
1178
- const descCheck = isValidDescription(fmQC.description, inputRef);
1179
- if (!descCheck.ok) {
1180
- findings.push({
1181
- kind: "invalid-description",
1182
- field: "description",
1183
- message: `Distilled lesson for ${inputRef} has an invalid description: ${descCheck.reason}.`,
1184
- });
1185
- }
1186
- const wtuCheck = isValidWhenToUse(fmQC.when_to_use, inputRef);
1187
- if (!wtuCheck.ok) {
1188
- findings.push({
1189
- kind: "invalid-when_to_use",
1190
- field: "when_to_use",
1191
- message: `Distilled lesson for ${inputRef} has an invalid when_to_use: ${wtuCheck.reason}.`,
1192
- });
1193
- }
1194
- // description and when_to_use must say different things.
1195
- if (descCheck.ok &&
1196
- wtuCheck.ok &&
1197
- typeof fmQC.description === "string" &&
1198
- typeof fmQC.when_to_use === "string" &&
1199
- fmQC.description.trim().toLowerCase() === fmQC.when_to_use.trim().toLowerCase()) {
1200
- findings.push({
1201
- kind: "description-equals-when_to_use",
1202
- field: "description",
1203
- message: `Distilled lesson for ${inputRef} has identical description and when_to_use.`,
1204
- });
1205
- }
1206
- // Double-frontmatter / pseudo-frontmatter pollution in the body.
1207
- const dfm = detectDoubleFrontmatter(content);
1208
- if (dfm) {
1209
- findings.push({ kind: dfm.kind, field: "body", message: `Distilled lesson for ${inputRef}: ${dfm.message}` });
1210
- }
799
+ findings.push(...collectLessonQualityFindings(content, inputRef));
1211
800
  }
1212
801
  if (findings.length > 0) {
1213
802
  appendEvent({
@@ -1228,7 +817,8 @@ export async function akmDistill(options) {
1228
817
  : "Lessons require non-empty `description` and `when_to_use` frontmatter fields. See v1 spec §13.");
1229
818
  }
1230
819
  // LLM-as-judge quality gate (P2-B). Only active when the feature flag is
1231
- // explicitly enabled. Fail-open: judge failures always pass through.
820
+ // explicitly enabled. Fail-CLOSED (07 P0-2): an unjudgeable proposal (no LLM
821
+ // / timeout / parse failure) is rejected, not passed through.
1232
822
  // D-5 / #388: Three-band system — review_needed band queues a proposal
1233
823
  // with review_needed outcome rather than auto-rejecting.
1234
824
  let lessonJudgeConfidence;
@@ -1245,12 +835,38 @@ export async function akmDistill(options) {
1245
835
  }
1246
836
  return writeQualityRejection(stash, inputRef, effectiveLessonRef, content, judgeResult.score, judgeResult.reason, exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}, options.eligibilitySource);
1247
837
  }
1248
- // Normalize 1-5 judge score to [0, 1]. Score of -1 means pass-through
1249
- // (no LLM / timeout / parse failure) leave confidence undefined so
1250
- // the auto-accept gate treats the proposal as unscored and skips it.
838
+ // Normalize 1-5 judge score to [0, 1]. Only a real passing verdict
839
+ // reaches here (07 P0-2: the judge now fails CLOSED on no-LLM / timeout /
840
+ // parse failure, so those return pass:false and never fall through to
841
+ // this line). A defensive score>0 guard keeps confidence undefined for any
842
+ // non-positive score the auto-accept gate should treat as unscored.
1251
843
  if (judgeResult.score > 0)
1252
844
  lessonJudgeConfidence = judgeResult.score / 5;
1253
845
  }
846
+ // WS-3b: Distill→source fidelity check (step 10).
847
+ // When fidelityCheck.enabled, check the distill proposal against its cited
848
+ // source memories. A contradiction flag routes to human review (not auto-accept).
849
+ // DEFAULT OFF. Fail-open: any error is treated as no-contradiction.
850
+ const fidelityConfig = getImproveProcessConfig(config, "distill", options.improveProfile)?.fidelityCheck ?? {};
851
+ if (fidelityConfig.enabled && assetContent) {
852
+ try {
853
+ const proposalBody = stripBodyForFidelity(content);
854
+ const sourceBodies = [stripBodyForFidelity(assetContent)];
855
+ const fidelityResult = checkDistillFidelity(proposalBody, sourceBodies, fidelityConfig);
856
+ if (fidelityResult.contradictionDetected) {
857
+ // Route to human review by writing a quality rejection with reviewNeeded=true.
858
+ return writeQualityRejection(stash, inputRef, effectiveLessonRef, content, 2.0, // below auto-accept threshold, signals review needed
859
+ fidelityResult.reason ?? "Proposal may contradict cited source memories.", {
860
+ reviewNeeded: true,
861
+ fidelityContradiction: true,
862
+ ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
863
+ }, options.eligibilitySource);
864
+ }
865
+ }
866
+ catch {
867
+ // Fail open — fidelity check is supplemental.
868
+ }
869
+ }
1254
870
  // Round-trip the parsed frontmatter so the proposal carries it as a
1255
871
  // structured payload alongside the raw content (matches the shape used by
1256
872
  // other proposal sources).
@@ -1298,6 +914,10 @@ export async function akmDistill(options) {
1298
914
  };
1299
915
  }
1300
916
  const proposal2 = proposalResult2;
917
+ // G4: content-score the distilled OUTPUT so it carries a real encoding
918
+ // salience (encoding_source='content') from creation — lessons never get
919
+ // another chance (they are refused as distill inputs).
920
+ persistOutputEncodingSalience(effectiveLessonRef, content, existingRefVocabulary, outcomeWeightEnabled);
1301
921
  appendEvent({
1302
922
  eventType: "distill_invoked",
1303
923
  ref: inputRef,
@@ -1307,6 +927,9 @@ export async function akmDistill(options) {
1307
927
  proposalRef: effectiveLessonRef,
1308
928
  proposalKind: effectiveProposalKind,
1309
929
  proposalId: proposal2.id,
930
+ // R3: judge verdicts are longitudinally queryable, not just a one-shot
931
+ // proposal.confidence write (normalized 1–5 score / 5).
932
+ ...(lessonJudgeConfidence !== undefined ? { judgeConfidence: lessonJudgeConfidence } : {}),
1310
933
  ...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
1311
934
  ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
1312
935
  ...(descriptionSwapped > 0 ? { descriptionSwapped } : {}),