akm-cli 0.9.0-beta.5 → 0.9.0-beta.51

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 (221) hide show
  1. package/CHANGELOG.md +711 -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/profiles/default.json +9 -4
  6. package/dist/assets/profiles/frequent.json +1 -1
  7. package/dist/assets/profiles/memory-focus.json +1 -1
  8. package/dist/assets/profiles/quick.json +1 -1
  9. package/dist/assets/profiles/synthesize.json +15 -0
  10. package/dist/assets/profiles/thorough.json +1 -1
  11. package/dist/assets/prompts/consolidate-system.md +23 -0
  12. package/dist/assets/prompts/contradiction-judge.md +33 -0
  13. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  14. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  15. package/dist/assets/prompts/extract-session.md +6 -2
  16. package/dist/assets/prompts/graph-extract-system.md +1 -0
  17. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  18. package/dist/assets/prompts/memory-infer-system.md +1 -0
  19. package/dist/assets/prompts/memory-infer-user.md +5 -0
  20. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  21. package/dist/assets/prompts/procedural-system.md +44 -0
  22. package/dist/assets/prompts/recombine-system.md +40 -0
  23. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  24. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  25. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  26. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  34. package/dist/assets/templates/html/health.html +281 -111
  35. package/dist/assets/wiki/ingest-workflow-template.md +38 -10
  36. package/dist/cli/parse-args.js +46 -1
  37. package/dist/cli/shared.js +28 -0
  38. package/dist/cli.js +27 -11
  39. package/dist/commands/agent/agent-dispatch.js +2 -2
  40. package/dist/commands/agent/agent-support.js +0 -7
  41. package/dist/commands/agent/contribute-cli.js +17 -4
  42. package/dist/commands/config-cli.js +18 -2
  43. package/dist/commands/env/child-env.js +47 -0
  44. package/dist/commands/env/env-cli.js +33 -26
  45. package/dist/commands/env/secret-cli.js +36 -22
  46. package/dist/commands/feedback-cli.js +15 -6
  47. package/dist/commands/graph/graph-cli.js +5 -13
  48. package/dist/commands/graph/graph.js +76 -72
  49. package/dist/commands/health/checks.js +49 -1
  50. package/dist/commands/health/html-report.js +422 -80
  51. package/dist/commands/health.js +386 -9
  52. package/dist/commands/improve/calibration.js +161 -0
  53. package/dist/commands/improve/consolidate/chunking.js +141 -0
  54. package/dist/commands/improve/consolidate/eligibility.js +81 -0
  55. package/dist/commands/improve/consolidate/merge.js +145 -0
  56. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  57. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  58. package/dist/commands/improve/consolidate.js +635 -660
  59. package/dist/commands/improve/dedup.js +482 -0
  60. package/dist/commands/improve/distill.js +159 -69
  61. package/dist/commands/improve/eligibility.js +434 -0
  62. package/dist/commands/improve/encoding-salience.js +205 -0
  63. package/dist/commands/improve/extract-cli.js +124 -2
  64. package/dist/commands/improve/extract-prompt.js +39 -2
  65. package/dist/commands/improve/extract-watch.js +140 -0
  66. package/dist/commands/improve/extract.js +389 -40
  67. package/dist/commands/improve/feedback-valence.js +54 -0
  68. package/dist/commands/improve/homeostatic.js +467 -0
  69. package/dist/commands/improve/improve-auto-accept.js +138 -7
  70. package/dist/commands/improve/improve-cli.js +36 -61
  71. package/dist/commands/improve/improve-profiles.js +14 -0
  72. package/dist/commands/improve/improve-result-file.js +14 -25
  73. package/dist/commands/improve/improve-session.js +58 -0
  74. package/dist/commands/improve/improve.js +485 -2498
  75. package/dist/commands/improve/locks.js +154 -0
  76. package/dist/commands/improve/loop-stages.js +1083 -0
  77. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  78. package/dist/commands/improve/outcome-loop.js +256 -0
  79. package/dist/commands/improve/preparation.js +1966 -0
  80. package/dist/commands/improve/proactive-maintenance.js +115 -0
  81. package/dist/commands/improve/procedural.js +418 -0
  82. package/dist/commands/improve/recombine.js +850 -0
  83. package/dist/commands/improve/reflect-noise.js +0 -0
  84. package/dist/commands/improve/reflect.js +183 -40
  85. package/dist/commands/improve/salience.js +438 -0
  86. package/dist/commands/improve/triage.js +93 -0
  87. package/dist/commands/lint/agent-linter.js +19 -24
  88. package/dist/commands/lint/base-linter.js +173 -60
  89. package/dist/commands/lint/command-linter.js +19 -24
  90. package/dist/commands/lint/env-key-rules.js +38 -1
  91. package/dist/commands/lint/fact-linter.js +39 -0
  92. package/dist/commands/lint/index.js +31 -13
  93. package/dist/commands/lint/memory-linter.js +1 -1
  94. package/dist/commands/lint/registry.js +7 -2
  95. package/dist/commands/lint/task-linter.js +3 -3
  96. package/dist/commands/lint/workflow-linter.js +26 -1
  97. package/dist/commands/proposal/drain-policies.js +5 -0
  98. package/dist/commands/proposal/drain.js +43 -50
  99. package/dist/commands/proposal/proposal-cli.js +21 -31
  100. package/dist/commands/proposal/proposal.js +5 -0
  101. package/dist/commands/proposal/propose.js +7 -2
  102. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  103. package/dist/commands/proposal/validators/proposals.js +189 -63
  104. package/dist/commands/read/curate.js +414 -94
  105. package/dist/commands/read/knowledge.js +6 -3
  106. package/dist/commands/read/search-cli.js +9 -4
  107. package/dist/commands/read/search.js +10 -6
  108. package/dist/commands/read/show.js +86 -7
  109. package/dist/commands/sources/init.js +49 -17
  110. package/dist/commands/sources/installed-stashes.js +11 -3
  111. package/dist/commands/sources/schema-repair.js +43 -45
  112. package/dist/commands/sources/self-update.js +2 -2
  113. package/dist/commands/sources/source-add.js +7 -3
  114. package/dist/commands/sources/stash-cli.js +28 -40
  115. package/dist/commands/sources/stash-skeleton.js +23 -8
  116. package/dist/commands/tasks/tasks-cli.js +19 -27
  117. package/dist/commands/tasks/tasks.js +39 -11
  118. package/dist/commands/wiki-cli.js +21 -35
  119. package/dist/core/asset/asset-registry.js +3 -1
  120. package/dist/core/asset/asset-spec.js +18 -2
  121. package/dist/core/asset/frontmatter.js +166 -167
  122. package/dist/core/asset/markdown.js +8 -0
  123. package/dist/core/authoring-rules.js +92 -0
  124. package/dist/core/common.js +0 -5
  125. package/dist/core/config/config-migration.js +12 -11
  126. package/dist/core/config/config-schema.js +340 -56
  127. package/dist/core/config/config-types.js +3 -3
  128. package/dist/core/config/config.js +28 -7
  129. package/dist/core/events.js +3 -7
  130. package/dist/core/improve-types.js +11 -8
  131. package/dist/core/logs-db.js +10 -66
  132. package/dist/core/parse.js +36 -16
  133. package/dist/core/paths.js +3 -0
  134. package/dist/core/standards/resolve-standards-context.js +87 -0
  135. package/dist/core/standards/resolve-stash-standards.js +99 -0
  136. package/dist/core/standards/resolve-type-conventions.js +66 -0
  137. package/dist/core/state/migrations.js +714 -0
  138. package/dist/core/state-db.js +525 -474
  139. package/dist/indexer/db/db.js +439 -247
  140. package/dist/indexer/db/graph-db.js +129 -86
  141. package/dist/indexer/ensure-index.js +152 -17
  142. package/dist/indexer/graph/graph-boost.js +51 -41
  143. package/dist/indexer/graph/graph-extraction.js +218 -4
  144. package/dist/indexer/index-writer-lock.js +99 -0
  145. package/dist/indexer/indexer.js +123 -221
  146. package/dist/indexer/passes/dir-staleness.js +114 -0
  147. package/dist/indexer/passes/memory-inference.js +13 -5
  148. package/dist/indexer/passes/staleness-detect.js +2 -5
  149. package/dist/indexer/search/db-search.js +19 -6
  150. package/dist/indexer/search/ranking-contributors.js +22 -0
  151. package/dist/indexer/search/ranking.js +4 -0
  152. package/dist/indexer/search/search-source.js +17 -18
  153. package/dist/indexer/search/semantic-status.js +4 -0
  154. package/dist/indexer/walk/matchers.js +9 -0
  155. package/dist/integrations/agent/config.js +6 -53
  156. package/dist/integrations/agent/index.js +2 -18
  157. package/dist/integrations/agent/prompts.js +75 -9
  158. package/dist/integrations/agent/runner-dispatch.js +59 -0
  159. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  160. package/dist/integrations/harnesses/index.js +2 -3
  161. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  162. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  163. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
  164. package/dist/integrations/session-logs/index.js +16 -0
  165. package/dist/llm/client.js +45 -15
  166. package/dist/llm/embedder.js +42 -3
  167. package/dist/llm/embedders/deterministic.js +66 -0
  168. package/dist/llm/embedders/local.js +66 -2
  169. package/dist/llm/feature-gate.js +8 -4
  170. package/dist/llm/graph-extract.js +67 -44
  171. package/dist/llm/memory-infer-impl.js +138 -0
  172. package/dist/llm/memory-infer.js +1 -127
  173. package/dist/llm/metadata-enhance.js +44 -31
  174. package/dist/llm/structured-call.js +49 -0
  175. package/dist/migrate-storage-node.mjs +8 -0
  176. package/dist/output/context.js +5 -5
  177. package/dist/output/renderers.js +74 -2
  178. package/dist/output/shapes/curate.js +14 -2
  179. package/dist/output/shapes/passthrough.js +0 -1
  180. package/dist/output/text/helpers.js +16 -1
  181. package/dist/registry/providers/skills-sh.js +21 -147
  182. package/dist/registry/providers/static-index.js +15 -157
  183. package/dist/registry/resolve.js +22 -9
  184. package/dist/runtime.js +25 -1
  185. package/dist/scripts/migrate-storage.js +2617 -1961
  186. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +759 -510
  187. package/dist/setup/setup.js +29 -8
  188. package/dist/sources/include.js +6 -2
  189. package/dist/sources/providers/filesystem.js +0 -1
  190. package/dist/sources/providers/git-install.js +210 -0
  191. package/dist/sources/providers/git-provider.js +234 -0
  192. package/dist/sources/providers/git-stash.js +248 -0
  193. package/dist/sources/providers/git.js +10 -661
  194. package/dist/sources/providers/npm.js +2 -6
  195. package/dist/sources/providers/provider-utils.js +13 -7
  196. package/dist/sources/providers/sync-from-ref.js +9 -1
  197. package/dist/sources/providers/tar-utils.js +16 -8
  198. package/dist/sources/providers/website.js +9 -5
  199. package/dist/sources/website-ingest.js +187 -29
  200. package/dist/sources/wiki-fetchers/registry.js +53 -0
  201. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  202. package/dist/storage/database.js +45 -10
  203. package/dist/storage/managed-db.js +82 -0
  204. package/dist/storage/repositories/registry-cache.js +92 -0
  205. package/dist/storage/sqlite-pragmas.js +146 -0
  206. package/dist/tasks/backends/cron.js +1 -1
  207. package/dist/tasks/backends/launchd.js +1 -1
  208. package/dist/tasks/backends/schtasks.js +1 -1
  209. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  210. package/dist/tasks/runner.js +5 -13
  211. package/dist/text-import-hook.mjs +0 -0
  212. package/dist/wiki/wiki.js +37 -0
  213. package/dist/workflows/db.js +3 -4
  214. package/dist/workflows/runtime/runs.js +1 -117
  215. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  216. package/dist/workflows/validate-summary.js +2 -7
  217. package/docs/data-and-telemetry.md +3 -2
  218. package/docs/migration/release-notes/0.9.0.md +39 -0
  219. package/package.json +13 -11
  220. package/dist/commands/db-cli.js +0 -23
  221. package/dist/indexer/db/db-backup.js +0 -376
