akm-cli 0.9.0-beta.6 → 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 (326) hide show
  1. package/CHANGELOG.md +663 -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 -14
  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 +153 -147
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +76 -72
  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 -1083
  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 +1313 -1278
  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 +243 -599
  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 +55 -4
  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 +160 -7
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +32 -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 +510 -2537
  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 +115 -0
  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 +212 -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 +7 -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 -882
  138. package/dist/commands/read/curate.js +419 -103
  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 +29 -41
  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 +389 -58
  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 +21 -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 +142 -1091
  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 +356 -780
  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 +103 -24
  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 +106 -0
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +291 -310
  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 +11 -1
  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 +57 -15
  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 +2718 -2354
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +891 -597
  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/tar-utils.js +16 -8
  286. package/dist/sources/providers/website.js +9 -5
  287. package/dist/sources/website-ingest.js +187 -29
  288. package/dist/sources/wiki-fetchers/registry.js +53 -0
  289. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  290. package/dist/storage/database.js +45 -10
  291. package/dist/storage/managed-db.js +82 -0
  292. package/dist/storage/repositories/canaries-repository.js +107 -0
  293. package/dist/storage/repositories/consolidation-repository.js +38 -0
  294. package/dist/storage/repositories/embeddings-repository.js +72 -0
  295. package/dist/storage/repositories/events-repository.js +187 -0
  296. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  297. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  298. package/dist/storage/repositories/index-db.js +14 -8
  299. package/dist/storage/repositories/proposals-repository.js +220 -0
  300. package/dist/storage/repositories/recombine-repository.js +213 -0
  301. package/dist/storage/repositories/registry-cache.js +93 -0
  302. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  303. package/dist/storage/repositories/task-history-repository.js +93 -0
  304. package/dist/storage/sqlite-pragmas.js +146 -0
  305. package/dist/tasks/backends/cron.js +1 -1
  306. package/dist/tasks/backends/index.js +9 -0
  307. package/dist/tasks/backends/launchd.js +1 -1
  308. package/dist/tasks/backends/schtasks.js +1 -1
  309. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  310. package/dist/tasks/runner.js +15 -13
  311. package/dist/text-import-hook.mjs +0 -0
  312. package/dist/wiki/wiki.js +52 -11
  313. package/dist/workflows/cli.js +1 -0
  314. package/dist/workflows/db.js +3 -4
  315. package/dist/workflows/runtime/runs.js +43 -118
  316. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  317. package/dist/workflows/validate-summary.js +2 -7
  318. package/docs/README.md +69 -18
  319. package/docs/data-and-telemetry.md +5 -4
  320. package/docs/migration/release-notes/0.7.0.md +1 -1
  321. package/docs/migration/release-notes/0.9.0.md +39 -0
  322. package/package.json +10 -10
  323. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  324. package/dist/commands/db-cli.js +0 -23
  325. package/dist/indexer/db/db-backup.js +0 -376
  326. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -27,9 +27,7 @@
27
27
  * - Zep / Graphiti (arXiv:2501.13956 §3) — unified belief-revision pipeline
28
28
  * - MemOS (arXiv:2507.03724) — formal archive/merge/transition with shared state model
29
29
  */
30
- import fs from "node:fs";
31
- import { assembleAsset } from "../../../core/asset/asset-serialize.js";
32
- import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
30
+ import { mutateFrontmatter } from "../../../core/asset/frontmatter.js";
33
31
  // ── Contradiction edge writer ─────────────────────────────────────────────────
34
32
  /**
35
33
  * Write `contradictedBy` and `beliefState: contradicted` edges to a memory
@@ -47,16 +45,17 @@ import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
47
45
  * @param contradictedByRef - The ref that contradicts this memory.
48
46
  */
49
47
  export function writeContradictEdge(filePath, contradictedByRef) {
50
- const raw = fs.readFileSync(filePath, "utf8");
51
- const parsed = parseFrontmatter(raw);
52
- const existing = Array.isArray(parsed.data.contradictedBy) ? parsed.data.contradictedBy : [];
53
- if (existing.includes(contradictedByRef))
54
- return; // Already written — idempotent.
55
- const nextContradictedBy = [...new Set([...existing, contradictedByRef])].sort();
56
- const nextFrontmatter = {
57
- ...parsed.data,
58
- contradictedBy: nextContradictedBy,
59
- beliefState: "contradicted",
60
- };
61
- fs.writeFileSync(filePath, assembleAsset(nextFrontmatter, parsed.content), "utf8");
48
+ mutateFrontmatter(filePath, (parsed) => {
49
+ const existing = Array.isArray(parsed.data.contradictedBy)
50
+ ? parsed.data.contradictedBy
51
+ : [];
52
+ if (existing.includes(contradictedByRef))
53
+ return null; // Already written — idempotent.
54
+ const nextContradictedBy = [...new Set([...existing, contradictedByRef])].sort();
55
+ return {
56
+ ...parsed.data,
57
+ contradictedBy: nextContradictedBy,
58
+ beliefState: "contradicted",
59
+ };
60
+ });
62
61
  }
