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
@@ -0,0 +1,455 @@
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
+ /**
5
+ * WS-1 — Unified SALIENCE model (S1 seam).
6
+ *
7
+ * Replaces the three competing attention formulas that existed across:
8
+ * - proactive-maintenance.ts:186 — `importance × log(1+freq) × recencyDecay / log10(size)`
9
+ * - feedback-valence.ts:111 — `combinedEligibilityScore = utility·0.7 + valence·0.3`
10
+ * - getUtilityScoresByIds — MemRL utility EMA (#386)
11
+ *
12
+ * ## Salience vector (three independently-stored, independently-decayable sub-scores)
13
+ *
14
+ * | Sub-score | Brain analogy | Source |
15
+ * |-------------------|------------------------|---------------------------------|
16
+ * | `encodingSalience`| Amygdala tagging (Gap 1)| Set at extract; v1 = type weight|
17
+ * | `outcomeSalience` | Dopaminergic outcome | WS-2 (0 until that lands) |
18
+ * | `retrievalSalience`| Hippocampal frequency+recency | usage_events + utility_scores |
19
+ *
20
+ * ## Projection
21
+ *
22
+ * `rankScore = (w_e·encoding + w_o·outcome + w_r·retrieval) × sizePenalty`, normalized [0,1].
23
+ *
24
+ * **WS-2 default-ON (R1 loop closure):**
25
+ * `w_o = 0.15` is applied by default now that `outcome_score` saturates at
26
+ * `OUTCOME_SCORE_MAX` (G2). Operators can opt out via
27
+ * `improve.salience.outcomeWeightEnabled: false`, which restores the WS-1
28
+ * parity weights `w_e=0.30, w_r=0.70, w_o=0`. `outcomeSalience` is populated
29
+ * from `asset_outcome.outcome_score` regardless of the flag.
30
+ *
31
+ * ## Plasticity
32
+ *
33
+ * `consecutive_no_ops` (INTEGER column in `asset_salience`) dampens CONSOLIDATION-
34
+ * SELECTION only — it is intentionally NOT applied to `rankScore`, so a stable
35
+ * asset stays fully retrievable while no longer consuming repeated LLM merge
36
+ * attempts. See `getConsecutiveNoOps` / `recordNoOp` / `resetConsecutiveNoOps`.
37
+ *
38
+ * ## Canonical store
39
+ *
40
+ * The three sub-scores live in `state.db :: asset_salience` (canonical).
41
+ * An optional frontmatter mirror of the stable `encodingSalience` is allowed for
42
+ * portability (issue #608 may write it there), but state.db is the source of truth
43
+ * for ranking. This prevents the frontmatter-vs-state.db split that issue #608
44
+ * would otherwise create (#608 pull-forward decision from Part VI).
45
+ *
46
+ * @module salience
47
+ */
48
+ import { makeAssetRef } from "../../core/asset/asset-ref.js";
49
+ import { getAllEntries, getUtilityScoresByIds } from "../../indexer/db/db.js";
50
+ import { WARM_START_CAP } from "./outcome-loop.js";
51
+ // ── One day in ms ─────────────────────────────────────────────────────────────
52
+ const DAY_MS = 86_400_000;
53
+ // ── Recency decay half-life (mirrors the proactive-maintenance prototype) ─────
54
+ const RECENCY_HALFLIFE_DAYS = 21;
55
+ // ── Recency-floor half-life (R4 — SHY-style continuous downscaling) ──────────
56
+ //
57
+ // The recency floor itself decays on this (much longer) half-life so an
58
+ // unreviewed-forever asset keeps drifting down instead of parking at the 0.1
59
+ // floor. This replaces the deleted homeostatic demotion pass (which was
60
+ // default-off and self-undoing — every salience recompute clobbered it);
61
+ // folding the decay into the always-applied recency term makes it persist by
62
+ // construction. At 180 days the floor halves; a 1-year-stale asset sits at
63
+ // ~0.025 instead of 0.1.
64
+ const RECENCY_FLOOR_HALFLIFE_DAYS = 180;
65
+ // Absolute epsilon under the decaying floor. Keeps the frequency term ordinal
66
+ // for assets whose last-use timestamp is unknown (utility_scores has no
67
+ // last_used_at) — without it their retrieval salience collapses to exactly 0
68
+ // and frequency ordering is lost for maintenance selection.
69
+ const RECENCY_EPSILON = 0.01;
70
+ // ── Size proxy floor (avoids log10(0)) ────────────────────────────────────────
71
+ const SIZE_FLOOR_BYTES = 200;
72
+ // ── Projection weights ────────────────────────────────────────────────────────
73
+ //
74
+ // These constants are the DEFAULT ranking weights (R1 loop closure). Operators
75
+ // can opt back out to the WS-1 parity weights (w_e=0.30, w_r=0.70, w_o=0) via
76
+ // `improve.salience.outcomeWeightEnabled: false`.
77
+ //
78
+ // WS-2 split (w_e=0.25, w_o=0.15, w_r=0.60, sum = 1.0):
79
+ // [exp] Expert recommendation: encoding should be moderate so a type-importance
80
+ // stub does not completely dominate; retrieval should be strong since it directly
81
+ // measures use; outcome provides a quality signal proportional to usefulness.
82
+ //
83
+ // Re-tune via the Part-V measurement protocol if the throughput/quality gate
84
+ // shows regression after enabling the outcome weight.
85
+ export const W_ENCODING = 0.25; // WS-2 target encoding weight (w_e)
86
+ export const W_OUTCOME = 0.15; // WS-2 target outcome weight (w_o)
87
+ export const W_RETRIEVAL = 0.6; // WS-2 target retrieval weight (w_r)
88
+ // Compile-time guard: weights must sum to 1.0 (±ε). The TS initializer runs
89
+ // at module load, not build time, so this acts as a startup assertion.
90
+ if (Math.abs(W_ENCODING + W_OUTCOME + W_RETRIEVAL - 1.0) > 1e-9) {
91
+ throw new Error(`salience.ts: W_ENCODING + W_OUTCOME + W_RETRIEVAL must equal 1.0 (got ${W_ENCODING + W_OUTCOME + W_RETRIEVAL})`);
92
+ }
93
+ // ── WS-1 parity weights ───────────────────────────────────────────────────────
94
+ //
95
+ // These constants reflect the WS-1 parity weights used when the operator
96
+ // explicitly opts out (`outcomeWeightEnabled: false`). They preserve the
97
+ // WS-1 two-way split (w_e=0.30, w_r=0.70) with w_o=0 so outcome does not
98
+ // affect rankScore in the opt-out mode.
99
+ //
100
+ // Named here (rather than inline literals in the else branch) so a future
101
+ // re-tune has a single source of truth and the sum-to-1 guard below catches
102
+ // any accidental mis-edit.
103
+ export const W_ENCODING_PARITY = 0.3; // WS-1 parity encoding weight
104
+ export const W_OUTCOME_PARITY = 0; // WS-1 parity outcome weight (0 = disabled)
105
+ export const W_RETRIEVAL_PARITY = 0.7; // WS-1 parity retrieval weight
106
+ // Startup guard: parity triple must also sum to 1.0 (±ε).
107
+ if (Math.abs(W_ENCODING_PARITY + W_OUTCOME_PARITY + W_RETRIEVAL_PARITY - 1.0) > 1e-9) {
108
+ throw new Error(`salience.ts: W_ENCODING_PARITY + W_OUTCOME_PARITY + W_RETRIEVAL_PARITY must equal 1.0 (got ${W_ENCODING_PARITY + W_OUTCOME_PARITY + W_RETRIEVAL_PARITY})`);
109
+ }
110
+ // ── Type-importance fallback weights (#608 landed) ────────────────────────────
111
+ //
112
+ // The real encoding salience estimator is `scoreEncodingSalience` in
113
+ // `encoding-salience.ts` (#608). These weights are the fallback used when
114
+ // `SalienceInputs.encodingSalience` is absent (pre-#608 assets without a
115
+ // frontmatter `salience:` field or a state.db row seeded by distill).
116
+ export const DEFAULT_TYPE_ENCODING_WEIGHTS = Object.freeze({
117
+ skill: 0.9,
118
+ agent: 0.9,
119
+ command: 0.8,
120
+ workflow: 0.8,
121
+ lesson: 0.75,
122
+ knowledge: 0.7,
123
+ script: 0.6,
124
+ memory: 0.5,
125
+ });
126
+ /** Default encoding salience for types not in the table above. */
127
+ export const DEFAULT_ENCODING_SALIENCE = 0.5;
128
+ // ── Core computation ─────────────────────────────────────────────────────────
129
+ /**
130
+ * Compute the salience vector for one asset.
131
+ *
132
+ * Pure function — no I/O. All inputs are pre-fetched by the caller.
133
+ */
134
+ export function computeSalience(inputs) {
135
+ const now = inputs.now ?? Date.now();
136
+ // ── Encoding salience ────────────────────────────────────────────────────────
137
+ //
138
+ // When `inputs.encodingSalience` is provided (computed by `scoreEncodingSalience`
139
+ // in encoding-salience.ts at extract/distill time, #608), use it directly.
140
+ // Fall back to the type-importance stub only when the caller has not yet
141
+ // computed a content-based score (e.g. on older assets before the first
142
+ // staleness refresh runs).
143
+ const encodingSource = inputs.encodingSalience !== undefined ? "content" : "type-stub";
144
+ const encoding = inputs.encodingSalience !== undefined
145
+ ? Math.min(1, Math.max(0, inputs.encodingSalience))
146
+ : (DEFAULT_TYPE_ENCODING_WEIGHTS[inputs.type] ?? DEFAULT_ENCODING_SALIENCE);
147
+ // ── Outcome salience (WS-2 active) ────────────────────────────────────────
148
+ //
149
+ // When `inputs.outcomeSalience` is provided (WS-2 has populated asset_outcome
150
+ // for this ref), use it directly — it has already been normalised by
151
+ // `outcomeScoreToSalience` in outcome-loop.ts (value in [DIVERSITY_FLOOR, 1]).
152
+ //
153
+ // When absent (new asset, no WS-2 row yet): fall back to the warm-start seed
154
+ // from `utilityScore` clipped to [0, WARM_START_CAP], matching the seed
155
+ // value that `updateAssetOutcome` writes on first row creation. This ensures
156
+ // `outcomeSalience` is non-zero at launch for assets with utility history
157
+ // (avoiding the starvation problem described in the plan §WS-2 warm start).
158
+ let outcome;
159
+ if (inputs.outcomeSalience !== undefined) {
160
+ // Direct pass-through — caller already normalised via outcomeScoreToSalience.
161
+ outcome = Math.min(1, Math.max(0, inputs.outcomeSalience));
162
+ }
163
+ else {
164
+ // Warm-start fallback: clip utility to [0, WARM_START_CAP] so the
165
+ // outcomeSalience term contributes a modest non-zero baseline.
166
+ outcome = Math.min(WARM_START_CAP, Math.max(0, inputs.utilityScore ?? 0));
167
+ }
168
+ // ── Retrieval salience ─────────────────────────────────────────────────────
169
+ //
170
+ // Formula: log(1 + freq) × recencyDecay
171
+ // log(1+freq): sub-linear frequency term (same as proactive-maintenance prototype).
172
+ // recencyDecay: max(ε, 0.1·0.5^(useAgeDays/180) + 0.5^(useAgeDays/21)) —
173
+ // the fast term halves every 21 days; the 0.1 floor itself halves every
174
+ // 180 days (R4: SHY-style continuous downscaling — an unreviewed-forever
175
+ // asset keeps drifting down instead of parking at the floor). The ε=0.01
176
+ // epsilon keeps the frequency term ordinal for unknown-last-use assets.
177
+ // lastUseMs=0/undefined → useAgeDays=9999 → recencyDecay=ε.
178
+ //
179
+ // The recency term is MANDATORY (plan requirement §WS-1 step 2). Without it
180
+ // retrievalSalience degenerates to a non-decaying frequency count. This
181
+ // always-applied decay replaces the deleted homeostatic demotion pass.
182
+ const lastUseMs = inputs.lastUseMs ?? 0;
183
+ const useAgeDays = lastUseMs > 0 ? (now - lastUseMs) / DAY_MS : 9999;
184
+ const recencyDecay = Math.max(RECENCY_EPSILON, 0.1 * 0.5 ** (useAgeDays / RECENCY_FLOOR_HALFLIFE_DAYS) + 0.5 ** (useAgeDays / RECENCY_HALFLIFE_DAYS));
185
+ const rawRetrieval = Math.log(1 + inputs.retrievalFreq) * recencyDecay;
186
+ // ── Size penalty ─────────────────────────────────────────────────────────────
187
+ // 1/log10(size): larger assets are slightly deprioritized (same as proactive prototype).
188
+ const sizeProxy = Math.max(SIZE_FLOOR_BYTES, inputs.sizeBytes ?? 0);
189
+ const sizePenalty = 1 / Math.log10(sizeProxy);
190
+ // ── Projection → rankScore ────────────────────────────────────────────────
191
+ //
192
+ // Raw projection may be > 1 (log retrieval terms can exceed 1 for high freq + fresh use).
193
+ // Normalize by the theoretical maximum of the retrieval component:
194
+ // max retrievalRaw = log(1 + Infinity) × (0.1 + 1.0) = Infinity, so we
195
+ // cap instead — rankScore is clamped to [0,1] after applying the size penalty.
196
+ //
197
+ // Normalization approach: we scale the combined linear sum to [0,1] by clamping,
198
+ // after applying the size penalty. The encoding term is already in [0,1]; the
199
+ // retrieval term is open-ended but bounded in practice by log(1+N)×1.1 where N
200
+ // is the retrieval count. We normalize `retrieval` to [0,1] using a soft cap:
201
+ // retrieval_normalized = rawRetrieval / (rawRetrieval + 1)
202
+ // which asymptotes to 1 and equals 0.5 at rawRetrieval=1. This is the same
203
+ // formula used for MemRL utility updates.
204
+ const retrieval = rawRetrieval / (rawRetrieval + 1);
205
+ // ── Weight selection (R1 — outcome loop closed by default) ───────────────
206
+ //
207
+ // When `outcomeWeightEnabled` is true/absent (DEFAULT ON since the G2
208
+ // saturation cap landed): use WS-2 weights (w_e=0.25, w_o=0.15, w_r=0.60)
209
+ // so the prediction-error outcome signal actually shapes rankScore — this
210
+ // is the R1 loop-closure from docs/design/improve-self-learning-analysis.md.
211
+ //
212
+ // When `outcomeWeightEnabled` is explicitly false (operator opt-out via
213
+ // `improve.salience.outcomeWeightEnabled: false`): fall back to the WS-1
214
+ // parity weights (w_e=0.30, w_r=0.70, w_o=0). The `outcome` sub-score is
215
+ // still computed and stored for observability in that mode.
216
+ let we;
217
+ let wo;
218
+ let wr;
219
+ if (inputs.outcomeWeightEnabled !== false) {
220
+ // WS-2 active (default): three-way split.
221
+ we = W_ENCODING; // 0.25
222
+ wo = W_OUTCOME; // 0.15
223
+ wr = W_RETRIEVAL; // 0.60
224
+ }
225
+ else {
226
+ // WS-1 parity (opt-out): w_o=0, redistribute to WS-1 proportions.
227
+ // Original WS-1 split was w_e=0.30, w_r=0.70.
228
+ we = W_ENCODING_PARITY;
229
+ wo = W_OUTCOME_PARITY;
230
+ wr = W_RETRIEVAL_PARITY;
231
+ }
232
+ const rawRankScore = (we * encoding + wo * outcome + wr * retrieval) * sizePenalty;
233
+ const rankScore = Math.min(1, Math.max(0, rawRankScore));
234
+ return { encoding, outcome, retrieval, rankScore, encodingSource };
235
+ }
236
+ /**
237
+ * Does this row carry a genuine content-derived `encoding_salience` (#644)?
238
+ *
239
+ * Returns true when the provenance flag is `"content"`. For legacy rows
240
+ * (`encoding_source === null`, written before migration 015) we apply a
241
+ * conservative heuristic: treat the stored value as content-derived only when it
242
+ * does NOT equal the pure type-weight stub for the asset's type — because before
243
+ * the #644 fix every run overwrote real scores with the stub, a value that still
244
+ * differs from the stub must have been content-written and never re-clobbered.
245
+ * When the type cannot be determined (no `type` given) a null-provenance row is
246
+ * treated as a stub (the safe default).
247
+ */
248
+ export function isContentEncodingRow(row, type) {
249
+ if (row.encoding_source === "content")
250
+ return true;
251
+ if (row.encoding_source === "type-stub")
252
+ return false;
253
+ // Legacy NULL provenance: differ-from-stub heuristic.
254
+ if (!type)
255
+ return false;
256
+ const stub = DEFAULT_TYPE_ENCODING_WEIGHTS[type] ?? DEFAULT_ENCODING_SALIENCE;
257
+ return Math.abs(row.encoding_salience - stub) > 1e-9;
258
+ }
259
+ /**
260
+ * Upsert salience scores for one asset into state.db.
261
+ *
262
+ * Idempotent: safe to call every run; updates the outcome / retrieval / rank
263
+ * columns on conflict.
264
+ *
265
+ * #644 — encoding provenance guard: the `encoding_salience` + `encoding_source`
266
+ * columns are NOT lowered from a real content-derived score to a type-weight
267
+ * stub. When the stored row is `encoding_source = 'content'` and the incoming
268
+ * vector is a `type-stub` fallback, the stored encoding score and its provenance
269
+ * are preserved (only the other sub-scores and `rank_score` advance). A `content`
270
+ * write always wins; a `type-stub` write only seeds a row that has no content
271
+ * score yet. This stops the improve loop's type-weight fallback re-asserting the
272
+ * stub over a distill-written score on every run.
273
+ *
274
+ * NOTE: when the guard preserves the stored encoding score, the incoming
275
+ * `vector.rankScore` (computed from the stub encoding) is still written. Callers
276
+ * that want the rank_score to reflect the preserved content score should pass the
277
+ * stored content score back in as `inputs.encodingSalience` to `computeSalience`
278
+ * — which the improve loop does. The guard here is the defensive backstop.
279
+ */
280
+ export function upsertAssetSalience(db, ref, vector, now) {
281
+ const ts = now ?? Date.now();
282
+ db.prepare(`INSERT INTO asset_salience
283
+ (asset_ref, encoding_salience, outcome_salience, retrieval_salience, rank_score, consecutive_no_ops, updated_at, encoding_source)
284
+ VALUES (?, ?, ?, ?, ?, 0, ?, ?)
285
+ ON CONFLICT(asset_ref) DO UPDATE SET
286
+ -- #644: never lower a real content-derived score to a type-weight stub.
287
+ -- Keep the stored encoding score + provenance when the stored row is
288
+ -- 'content' and the incoming write is a 'type-stub' fallback.
289
+ encoding_salience = CASE
290
+ WHEN asset_salience.encoding_source = 'content' AND excluded.encoding_source = 'type-stub'
291
+ THEN asset_salience.encoding_salience
292
+ ELSE excluded.encoding_salience
293
+ END,
294
+ encoding_source = CASE
295
+ WHEN asset_salience.encoding_source = 'content' AND excluded.encoding_source = 'type-stub'
296
+ THEN asset_salience.encoding_source
297
+ ELSE excluded.encoding_source
298
+ END,
299
+ outcome_salience = excluded.outcome_salience,
300
+ retrieval_salience = excluded.retrieval_salience,
301
+ rank_score = excluded.rank_score,
302
+ updated_at = excluded.updated_at`).run(ref, vector.encoding, vector.outcome, vector.retrieval, vector.rankScore, ts, vector.encodingSource ?? "type-stub");
303
+ }
304
+ /**
305
+ * Load the salience row for one asset, or undefined if not yet computed.
306
+ */
307
+ export function getAssetSalience(db, ref) {
308
+ const row = db
309
+ .prepare(`SELECT asset_ref, encoding_salience, outcome_salience, retrieval_salience,
310
+ rank_score, consecutive_no_ops, updated_at, encoding_source
311
+ FROM asset_salience WHERE asset_ref = ?`)
312
+ .get(ref);
313
+ // Bun SQLite returns null (not undefined) when no row found.
314
+ return row == null ? undefined : row;
315
+ }
316
+ /**
317
+ * Load ALL rank scores from the asset_salience table (full-stash query).
318
+ *
319
+ * Used by the forgetting-safety report (plan §WS-1 step 7) to compute stash-wide
320
+ * rank positions rather than pool-relative positions. Returns an empty Map when the
321
+ * table is empty (first WS-1 run = no pre-existing rows).
322
+ *
323
+ * Order is unspecified; callers must sort before assigning 1-indexed positions.
324
+ */
325
+ export function getAllRankScores(db) {
326
+ const rows = db.prepare("SELECT asset_ref, rank_score FROM asset_salience").all();
327
+ const result = new Map();
328
+ for (const row of rows) {
329
+ result.set(row.asset_ref, row.rank_score);
330
+ }
331
+ return result;
332
+ }
333
+ // ── Plasticity helpers ────────────────────────────────────────────────────────
334
+ /**
335
+ * Increment `consecutive_no_ops` for an asset. Called after a no-op reflect/distill.
336
+ * Has NO effect on `rank_score` — the plasticity counter only dampens consolidation
337
+ * selection, not retrieval ranking. See plan §WS-1 step 8.
338
+ *
339
+ * Invariant: recordNoOp must never originate rank_score semantics. If the asset has
340
+ * no salience row yet (persistence's best-effort try/catch may have swallowed an
341
+ * error), we do nothing — a no-op counter is meaningless without a rank_score row,
342
+ * and a synthetic INSERT would fabricate a rank_score=0 entry that could produce
343
+ * false catastrophic-forgetting signals in buildRankChangeReport.
344
+ */
345
+ export function recordNoOp(db, ref) {
346
+ db.prepare(`UPDATE asset_salience SET consecutive_no_ops = consecutive_no_ops + 1, updated_at = ? WHERE asset_ref = ?`).run(Date.now(), ref);
347
+ // If changes === 0 the asset has no salience row yet — leave the table unchanged.
348
+ }
349
+ /**
350
+ * Reset `consecutive_no_ops` to 0 when an asset produces an accepted change.
351
+ * Call after a successful proposal acceptance or detected mutation.
352
+ */
353
+ export function resetConsecutiveNoOps(db, ref) {
354
+ db.prepare(`UPDATE asset_salience SET consecutive_no_ops = 0, updated_at = ? WHERE asset_ref = ?`).run(Date.now(), ref);
355
+ }
356
+ /**
357
+ * Return the `consecutive_no_ops` count for one asset. 0 when unknown.
358
+ */
359
+ export function getConsecutiveNoOps(db, ref) {
360
+ const row = db.prepare(`SELECT consecutive_no_ops FROM asset_salience WHERE asset_ref = ?`).get(ref);
361
+ return row?.consecutive_no_ops ?? 0;
362
+ }
363
+ // ── Consolidation-selection dampener constants ────────────────────────────────
364
+ //
365
+ // Assets with consecutive_no_ops >= THRESHOLD are deprioritised in the
366
+ // SELECTION ORDER only. The persisted rank_score is intentionally left
367
+ // unchanged so stable assets remain fully retrievable by other callers.
368
+ //
369
+ // Tuning guidance:
370
+ // THRESHOLD — how many consecutive no-op runs before dampening kicks in.
371
+ // 3 means "skipped three times in a row", which signals the
372
+ // LLM consistently has nothing to say about this asset.
373
+ // FACTOR — multiplicative penalty on the effective selection score.
374
+ // 0.5 halves the apparent score so a dampened asset sorts
375
+ // after any peer with >= half its rankScore.
376
+ export const SALIENCE_NO_OP_DAMPEN_THRESHOLD = 3;
377
+ export const SALIENCE_NO_OP_DAMPEN_FACTOR = 0.5;
378
+ /**
379
+ * Emit the forgetting-safety rank-change distribution report.
380
+ *
381
+ * Compares the provided `newRanks` (Map<ref, position (1-indexed)>) against
382
+ * the provided `oldRanks` and flags refs that were in the old top-200 but
383
+ * are now below position 500 as "forgetting candidates".
384
+ *
385
+ * Caller is responsible for computing old/new rank positions before and after
386
+ * the WS-1 formula cutover. Called once at cutover, not every run.
387
+ *
388
+ * @param oldRanks - Map<ref, 1-indexed rank position> under the OLD formula.
389
+ * @param newRanks - Map<ref, 1-indexed rank position> under the NEW formula.
390
+ * @param oldTopN - Assets in old top-N to guard (default: 200).
391
+ * @param forgettingThreshold - New rank position below which a fall is flagged (default: 500).
392
+ */
393
+ export function buildRankChangeReport(oldRanks, newRanks, oldTopN = 200, forgettingThreshold = 500) {
394
+ const allChanges = [];
395
+ const forgettingCandidates = [];
396
+ for (const [ref, oldRank] of oldRanks) {
397
+ const newRank = newRanks.get(ref);
398
+ if (newRank === undefined)
399
+ continue; // ref not in new ranking
400
+ const rankDelta = newRank - oldRank; // positive = fell in rank
401
+ allChanges.push({ ref, oldRank, newRank, rankDelta });
402
+ if (oldRank <= oldTopN && newRank > forgettingThreshold) {
403
+ forgettingCandidates.push({ ref, oldRank, newRank, rankDelta });
404
+ }
405
+ }
406
+ // Sort by magnitude of rank drop (most dramatic first).
407
+ forgettingCandidates.sort((a, b) => b.rankDelta - a.rankDelta);
408
+ return { forgettingCandidates, allChanges };
409
+ }
410
+ // ── Last-use timestamp lookup helper ─────────────────────────────────────────
411
+ //
412
+ // Wraps the index DB query to retrieve the last-retrieval timestamp per ref,
413
+ // so callers do not need to import the raw db helpers directly. Returns a Map
414
+ // keyed by the same ref strings passed in.
415
+ //
416
+ // Source: `utility_scores.last_used_at` (ISO-8601 string) joined to entries
417
+ // via entry_id. WS-2 may later supersede this with `asset_outcome.last_retrieved_at`.
418
+ /**
419
+ * Build a Map<ref, lastUseMs> from the index database's utility_scores table.
420
+ *
421
+ * Returns only refs that have a non-null `last_used_at`. Refs absent from the
422
+ * map should be treated as never retrieved (lastUseMs = 0).
423
+ *
424
+ * @param indexDb - An open read-capable index database connection.
425
+ * @param refs - The set of asset refs to look up.
426
+ */
427
+ export function getLastUseMsByRef(indexDb, refs) {
428
+ const result = new Map();
429
+ if (refs.length === 0)
430
+ return result;
431
+ const refSet = new Set(refs);
432
+ const allEntries = getAllEntries(indexDb);
433
+ const idToRef = new Map();
434
+ for (const indexed of allEntries) {
435
+ const ref = makeAssetRef(indexed.entry.type, indexed.entry.name);
436
+ if (refSet.has(ref))
437
+ idToRef.set(indexed.id, ref);
438
+ }
439
+ const ids = [...idToRef.keys()];
440
+ if (ids.length === 0)
441
+ return result;
442
+ const { global: scores } = getUtilityScoresByIds(indexDb, ids);
443
+ for (const [id, row] of scores) {
444
+ const ref = idToRef.get(id);
445
+ if (!ref)
446
+ continue;
447
+ const lastUsedAt = row.lastUsedAt;
448
+ if (!lastUsedAt)
449
+ continue;
450
+ const ms = typeof lastUsedAt === "number" ? lastUsedAt : Date.parse(lastUsedAt);
451
+ if (ms > 0)
452
+ result.set(ref, ms);
453
+ }
454
+ return result;
455
+ }
@@ -0,0 +1,168 @@
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
+ /**
5
+ * WS-3b Step 0b — Schema-similarity intake gate.
6
+ *
7
+ * At intake, if a new candidate's body embedding is within ε of an existing
8
+ * derived-layer lesson/knowledge node, mark `schema-consistent` and lower its
9
+ * priority; only schema-inconsistent/contradicting candidates get full
10
+ * `encodingSalience`. One embedding lookup via body_embeddings cache; relieves
11
+ * dedup pressure before it accumulates.
12
+ *
13
+ * @module schema-similarity-gate
14
+ */
15
+ import { warn } from "../../core/warn.js";
16
+ import { closeDatabase, openExistingDatabase } from "../../indexer/db/db.js";
17
+ /** Default epsilon for schema-similarity gate (looser than dedup's 0.97). */
18
+ export const DEFAULT_SCHEMA_SIMILARITY_EPSILON = 0.85;
19
+ /** Default multiplicative confidence penalty applied to schema-consistent candidates. */
20
+ export const DEFAULT_SCHEMA_CONFIDENCE_PENALTY = 0.5;
21
+ /**
22
+ * Check whether a candidate body embedding is schema-consistent with an existing
23
+ * derived-layer lesson/knowledge node. Returns `true` when the candidate is
24
+ * within ε of ANY existing derived node (i.e. it's likely covering ground the
25
+ * derived layer already knows about, so give it lower priority).
26
+ *
27
+ * One embedding lookup via the body_embeddings cache; no LLM call.
28
+ * Fails open: returns `false` (not schema-consistent) on any error so the
29
+ * candidate is not silently dropped.
30
+ *
31
+ * @param candidateEmbedding - Float32 embedding vector for the candidate body.
32
+ * @param existingDerivedEmbeddings - Pre-loaded embeddings for existing derived assets.
33
+ * @param config - Schema-similarity gate config.
34
+ */
35
+ export function isSchemaConsistent(candidateEmbedding, existingDerivedEmbeddings, config) {
36
+ if (!config.enabled || existingDerivedEmbeddings.length === 0) {
37
+ return { consistent: false };
38
+ }
39
+ const epsilon = config.epsilon ?? DEFAULT_SCHEMA_SIMILARITY_EPSILON;
40
+ let bestSim = -Infinity;
41
+ let bestRef;
42
+ for (const { ref, embedding } of existingDerivedEmbeddings) {
43
+ // cosine similarity: dot(a,b) / (|a| * |b|)
44
+ let dot = 0;
45
+ let magA = 0;
46
+ let magB = 0;
47
+ for (let i = 0; i < candidateEmbedding.length; i++) {
48
+ const a = candidateEmbedding[i] ?? 0;
49
+ const b = embedding[i] ?? 0;
50
+ dot += a * b;
51
+ magA += a * a;
52
+ magB += b * b;
53
+ }
54
+ const sim = magA === 0 || magB === 0 ? 0 : dot / (Math.sqrt(magA) * Math.sqrt(magB));
55
+ if (sim > bestSim) {
56
+ bestSim = sim;
57
+ bestRef = ref;
58
+ }
59
+ }
60
+ if (bestSim >= epsilon) {
61
+ return { consistent: true, matchedRef: bestRef, similarity: bestSim };
62
+ }
63
+ return { consistent: false };
64
+ }
65
+ /**
66
+ * WS-3b Step-0b: apply the schema-similarity intake gate to one extract
67
+ * candidate. Pure/deterministic given `embedText`, so it is directly unit
68
+ * testable without the full extract→LLM harness.
69
+ *
70
+ * Returns the (possibly penalised) effective confidence plus a `penalised` flag
71
+ * and an optional human-readable `warning`. Parity guarantees:
72
+ * - `ctx === null` (gate disabled / default-off) → no change, never embeds.
73
+ * - empty `derivedEmbeddings` → no change, never embeds.
74
+ * - candidate type not lesson/knowledge → no change, never embeds.
75
+ * - embed throws → fail open (no change), warns.
76
+ */
77
+ export async function applySchemaSimilarityPenalty(candidate, ctx, embedText) {
78
+ const baseConfidence = typeof candidate.confidence === "number" ? candidate.confidence : undefined;
79
+ if (ctx === null || ctx.derivedEmbeddings.length === 0) {
80
+ return { effectiveConfidence: baseConfidence, penalised: false };
81
+ }
82
+ if (candidate.type !== "lesson" && candidate.type !== "knowledge") {
83
+ return { effectiveConfidence: baseConfidence, penalised: false };
84
+ }
85
+ try {
86
+ const candidateVec = await embedText(candidate.body);
87
+ const check = isSchemaConsistent(candidateVec, ctx.derivedEmbeddings, ctx.config);
88
+ if (check.consistent) {
89
+ const penalty = ctx.config.confidencePenalty ?? DEFAULT_SCHEMA_CONFIDENCE_PENALTY;
90
+ return {
91
+ effectiveConfidence: (baseConfidence ?? 1.0) * penalty,
92
+ penalised: true,
93
+ warning: `[extract] schema-consistent candidate ${candidate.type}:${candidate.name} ` +
94
+ `(sim=${check.similarity?.toFixed(3)} vs ${check.matchedRef}) — confidence penalised ×${penalty}`,
95
+ };
96
+ }
97
+ return { effectiveConfidence: baseConfidence, penalised: false };
98
+ }
99
+ catch (embedErr) {
100
+ // Fail open: embed errors must never abort extraction.
101
+ return {
102
+ effectiveConfidence: baseConfidence,
103
+ penalised: false,
104
+ warning: `[extract] schema-similarity embed failed for ${candidate.type}:${candidate.name} — skipping gate: ` +
105
+ (embedErr instanceof Error ? embedErr.message : String(embedErr)),
106
+ };
107
+ }
108
+ }
109
+ /**
110
+ * Load persisted body embeddings for all indexed **derived-layer**
111
+ * (lesson + knowledge) entries from index.db. Returns an empty array when
112
+ * the DB is unavailable, empty, or the embeddings table has no entries for
113
+ * those types — the caller treats an empty array as "gate inactive".
114
+ *
115
+ * FAIL-OPEN: any error emits a debug warning and returns an empty array.
116
+ * This ensures the extract pass never fails because of a missing index.
117
+ *
118
+ * The returned entries are keyed by `entry_key` (e.g. "lesson:foo",
119
+ * "knowledge:bar"). Only entries whose embedding dimension matches the first
120
+ * observed dimension are included (mixed-dim BLOBs are silently skipped).
121
+ *
122
+ * @param dbPath - Optional path override for index.db (for testing).
123
+ */
124
+ export function loadDerivedLayerEmbeddings(dbPath) {
125
+ let db;
126
+ try {
127
+ db = openExistingDatabase(dbPath);
128
+ const rows = db
129
+ .prepare(`SELECT e.entry_key, emb.embedding
130
+ FROM entries e
131
+ JOIN embeddings emb ON emb.id = e.id
132
+ WHERE e.entry_type IN ('lesson', 'knowledge')`)
133
+ .all();
134
+ if (rows.length === 0)
135
+ return [];
136
+ let expectedDim;
137
+ const result = [];
138
+ for (const row of rows) {
139
+ const buf = row.embedding;
140
+ if (!buf || buf.byteLength === 0 || buf.byteLength % 4 !== 0)
141
+ continue;
142
+ const dim = buf.byteLength / 4;
143
+ if (expectedDim === undefined)
144
+ expectedDim = dim;
145
+ if (dim !== expectedDim)
146
+ continue;
147
+ const aligned = new ArrayBuffer(buf.byteLength);
148
+ new Uint8Array(aligned).set(buf);
149
+ const f32 = new Float32Array(aligned);
150
+ result.push({ ref: row.entry_key, embedding: Array.from(f32) });
151
+ }
152
+ return result;
153
+ }
154
+ catch (err) {
155
+ warn("[schema-similarity-gate] loadDerivedLayerEmbeddings: failed to load from index.db — gate inactive:", err instanceof Error ? err.message : String(err));
156
+ return [];
157
+ }
158
+ finally {
159
+ if (db) {
160
+ try {
161
+ closeDatabase(db);
162
+ }
163
+ catch {
164
+ // ignore close errors
165
+ }
166
+ }
167
+ }
168
+ }
@@ -0,0 +1,51 @@
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
+ import { getDefaultLlmConfig, getImproveProcessConfig } from "../../core/config/config.js";
5
+ import { warn } from "../../core/warn.js";
6
+ import { resolveImproveProcessRunnerFromProfile, runnerIsLlm } from "../../integrations/agent/runner.js";
7
+ import { chatCompletion } from "../../llm/client.js";
8
+ /** Normalize an unknown thrown value to a human-readable message string. */
9
+ export function errMessage(e) {
10
+ return e instanceof Error ? e.message : String(e);
11
+ }
12
+ /**
13
+ * Slugify an asset ref for use in eval-case / rejection filenames: lowercase,
14
+ * non-alphanumerics collapsed to `-`, capped at 60 characters.
15
+ */
16
+ export function refSlug(ref) {
17
+ return ref
18
+ .replace(/[^a-z0-9]/gi, "-")
19
+ .toLowerCase()
20
+ .slice(0, 60);
21
+ }
22
+ /**
23
+ * Resolve the production LLM seam for an improve process (`recombine` /
24
+ * `procedural`). Returns a function that issues one bounded chatCompletion per
25
+ * call, or `undefined` when no LLM is configured (the pass then makes no
26
+ * calls). Previously copied verbatim in recombine.ts and procedural.ts.
27
+ *
28
+ * When `opts.activeProfile` is supplied, its per-process runner override wins
29
+ * over the `default` profile so `akm improve --profile <name>` selects the
30
+ * profile's model; absent falls back to `default`.
31
+ */
32
+ export function resolveImproveLlmFn(config, opts) {
33
+ const processConfig = getImproveProcessConfig(config, opts.processKey, opts.activeProfile);
34
+ const runnerSpec = resolveImproveProcessRunnerFromProfile(processConfig, config);
35
+ const llmConfig = runnerSpec && runnerIsLlm(runnerSpec) ? runnerSpec.connection : getDefaultLlmConfig(config);
36
+ if (!llmConfig)
37
+ return undefined;
38
+ return async (prompt) => {
39
+ const messages = [
40
+ { role: "system", content: opts.systemPrompt },
41
+ { role: "user", content: prompt },
42
+ ];
43
+ try {
44
+ return await chatCompletion(llmConfig, messages, { signal: opts.signal, enableThinking: false });
45
+ }
46
+ catch (e) {
47
+ warn(`${opts.tag} LLM call failed: ${String(e)}`);
48
+ return null;
49
+ }
50
+ };
51
+ }