@@ -35,6 +35,7 @@
35
35
  */
36
36
  import fs from "node:fs";
37
37
  import path from "node:path";
38
+ import contradictionJudgeTemplate from "../../../assets/prompts/contradiction-judge.md" with { type: "text" };
38
39
  import { assembleAsset } from "../../../core/asset/asset-serialize.js";
39
40
  import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
40
41
  import { getDefaultLlmConfig } from "../../../core/config/config.js";
@@ -51,6 +52,13 @@ const MAX_FAMILY_SIZE = 8;
51
52
  * families. Prevents runaway LLM usage on stashes with many memories.
52
53
  */
53
54
  const MAX_PAIRS_PER_RUN = 20;
55
+ /**
56
+ * Minimum confidence required to write a contradiction edge. Below this
57
+ * threshold the LLM may be flagging topic-overlap rather than genuine logical
58
+ * exclusivity (investigation 2026-06-18). Absent confidence fields default to
59
+ * 1.0 for backward compatibility with older judge responses.
60
+ */
61
+ const CONTRADICT_CONFIDENCE_THRESHOLD = 0.92;
54
62
  /**
55
63
  * Truncation limit for memory body content sent to the LLM judge.
56
64
  * Keeps prompts compact while preserving the key factual claims.
@@ -58,34 +66,13 @@ const MAX_PAIRS_PER_RUN = 20;
58
66
  const BODY_TRUNCATION = 800;
59
67
  // ── Prompt builder ────────────────────────────────────────────────────────────
60
68
  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");
69
+ return contradictionJudgeTemplate
70
+ .replace("{{A_REF}}", a.ref)
71
+ .replace("{{A_DESCRIPTION}}", a.description || "(none)")
72
+ .replace("{{A_BODY}}", a.body.slice(0, BODY_TRUNCATION))
73
+ .replace("{{B_REF}}", b.ref)
74
+ .replace("{{B_DESCRIPTION}}", b.description || "(none)")
75
+ .replace("{{B_BODY}}", b.body.slice(0, BODY_TRUNCATION));
89
76
  }
90
77
  // ── Filesystem helpers ────────────────────────────────────────────────────────
91
78
  function* walkMarkdownFilesLocal(root) {
@@ -256,6 +243,14 @@ export async function detectAndWriteContradictions(stashDir, config, chat = chat
256
243
  }
257
244
  if (!parsed?.contradicts)
258
245
  continue;
246
+ // Confidence gate: absent field defaults to 1.0 (backward compat with
247
+ // pre-confidence responses). Do NOT default to 0 — that would silently
248
+ // disable all detection during the rollout period.
249
+ const confidence = typeof parsed.confidence === "number" ? parsed.confidence : 1.0;
250
+ if (confidence < CONTRADICT_CONFIDENCE_THRESHOLD) {
251
+ result.warnings.push(`Pair ${a.ref} / ${b.ref}: confidence ${confidence.toFixed(2)} below ${CONTRADICT_CONFIDENCE_THRESHOLD} threshold — skipped.`);
252
+ continue;
253
+ }
259
254
  // Write contradiction edges: both members get contradictedBy pointing to each other.
260
255
  try {
261
256
  const wroteA = writeContradictedByEdge(a.filePath, b.ref);
@@ -0,0 +1,256 @@
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
+ * Weight on the "retrieved-but-never-improved" penalty term. Setting this to
7
+ * 0 degrades to a pure prediction-error score (no quality filter); setting it
8
+ * to 1 heavily penalises assets whose retrievals never led to accepted changes.
9
+ */
10
+ export const OUTCOME_PENALTY_WEIGHT = 0.3;
11
+ /**
12
+ * EMA decay factor for the expected-retrieval rolling mean (α).
13
+ * New expected = α × new_count + (1−α) × old_expected.
14
+ * At α = 0.3 the window is ≈ 3 cycles.
15
+ */
16
+ export const OUTCOME_EMA_ALPHA = 0.3;
17
+ /**
18
+ * Maximum K improve cycles for the eligibility-trace window. Retrievals older
19
+ * than K cycles contribute via the EMA decay naturally (they are already baked
20
+ * into `expected_retrieval_rate`).
21
+ */
22
+ export const OUTCOME_TRACE_CYCLES = 5;
23
+ /**
24
+ * Warm-start cap: the maximum `outcome_score` a brand-new row can be seeded with
25
+ * (from the utility EMA). Prevents a `[0,1]`-range utility value from generating
26
+ * a score that the first negative delta could catastrophically invert. Clipped to
27
+ * the maximum plausible first differential update: `max(0, 1 − 0) = 1`, so cap
28
+ * at a modest 0.3 to avoid spurious rank-flip on first cycle.
29
+ */
30
+ export const WARM_START_CAP = 0.3;
31
+ /**
32
+ * Penalty cap: the minimum outcome_score is capped at this value so a single
33
+ * very-negative run can't send the score to −∞.
34
+ */
35
+ export const OUTCOME_SCORE_MIN = -1.0;
36
+ /**
37
+ * Diversity floor: `outcomeSalience` for any asset is at least this fraction
38
+ * of the maximum observed `outcome_score` in the table, so rare-but-correct
39
+ * assets cannot be permanently outcompeted. 0 = disabled (pure competition).
40
+ */
41
+ export const DIVERSITY_FLOOR_FRACTION = 0.1;
42
+ /**
43
+ * Default review_pressure increment per negative-feedback retrieval cycle.
44
+ * Each cycle where negative_feedback_count > 0, pressure increases by this
45
+ * much (and decreases by REVIEW_PRESSURE_DECAY if no new negatives).
46
+ */
47
+ export const REVIEW_PRESSURE_INCREMENT = 1;
48
+ /**
49
+ * Decay per non-negative cycle. Keeps review_pressure from accumulating forever
50
+ * when an asset's quality problems are addressed and feedback turns positive.
51
+ */
52
+ export const REVIEW_PRESSURE_DECAY = 1;
53
+ /**
54
+ * Upsert one asset's outcome row.
55
+ *
56
+ * On first call for a ref (no prior row): warm-starts from `utilityScore`.
57
+ * On subsequent calls: applies the differential update formula.
58
+ *
59
+ * Returns the resulting `outcome_score` and `review_pressure` so the caller
60
+ * can pass them to `computeSalience` without a second read.
61
+ */
62
+ export function updateAssetOutcome(db, inputs) {
63
+ const now = inputs.now ?? Date.now();
64
+ const valence = inputs.valence ?? 0;
65
+ // Fetch existing row (or undefined for first insert).
66
+ const existing = getAssetOutcome(db, inputs.ref);
67
+ const isNewRow = existing === undefined;
68
+ let outcomeScore;
69
+ let reviewPressure;
70
+ let expectedRetrievalRate;
71
+ if (isNewRow) {
72
+ // ── Warm-start ─────────────────────────────────────────────────────────
73
+ // Seed from utility EMA, clipped to [0, WARM_START_CAP].
74
+ // The warm-start is a non-negative seed; the first real differential update
75
+ // will adjust it up or down from there.
76
+ const seedScore = Math.min(WARM_START_CAP, Math.max(0, inputs.utilityScore ?? 0));
77
+ outcomeScore = seedScore;
78
+ reviewPressure = 0;
79
+ // Seed expected_retrieval_rate = 0 (no delta history yet).
80
+ // Seeding with currentRetrievalCount would produce a large spurious negative
81
+ // prediction error on the first real cycle (delta ≪ cumulative count).
82
+ expectedRetrievalRate = 0;
83
+ }
84
+ else {
85
+ // ── Differential update ────────────────────────────────────────────────
86
+ //
87
+ // retrieval_delta = current − stored (non-negative — we never go backwards)
88
+ const retrievalDelta = Math.max(0, inputs.currentRetrievalCount - existing.retrieval_count);
89
+ // accepted_change_rate = accepted_count / max(1, retrieval_count)
90
+ const acceptedChangeRate = inputs.acceptedChangeCount / Math.max(1, inputs.currentRetrievalCount);
91
+ // Differential prediction-error term:
92
+ // outcome = (retrieval_delta − expected_delta)
93
+ // − PENALTY × retrieval_delta × (1 − accepted_change_rate)
94
+ // + valence
95
+ //
96
+ // Prediction error is computed against the PRIOR stored EMA (before folding
97
+ // in this cycle's observation), so the current delta cannot leak into its own
98
+ // expectation. Negative values are intentional — they signal below-average cycles.
99
+ const expectedDelta = existing.expected_retrieval_rate;
100
+ const predictionError = retrievalDelta - expectedDelta;
101
+ // Advance the EMA over the OBSERVED delta (not the cumulative count).
102
+ // expected' = α × delta + (1−α) × prior_expected
103
+ expectedRetrievalRate =
104
+ OUTCOME_EMA_ALPHA * retrievalDelta + (1 - OUTCOME_EMA_ALPHA) * existing.expected_retrieval_rate;
105
+ const penalty = OUTCOME_PENALTY_WEIGHT * retrievalDelta * (1 - acceptedChangeRate);
106
+ // Running sum (EMA approach): new score = α × update + (1−α) × old
107
+ // so the score tracks the moving signal, not the cumulative sum.
108
+ const rawUpdate = predictionError - penalty + valence;
109
+ const newScore = OUTCOME_EMA_ALPHA * rawUpdate + (1 - OUTCOME_EMA_ALPHA) * existing.outcome_score;
110
+ // Clip to [OUTCOME_SCORE_MIN, +Infinity) — no upper cap so that very-active
111
+ // useful assets can accumulate a high positive score.
112
+ outcomeScore = Math.max(OUTCOME_SCORE_MIN, newScore);
113
+ // ── review_pressure (#613) ─────────────────────────────────────────────
114
+ // New negatives this cycle.
115
+ const newNegatives = Math.max(0, inputs.negativeFeedbackCount - existing.negative_feedback_count);
116
+ if (newNegatives > 0) {
117
+ reviewPressure = Math.min(10, existing.review_pressure + REVIEW_PRESSURE_INCREMENT * newNegatives);
118
+ }
119
+ else {
120
+ // Decay when no new negatives.
121
+ reviewPressure = Math.max(0, existing.review_pressure - REVIEW_PRESSURE_DECAY);
122
+ }
123
+ }
124
+ // Upsert the row.
125
+ db.prepare(`INSERT INTO asset_outcome
126
+ (asset_ref, last_retrieved_at, retrieval_count, expected_retrieval_rate,
127
+ negative_feedback_count, accepted_change_count, review_pressure,
128
+ outcome_score, updated_at)
129
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
130
+ ON CONFLICT(asset_ref) DO UPDATE SET
131
+ last_retrieved_at = excluded.last_retrieved_at,
132
+ retrieval_count = excluded.retrieval_count,
133
+ expected_retrieval_rate= excluded.expected_retrieval_rate,
134
+ negative_feedback_count= excluded.negative_feedback_count,
135
+ accepted_change_count = excluded.accepted_change_count,
136
+ review_pressure = excluded.review_pressure,
137
+ outcome_score = excluded.outcome_score,
138
+ updated_at = excluded.updated_at`).run(inputs.ref, inputs.lastRetrievedAt, inputs.currentRetrievalCount, expectedRetrievalRate, inputs.negativeFeedbackCount, inputs.acceptedChangeCount, reviewPressure, outcomeScore, now);
139
+ return { outcomeScore, reviewPressure, isNewRow };
140
+ }
141
+ // ── Reader ────────────────────────────────────────────────────────────────────
142
+ /**
143
+ * Load the outcome row for one asset, or `undefined` if not yet written.
144
+ */
145
+ export function getAssetOutcome(db, ref) {
146
+ const row = db
147
+ .prepare(`SELECT asset_ref, last_retrieved_at, retrieval_count, expected_retrieval_rate,
148
+ negative_feedback_count, accepted_change_count, review_pressure,
149
+ outcome_score, updated_at
150
+ FROM asset_outcome WHERE asset_ref = ?`)
151
+ .get(ref);
152
+ return row == null ? undefined : row;
153
+ }
154
+ /**
155
+ * Load ALL asset_outcome rows. Used for the proxy-adequacy tripwire computation.
156
+ */
157
+ export function getAllAssetOutcomes(db) {
158
+ return db
159
+ .prepare(`SELECT asset_ref, last_retrieved_at, retrieval_count, expected_retrieval_rate,
160
+ negative_feedback_count, accepted_change_count, review_pressure,
161
+ outcome_score, updated_at
162
+ FROM asset_outcome ORDER BY asset_ref`)
163
+ .all();
164
+ }
165
+ /**
166
+ * Build a Map<ref, outcome_score> for a set of refs in one query.
167
+ * Used by `salience.ts` to populate `outcomeSalience`.
168
+ */
169
+ export function getOutcomeScoresByRef(db, refs) {
170
+ const result = new Map();
171
+ if (refs.length === 0)
172
+ return result;
173
+ const CHUNK = 500;
174
+ for (let i = 0; i < refs.length; i += CHUNK) {
175
+ const chunk = refs.slice(i, i + CHUNK);
176
+ const placeholders = chunk.map(() => "?").join(",");
177
+ const rows = db
178
+ .prepare(`SELECT asset_ref, outcome_score FROM asset_outcome WHERE asset_ref IN (${placeholders})`)
179
+ .all(...chunk);
180
+ for (const row of rows) {
181
+ result.set(row.asset_ref, row.outcome_score);
182
+ }
183
+ }
184
+ return result;
185
+ }
186
+ // ── outcomeSalience projection ────────────────────────────────────────────────
187
+ /**
188
+ * Convert a raw `outcome_score` (differential, may be negative) to a
189
+ * `outcomeSalience` value in [0, 1] suitable for use in the salience projection.
190
+ *
191
+ * Approach:
192
+ * 1. Clip negative scores to 0 (a negative outcome just means "below expected",
193
+ * not "irrelevant"; it should not harm the base retrieval/encoding ranking).
194
+ * 2. Apply the diversity floor so rare-but-correct assets always retain a
195
+ * minimum `outcomeSalience` relative to the stash-wide maximum.
196
+ * 3. Normalise by `maxScore` (the stash-wide max outcome_score) so the term
197
+ * lives in [0, 1]. When maxScore ≤ 0 (all seeds, nothing observed yet),
198
+ * return the floor (or 0 if DIVERSITY_FLOOR_FRACTION = 0).
199
+ *
200
+ * @param outcomeScore - Raw outcome_score from asset_outcome.
201
+ * @param maxScore - Maximum outcome_score across ALL rows (≥ 0 after clip).
202
+ * Callers must compute this once per batch and pass it in.
203
+ */
204
+ export function outcomeScoreToSalience(outcomeScore, maxScore) {
205
+ const clipped = Math.max(0, outcomeScore);
206
+ if (maxScore <= 0) {
207
+ // No positive scores observed yet — return diversity floor.
208
+ return DIVERSITY_FLOOR_FRACTION;
209
+ }
210
+ const normalised = clipped / maxScore;
211
+ // Apply diversity floor.
212
+ return Math.max(DIVERSITY_FLOOR_FRACTION, normalised);
213
+ }
214
+ /**
215
+ * Compute `corr(outcome_score, accepted_change_rate)` across all asset_outcome
216
+ * rows. Returns `{correlation: NaN, n, isInverted: false}` when there is
217
+ * insufficient data (fewer than 3 rows or zero variance in either variable).
218
+ *
219
+ * A NEGATIVE correlation means: assets with a HIGH outcome_score have a LOW
220
+ * accepted_change_rate — i.e. the assets the proxy rates as "doing well"
221
+ * (frequently retrieved) are precisely the ones that rarely yield an accepted
222
+ * improvement. That inverts the proxy: high outcome_score ≠ "doing well", so the
223
+ * coarse retrieval-delta signal is no longer trustworthy and the 0.10+ rich
224
+ * signal is due. (`isInverted = correlation < -0.3`.)
225
+ */
226
+ export function computeProxyAdequacy(rows) {
227
+ const n = rows.length;
228
+ if (n < 3)
229
+ return { correlation: Number.NaN, n, isInverted: false };
230
+ // accepted_change_rate per row.
231
+ const xs = rows.map((r) => r.outcome_score);
232
+ const ys = rows.map((r) => r.accepted_change_count / Math.max(1, r.retrieval_count));
233
+ const meanX = xs.reduce((a, b) => a + b, 0) / n;
234
+ const meanY = ys.reduce((a, b) => a + b, 0) / n;
235
+ let covXY = 0;
236
+ let varX = 0;
237
+ let varY = 0;
238
+ for (let i = 0; i < n; i++) {
239
+ const dx = xs[i] - meanX;
240
+ const dy = ys[i] - meanY;
241
+ covXY += dx * dy;
242
+ varX += dx * dx;
243
+ varY += dy * dy;
244
+ }
245
+ covXY /= n;
246
+ varX /= n;
247
+ varY /= n;
248
+ const denom = Math.sqrt(varX) * Math.sqrt(varY);
249
+ if (denom < 1e-12)
250
+ return { correlation: Number.NaN, n, isInverted: false };
251
+ const correlation = covXY / denom;
252
+ // Inverted proxy: negative correlation between outcome and accepted_change_rate
253
+ // means high-outcome assets are also high-need — the opposite of "useful".
254
+ const isInverted = correlation < -0.3;
255
+ return { correlation, n, isInverted };
256
+ }