@@ -35,8 +35,8 @@
35
35
  */
36
36
  import fs from "node:fs";
37
37
  import path from "node:path";
38
- import { assembleAsset } from "../../../core/asset/asset-serialize.js";
39
- import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
38
+ import contradictionJudgeTemplate from "../../../assets/prompts/contradiction-judge.md" with { type: "text" };
39
+ import { mutateFrontmatter, parseFrontmatter } from "../../../core/asset/frontmatter.js";
40
40
  import { getDefaultLlmConfig } from "../../../core/config/config.js";
41
41
  import { chatCompletion, parseEmbeddedJsonResponse } from "../../../llm/client.js";
42
42
  import { tryLlmFeature } from "../../../llm/feature-gate.js";
@@ -51,6 +51,13 @@ const MAX_FAMILY_SIZE = 8;
51
51
  * families. Prevents runaway LLM usage on stashes with many memories.
52
52
  */
53
53
  const MAX_PAIRS_PER_RUN = 20;
54
+ /**
55
+ * Minimum confidence required to write a contradiction edge. Below this
56
+ * threshold the LLM may be flagging topic-overlap rather than genuine logical
57
+ * exclusivity (investigation 2026-06-18). Absent confidence fields default to
58
+ * 1.0 for backward compatibility with older judge responses.
59
+ */
60
+ const CONTRADICT_CONFIDENCE_THRESHOLD = 0.92;
54
61
  /**
55
62
  * Truncation limit for memory body content sent to the LLM judge.
56
63
  * Keeps prompts compact while preserving the key factual claims.
@@ -58,34 +65,13 @@ const MAX_PAIRS_PER_RUN = 20;
58
65
  const BODY_TRUNCATION = 800;
59
66
  // ── Prompt builder ────────────────────────────────────────────────────────────
60
67
  function buildContradictionJudgePrompt(a, b) {
61
- return [
62
- "You are evaluating two derived memory entries to determine if they contain",
63
- "directly contradictory factual claims about the same subject.",
64
- "",
65
- "Memory A:",
66
- `Ref: ${a.ref}`,
67
- `Description: ${a.description || "(none)"}`,
68
- "Content:",
69
- "```",
70
- a.body.slice(0, BODY_TRUNCATION),
71
- "```",
72
- "",
73
- "Memory B:",
74
- `Ref: ${b.ref}`,
75
- `Description: ${b.description || "(none)"}`,
76
- "Content:",
77
- "```",
78
- b.body.slice(0, BODY_TRUNCATION),
79
- "```",
80
- "",
81
- "Answer ONLY with valid JSON — no prose, no code fences:",
82
- '{"contradicts": true|false, "reason": "<one sentence explaining why or why not>"}',
83
- "",
84
- "A contradiction means the memories make mutually exclusive factual claims about the",
85
- "same topic (e.g. Memory A says 'always use VPN' while Memory B says 'VPN is optional').",
86
- "If the memories are complementary, about different topics, or one supersedes the other",
87
- "without direct conflict, return false.",
88
- ].join("\n");
68
+ return contradictionJudgeTemplate
69
+ .replace("{{A_REF}}", a.ref)
70
+ .replace("{{A_DESCRIPTION}}", a.description || "(none)")
71
+ .replace("{{A_BODY}}", a.body.slice(0, BODY_TRUNCATION))
72
+ .replace("{{B_REF}}", b.ref)
73
+ .replace("{{B_DESCRIPTION}}", b.description || "(none)")
74
+ .replace("{{B_BODY}}", b.body.slice(0, BODY_TRUNCATION));
89
75
  }
90
76
  // ── Filesystem helpers ────────────────────────────────────────────────────────
91
77
  function* walkMarkdownFilesLocal(root) {
@@ -139,19 +125,42 @@ function resolveParentRef(filePath, frontmatter, memoriesRootDir) {
139
125
  */
140
126
  /** Returns true if the edge was newly written, false if it already existed. */
141
127
  function writeContradictedByEdge(filePath, contradictedByRef) {
142
- const raw = fs.readFileSync(filePath, "utf8");
143
- const parsed = parseFrontmatter(raw);
144
- const existing = Array.isArray(parsed.data.contradictedBy) ? parsed.data.contradictedBy : [];
145
- if (existing.includes(contradictedByRef))
146
- return false; // Edge already written.
147
- const updatedContradictedBy = [...new Set([...existing, contradictedByRef])].sort();
148
- const nextFrontmatter = {
149
- ...parsed.data,
150
- contradictedBy: updatedContradictedBy,
151
- beliefState: "contradicted",
152
- };
153
- fs.writeFileSync(filePath, assembleAsset(nextFrontmatter, parsed.content), "utf8");
154
- return true;
128
+ return mutateFrontmatter(filePath, (parsed) => {
129
+ const existing = Array.isArray(parsed.data.contradictedBy)
130
+ ? parsed.data.contradictedBy
131
+ : [];
132
+ if (existing.includes(contradictedByRef))
133
+ return null; // Edge already written.
134
+ const updatedContradictedBy = [...new Set([...existing, contradictedByRef])].sort();
135
+ return {
136
+ ...parsed.data,
137
+ contradictedBy: updatedContradictedBy,
138
+ beliefState: "contradicted",
139
+ };
140
+ });
141
+ }
142
+ /**
143
+ * Deterministically pick, for a confirmed-contradiction pair, the LOSER memory
144
+ * that receives the single directed `contradictedBy` edge (SCC-resolved to
145
+ * `contradicted`) and the WINNER ref that survives as the current belief.
146
+ *
147
+ * A SINGLE directed edge is essential. Writing mutual A↔B edges forms a 2-cycle
148
+ * that {@link resolveFamilyContradictions} collapses into one strongly-connected
149
+ * SINK component and refreshes BOTH members back to active — erasing the
150
+ * contradiction on every run (the self-erasing bug this fix removes).
151
+ *
152
+ * Direction = lexicographic ref order: the ref that sorts LATER is the loser.
153
+ * This is a **total order** over the family's (distinct) refs, so the induced
154
+ * edges are always acyclic — a family of any size resolves to a DAG with a
155
+ * single sink, never a cycle that the resolver would refresh back to active.
156
+ * It is also immutable across runs (unlike file mtime, which the resolver
157
+ * bumps when it rewrites loser files), so detection is idempotent. Ref order
158
+ * carries no recency meaning — no derived-memory writer sets a `createdAt`/
159
+ * timestamp today — but the mechanism only needs a stable, acyclic direction;
160
+ * eliminating worst-case self-erasure, not ranking by recency, is the goal.
161
+ */
162
+ function pickContradictionLoser(a, b) {
163
+ return a.ref < b.ref ? { loser: b, winnerRef: a.ref } : { loser: a, winnerRef: b.ref };
155
164
  }
156
165
  // ── Main entry point ──────────────────────────────────────────────────────────
157
166
  /**
@@ -222,18 +231,22 @@ export async function detectAndWriteContradictions(stashDir, config, chat = chat
222
231
  const b = family[j];
223
232
  if (!a || !b)
224
233
  continue;
225
- // Skip pairs where edges already exist in BOTH directions (no new information).
226
- const aRaw = fs.readFileSync(a.filePath, "utf8");
227
- const aParsed = parseFrontmatter(aRaw);
228
- const aCB = Array.isArray(aParsed.data.contradictedBy)
229
- ? aParsed.data.contradictedBy
230
- : [];
231
- const bRaw = fs.readFileSync(b.filePath, "utf8");
232
- const bParsed = parseFrontmatter(bRaw);
233
- const bCB = Array.isArray(bParsed.data.contradictedBy)
234
- ? bParsed.data.contradictedBy
235
- : [];
236
- if (aCB.includes(b.ref) && bCB.includes(a.ref))
234
+ // Resolve the directed edge up front (independent of the judge it is
235
+ // decided by lexicographic ref order). Skip when that single loser→winner
236
+ // edge already exists (no new information; avoids re-judging resolved
237
+ // pairs across runs).
238
+ //
239
+ // Legacy mutual A↔B edges written by the pre-fix pass self-heal: the
240
+ // skip fires this run, but the SCC resolver treats the 2-cycle as a sink
241
+ // and refreshes both to active — DELETING both `contradictedBy` arrays
242
+ // (memory-improve.ts persistBeliefStateTransition). The next detection
243
+ // run then sees no edge, re-judges, and writes the single canonical edge.
244
+ const aParsed = parseFrontmatter(fs.readFileSync(a.filePath, "utf8"));
245
+ const bParsed = parseFrontmatter(fs.readFileSync(b.filePath, "utf8"));
246
+ const { loser, winnerRef } = pickContradictionLoser(a, b);
247
+ const loserData = loser === a ? aParsed.data : bParsed.data;
248
+ const loserCB = Array.isArray(loserData.contradictedBy) ? loserData.contradictedBy : [];
249
+ if (loserCB.includes(winnerRef))
237
250
  continue;
238
251
  const prompt = buildContradictionJudgePrompt(a, b);
239
252
  const judgeResult = await tryLlmFeature("memory_contradiction_detection", config, async () => {
@@ -256,14 +269,24 @@ export async function detectAndWriteContradictions(stashDir, config, chat = chat
256
269
  }
257
270
  if (!parsed?.contradicts)
258
271
  continue;
259
- // Write contradiction edges: both members get contradictedBy pointing to each other.
272
+ // Confidence gate: absent field defaults to 1.0 (backward compat with
273
+ // pre-confidence responses). Do NOT default to 0 — that would silently
274
+ // disable all detection during the rollout period.
275
+ const confidence = typeof parsed.confidence === "number" ? parsed.confidence : 1.0;
276
+ if (confidence < CONTRADICT_CONFIDENCE_THRESHOLD) {
277
+ result.warnings.push(`Pair ${a.ref} / ${b.ref}: confidence ${confidence.toFixed(2)} below ${CONTRADICT_CONFIDENCE_THRESHOLD} threshold — skipped.`);
278
+ continue;
279
+ }
280
+ // Write a SINGLE directed contradiction edge: the losing (older) memory
281
+ // gets `contradictedBy` pointing to the winner. A mutual A↔B pair forms
282
+ // a 2-cycle that the SCC resolver refreshes back to active, erasing the
283
+ // contradiction every run (see pickContradictionLoser).
260
284
  try {
261
- const wroteA = writeContradictedByEdge(a.filePath, b.ref);
262
- const wroteB = writeContradictedByEdge(b.filePath, a.ref);
263
- result.edgesWritten += (wroteA ? 1 : 0) + (wroteB ? 1 : 0);
285
+ const wrote = writeContradictedByEdge(loser.filePath, winnerRef);
286
+ result.edgesWritten += wrote ? 1 : 0;
264
287
  }
265
288
  catch (err) {
266
- result.warnings.push(`Failed to write contradiction edge ${a.ref} <-> ${b.ref}: ${err instanceof Error ? err.message : String(err)}`);
289
+ result.warnings.push(`Failed to write contradiction edge ${loser.ref} -> ${winnerRef}: ${err instanceof Error ? err.message : String(err)}`);
267
290
  }
268
291
  }
269
292
  if (totalPairsChecked >= MAX_PAIRS_PER_RUN)
@@ -5,8 +5,8 @@ import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { makeAssetRef, parseAssetRef } from "../../../core/asset/asset-ref.js";
7
7
  import { assembleAsset } from "../../../core/asset/asset-serialize.js";
8
- import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
9
- import { firstString, groupBy, stringArray } from "../../../core/common.js";
8
+ import { mutateFrontmatter, parseFrontmatter } from "../../../core/asset/frontmatter.js";
9
+ import { asNonEmptyString, groupBy, stringArray } from "../../../core/common.js";
10
10
  const DERIVED_SUFFIX = ".derived";
11
11
  export function analyzeMemoryCleanup(stashDir, options = {}) {
12
12
  const records = collectDerivedMemories(stashDir, options.parentRef);
@@ -491,27 +491,27 @@ function archiveCleanupCandidate(stashDir, candidate, filePath) {
491
491
  };
492
492
  }
493
493
  function persistBeliefStateTransition(filePath, transition) {
494
- const raw = fs.readFileSync(filePath, "utf8");
495
- const parsed = parseFrontmatter(raw);
496
- const nextFrontmatter = {
497
- ...parsed.data,
498
- beliefState: transition.toState,
499
- };
500
- const currentBeliefRefs = [...new Set(transition.currentBeliefRefs ?? [])].sort();
501
- if (transition.toState === "contradicted") {
502
- nextFrontmatter.contradictedBy = [...currentBeliefRefs];
503
- }
504
- else {
505
- delete nextFrontmatter.contradictedBy;
506
- if (parsed.data.supersededBy !== undefined && refArray(parsed.data.supersededBy).length === 0) {
507
- delete nextFrontmatter.supersededBy;
494
+ mutateFrontmatter(filePath, (parsed) => {
495
+ const nextFrontmatter = {
496
+ ...parsed.data,
497
+ beliefState: transition.toState,
498
+ };
499
+ const currentBeliefRefs = [...new Set(transition.currentBeliefRefs ?? [])].sort();
500
+ if (transition.toState === "contradicted") {
501
+ nextFrontmatter.contradictedBy = [...currentBeliefRefs];
508
502
  }
509
- }
510
- if (currentBeliefRefs.length > 0)
511
- nextFrontmatter.currentBeliefRefs = [...currentBeliefRefs];
512
- else
513
- delete nextFrontmatter.currentBeliefRefs;
514
- fs.writeFileSync(filePath, assembleAsset(nextFrontmatter, parsed.content), "utf8");
503
+ else {
504
+ delete nextFrontmatter.contradictedBy;
505
+ if (parsed.data.supersededBy !== undefined && refArray(parsed.data.supersededBy).length === 0) {
506
+ delete nextFrontmatter.supersededBy;
507
+ }
508
+ }
509
+ if (currentBeliefRefs.length > 0)
510
+ nextFrontmatter.currentBeliefRefs = [...currentBeliefRefs];
511
+ else
512
+ delete nextFrontmatter.currentBeliefRefs;
513
+ return nextFrontmatter;
514
+ });
515
515
  }
516
516
  function appendBeliefStateTransitionLog(stashDir, transitions) {
517
517
  const logDir = path.join(stashDir, ".akm", "memory-cleanup");
@@ -580,8 +580,8 @@ function collectDerivedMemories(stashDir, parentRefFilter) {
580
580
  continue;
581
581
  if (!isDerivedMemory(name, parsed.data))
582
582
  continue;
583
- const title = firstString(parsed.data.title) ?? extractHeading(parsed.content) ?? "";
584
- const description = firstString(parsed.data.description) ?? "";
583
+ const title = asNonEmptyString(parsed.data.title) ?? extractHeading(parsed.content) ?? "";
584
+ const description = asNonEmptyString(parsed.data.description) ?? "";
585
585
  const tags = stringArray(parsed.data.tags);
586
586
  const searchHints = stringArray(parsed.data.searchHints);
587
587
  const body = parsed.content.trim();
@@ -637,7 +637,7 @@ function isFrozenHistoricalBeliefState(state) {
637
637
  return state === "deprecated";
638
638
  }
639
639
  function resolveBeliefState(frontmatter) {
640
- const explicit = firstString(frontmatter.beliefState);
640
+ const explicit = asNonEmptyString(frontmatter.beliefState);
641
641
  if (explicit === "active" ||
642
642
  explicit === "asserted" ||
643
643
  explicit === "deprecated" ||
@@ -651,10 +651,10 @@ function isDerivedMemory(name, frontmatter) {
651
651
  return frontmatter.inferred === true || name.endsWith(DERIVED_SUFFIX);
652
652
  }
653
653
  function resolveParentRef(name, frontmatter) {
654
- const fromSource = parseMemoryRef(firstString(frontmatter.source));
654
+ const fromSource = parseMemoryRef(asNonEmptyString(frontmatter.source));
655
655
  if (fromSource)
656
656
  return fromSource;
657
- const derivedFrom = firstString(frontmatter.derivedFrom);
657
+ const derivedFrom = asNonEmptyString(frontmatter.derivedFrom);
658
658
  if (derivedFrom)
659
659
  return makeAssetRef("memory", derivedFrom);
660
660
  if (name.endsWith(DERIVED_SUFFIX)) {
@@ -0,0 +1,270 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ // ── Constants ─────────────────────────────────────────────────────────────────
5
+ /**
6
+ * EMA decay factor for the expected-retrieval rolling mean (α).
7
+ * New expected = α × new_count + (1−α) × old_expected.
8
+ * At α = 0.3 the window is ≈ 3 cycles.
9
+ */
10
+ export const OUTCOME_EMA_ALPHA = 0.3;
11
+ /**
12
+ * Maximum K improve cycles for the eligibility-trace window. Retrievals older
13
+ * than K cycles contribute via the EMA decay naturally (they are already baked
14
+ * into `expected_retrieval_rate`).
15
+ */
16
+ export const OUTCOME_TRACE_CYCLES = 5;
17
+ /**
18
+ * Warm-start cap: the maximum `outcome_score` a brand-new row can be seeded with
19
+ * (from the utility EMA). Prevents a `[0,1]`-range utility value from generating
20
+ * a score that the first negative delta could catastrophically invert. Clipped to
21
+ * the maximum plausible first differential update: `max(0, 1 − 0) = 1`, so cap
22
+ * at a modest 0.3 to avoid spurious rank-flip on first cycle.
23
+ */
24
+ export const WARM_START_CAP = 0.3;
25
+ /**
26
+ * Penalty cap: the minimum outcome_score is capped at this value so a single
27
+ * very-negative run can't send the score to −∞.
28
+ */
29
+ export const OUTCOME_SCORE_MIN = -1.0;
30
+ /**
31
+ * Saturation ceiling: the maximum outcome_score. Biological RPE saturates —
32
+ * a fully predicted reward produces zero response, not an ever-growing one —
33
+ * so a long-lived popular asset must not accrue unbounded outcome mass that
34
+ * would dominate ranking once the outcome weight is enabled (analysis G2).
35
+ * 1.5 comfortably exceeds the max plausible single-cycle raw update while
36
+ * keeping the normalised outcomeSalience spread meaningful.
37
+ */
38
+ export const OUTCOME_SCORE_MAX = 1.5;
39
+ /**
40
+ * Diversity floor: `outcomeSalience` for any asset is at least this fraction
41
+ * of the maximum observed `outcome_score` in the table, so rare-but-correct
42
+ * assets cannot be permanently outcompeted. 0 = disabled (pure competition).
43
+ */
44
+ export const DIVERSITY_FLOOR_FRACTION = 0.1;
45
+ /**
46
+ * Default review_pressure increment per negative-feedback retrieval cycle.
47
+ * Each cycle where negative_feedback_count > 0, pressure increases by this
48
+ * much (and decreases by REVIEW_PRESSURE_DECAY if no new negatives).
49
+ */
50
+ export const REVIEW_PRESSURE_INCREMENT = 1;
51
+ /**
52
+ * Decay per non-negative cycle. Keeps review_pressure from accumulating forever
53
+ * when an asset's quality problems are addressed and feedback turns positive.
54
+ */
55
+ export const REVIEW_PRESSURE_DECAY = 1;
56
+ /**
57
+ * Upsert one asset's outcome row.
58
+ *
59
+ * On first call for a ref (no prior row): warm-starts from `utilityScore`.
60
+ * On subsequent calls: applies the differential update formula.
61
+ *
62
+ * Returns the resulting `outcome_score` and `review_pressure` so the caller
63
+ * can pass them to `computeSalience` without a second read.
64
+ */
65
+ export function updateAssetOutcome(db, inputs) {
66
+ const now = inputs.now ?? Date.now();
67
+ const valence = inputs.valence ?? 0;
68
+ // Fetch existing row (or undefined for first insert).
69
+ const existing = getAssetOutcome(db, inputs.ref);
70
+ const isNewRow = existing === undefined;
71
+ let outcomeScore;
72
+ let reviewPressure;
73
+ let expectedRetrievalRate;
74
+ if (isNewRow) {
75
+ // ── Warm-start ─────────────────────────────────────────────────────────
76
+ // Seed from utility EMA, clipped to [0, WARM_START_CAP].
77
+ // The warm-start is a non-negative seed; the first real differential update
78
+ // will adjust it up or down from there.
79
+ const seedScore = Math.min(WARM_START_CAP, Math.max(0, inputs.utilityScore ?? 0));
80
+ outcomeScore = seedScore;
81
+ reviewPressure = 0;
82
+ // Seed expected_retrieval_rate = 0 (no delta history yet).
83
+ // Seeding with currentRetrievalCount would produce a large spurious negative
84
+ // prediction error on the first real cycle (delta ≪ cumulative count).
85
+ expectedRetrievalRate = 0;
86
+ }
87
+ else {
88
+ // ── Differential update ────────────────────────────────────────────────
89
+ //
90
+ // retrieval_delta = current − stored (non-negative — we never go backwards)
91
+ const retrievalDelta = Math.max(0, inputs.currentRetrievalCount - existing.retrieval_count);
92
+ // Differential prediction-error term:
93
+ // outcome = (retrieval_delta − expected_delta) + valence
94
+ //
95
+ // Prediction error is computed against the PRIOR stored EMA (before folding
96
+ // in this cycle's observation), so the current delta cannot leak into its own
97
+ // expectation. Negative values are intentional — they signal below-average cycles.
98
+ const expectedDelta = existing.expected_retrieval_rate;
99
+ const predictionError = retrievalDelta - expectedDelta;
100
+ // Advance the EMA over the OBSERVED delta (not the cumulative count).
101
+ // expected' = α × delta + (1−α) × prior_expected
102
+ expectedRetrievalRate =
103
+ OUTCOME_EMA_ALPHA * retrievalDelta + (1 - OUTCOME_EMA_ALPHA) * existing.expected_retrieval_rate;
104
+ // Running sum (EMA approach): new score = α × update + (1−α) × old
105
+ // so the score tracks the moving signal, not the cumulative sum.
106
+ const rawUpdate = predictionError + valence;
107
+ const newScore = OUTCOME_EMA_ALPHA * rawUpdate + (1 - OUTCOME_EMA_ALPHA) * existing.outcome_score;
108
+ // Clip to [OUTCOME_SCORE_MIN, OUTCOME_SCORE_MAX] — the ceiling is the RPE
109
+ // saturation analog (G2): without it, long-lived popular assets accumulate
110
+ // unbounded positive mass (live max was 3.13) and would dominate rank_score
111
+ // the moment the outcome weight is enabled. Stored legacy scores above the
112
+ // ceiling converge back under it on their next differential update.
113
+ outcomeScore = Math.min(OUTCOME_SCORE_MAX, Math.max(OUTCOME_SCORE_MIN, newScore));
114
+ // ── review_pressure (#613) ─────────────────────────────────────────────
115
+ // New negatives this cycle.
116
+ const newNegatives = Math.max(0, inputs.negativeFeedbackCount - existing.negative_feedback_count);
117
+ if (newNegatives > 0) {
118
+ reviewPressure = Math.min(10, existing.review_pressure + REVIEW_PRESSURE_INCREMENT * newNegatives);
119
+ }
120
+ else {
121
+ // Decay when no new negatives.
122
+ reviewPressure = Math.max(0, existing.review_pressure - REVIEW_PRESSURE_DECAY);
123
+ }
124
+ }
125
+ // Upsert the row.
126
+ db.prepare(`INSERT INTO asset_outcome
127
+ (asset_ref, last_retrieved_at, retrieval_count, expected_retrieval_rate,
128
+ negative_feedback_count, accepted_change_count, review_pressure,
129
+ outcome_score, updated_at)
130
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
131
+ ON CONFLICT(asset_ref) DO UPDATE SET
132
+ last_retrieved_at = excluded.last_retrieved_at,
133
+ retrieval_count = excluded.retrieval_count,
134
+ expected_retrieval_rate= excluded.expected_retrieval_rate,
135
+ negative_feedback_count= excluded.negative_feedback_count,
136
+ accepted_change_count = excluded.accepted_change_count,
137
+ review_pressure = excluded.review_pressure,
138
+ outcome_score = excluded.outcome_score,
139
+ updated_at = excluded.updated_at`).run(inputs.ref, inputs.lastRetrievedAt, inputs.currentRetrievalCount, expectedRetrievalRate, inputs.negativeFeedbackCount, inputs.acceptedChangeCount, reviewPressure, outcomeScore, now);
140
+ return { outcomeScore, reviewPressure, isNewRow };
141
+ }
142
+ // ── Reader ────────────────────────────────────────────────────────────────────
143
+ /**
144
+ * Load the outcome row for one asset, or `undefined` if not yet written.
145
+ */
146
+ export function getAssetOutcome(db, ref) {
147
+ const row = db
148
+ .prepare(`SELECT asset_ref, last_retrieved_at, retrieval_count, expected_retrieval_rate,
149
+ negative_feedback_count, accepted_change_count, review_pressure,
150
+ outcome_score, updated_at
151
+ FROM asset_outcome WHERE asset_ref = ?`)
152
+ .get(ref);
153
+ return row == null ? undefined : row;
154
+ }
155
+ /**
156
+ * Load ALL asset_outcome rows. Used for the proxy-adequacy tripwire computation.
157
+ */
158
+ export function getAllAssetOutcomes(db) {
159
+ return db
160
+ .prepare(`SELECT asset_ref, last_retrieved_at, retrieval_count, expected_retrieval_rate,
161
+ negative_feedback_count, accepted_change_count, review_pressure,
162
+ outcome_score, updated_at
163
+ FROM asset_outcome ORDER BY asset_ref`)
164
+ .all();
165
+ }
166
+ /**
167
+ * Build a Map<ref, outcome_score> for a set of refs in one query.
168
+ * Used by `salience.ts` to populate `outcomeSalience`.
169
+ */
170
+ export function getOutcomeScoresByRef(db, refs) {
171
+ const result = new Map();
172
+ if (refs.length === 0)
173
+ return result;
174
+ const CHUNK = 500;
175
+ for (let i = 0; i < refs.length; i += CHUNK) {
176
+ const chunk = refs.slice(i, i + CHUNK);
177
+ const placeholders = chunk.map(() => "?").join(",");
178
+ const rows = db
179
+ .prepare(`SELECT asset_ref, outcome_score FROM asset_outcome WHERE asset_ref IN (${placeholders})`)
180
+ .all(...chunk);
181
+ for (const row of rows) {
182
+ result.set(row.asset_ref, row.outcome_score);
183
+ }
184
+ }
185
+ return result;
186
+ }
187
+ // ── outcomeSalience projection ────────────────────────────────────────────────
188
+ /**
189
+ * Convert a raw `outcome_score` (differential, may be negative) to a
190
+ * `outcomeSalience` value in [0, 1] suitable for use in the salience projection.
191
+ *
192
+ * Approach:
193
+ * 1. Clip negative scores to 0 (a negative outcome just means "below expected",
194
+ * not "irrelevant"; it should not harm the base retrieval/encoding ranking).
195
+ * 2. Apply the diversity floor so rare-but-correct assets always retain a
196
+ * minimum `outcomeSalience` relative to the stash-wide maximum.
197
+ * 3. Normalise by `maxScore` (the stash-wide max outcome_score) so the term
198
+ * lives in [0, 1]. When maxScore ≤ 0 (all seeds, nothing observed yet),
199
+ * return the floor (or 0 if DIVERSITY_FLOOR_FRACTION = 0).
200
+ *
201
+ * @param outcomeScore - Raw outcome_score from asset_outcome.
202
+ * @param maxScore - Maximum outcome_score across ALL rows (≥ 0 after clip).
203
+ * Callers must compute this once per batch and pass it in.
204
+ */
205
+ export function outcomeScoreToSalience(outcomeScore, maxScore) {
206
+ const clipped = Math.max(0, outcomeScore);
207
+ if (maxScore <= 0) {
208
+ // No positive scores observed yet — return diversity floor.
209
+ return DIVERSITY_FLOOR_FRACTION;
210
+ }
211
+ const normalised = clipped / maxScore;
212
+ // Apply diversity floor.
213
+ return Math.max(DIVERSITY_FLOOR_FRACTION, normalised);
214
+ }
215
+ // ── Proxy-adequacy tripwire ───────────────────────────────────────────────────
216
+ /**
217
+ * Dead-proxy threshold: |corr| below this means outcome_score carries no
218
+ * information about improvement need (pure noise).
219
+ */
220
+ export const PROXY_DEAD_CORR_THRESHOLD = 0.1;
221
+ /**
222
+ * Minimum sample size before the dead-proxy check fires. Below this, a
223
+ * near-zero correlation is indistinguishable from small-sample noise.
224
+ */
225
+ export const PROXY_DEAD_MIN_N = 500;
226
+ /**
227
+ * Compute `corr(outcome_score, accepted_change_rate)` across all asset_outcome
228
+ * rows. Returns `{correlation: NaN, n, isInverted: false}` when there is
229
+ * insufficient data (fewer than 3 rows or zero variance in either variable).
230
+ *
231
+ * A NEGATIVE correlation means: assets with a HIGH outcome_score have a LOW
232
+ * accepted_change_rate — i.e. the assets the proxy rates as "doing well"
233
+ * (frequently retrieved) are precisely the ones that rarely yield an accepted
234
+ * improvement. That inverts the proxy: high outcome_score ≠ "doing well", so the
235
+ * coarse retrieval-delta signal is no longer trustworthy and the 0.10+ rich
236
+ * signal is due. (`isInverted = correlation < -0.3`.)
237
+ */
238
+ export function computeProxyAdequacy(rows) {
239
+ const n = rows.length;
240
+ if (n < 3)
241
+ return { correlation: Number.NaN, n, isInverted: false, isDead: false };
242
+ // accepted_change_rate per row.
243
+ const xs = rows.map((r) => r.outcome_score);
244
+ const ys = rows.map((r) => r.accepted_change_count / Math.max(1, r.retrieval_count));
245
+ const meanX = xs.reduce((a, b) => a + b, 0) / n;
246
+ const meanY = ys.reduce((a, b) => a + b, 0) / n;
247
+ let covXY = 0;
248
+ let varX = 0;
249
+ let varY = 0;
250
+ for (let i = 0; i < n; i++) {
251
+ const dx = xs[i] - meanX;
252
+ const dy = ys[i] - meanY;
253
+ covXY += dx * dy;
254
+ varX += dx * dx;
255
+ varY += dy * dy;
256
+ }
257
+ covXY /= n;
258
+ varX /= n;
259
+ varY /= n;
260
+ const denom = Math.sqrt(varX) * Math.sqrt(varY);
261
+ if (denom < 1e-12)
262
+ return { correlation: Number.NaN, n, isInverted: false, isDead: false };
263
+ const correlation = covXY / denom;
264
+ // Inverted proxy: negative correlation between outcome and accepted_change_rate
265
+ // means high-outcome assets are also high-need — the opposite of "useful".
266
+ const isInverted = correlation < -0.3;
267
+ // Dead proxy: near-zero correlation at scale — the score is noise.
268
+ const isDead = n >= PROXY_DEAD_MIN_N && Math.abs(correlation) < PROXY_DEAD_CORR_THRESHOLD;
269
+ return { correlation, n, isInverted, isDead };
270
+ }