akm-cli 0.9.0-beta.4 → 0.9.0-beta.40

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 (131) hide show
  1. package/CHANGELOG.md +626 -0
  2. package/dist/assets/prompts/consolidate-system.md +23 -0
  3. package/dist/assets/prompts/contradiction-judge.md +33 -0
  4. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  5. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  6. package/dist/assets/prompts/extract-session.md +6 -2
  7. package/dist/assets/prompts/graph-extract-system.md +1 -0
  8. package/dist/assets/prompts/memory-infer-system.md +1 -0
  9. package/dist/assets/prompts/memory-infer-user.md +5 -0
  10. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  11. package/dist/assets/prompts/procedural-system.md +44 -0
  12. package/dist/assets/prompts/recombine-system.md +40 -0
  13. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  14. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +22 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +22 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +24 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +22 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +25 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +21 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +21 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +23 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +22 -0
  24. package/dist/assets/templates/html/health.html +281 -111
  25. package/dist/cli.js +14 -3
  26. package/dist/commands/agent/contribute-cli.js +16 -3
  27. package/dist/commands/feedback-cli.js +15 -6
  28. package/dist/commands/graph/graph.js +75 -71
  29. package/dist/commands/health/checks.js +48 -0
  30. package/dist/commands/health/html-report.js +422 -80
  31. package/dist/commands/health.js +381 -9
  32. package/dist/commands/improve/calibration.js +161 -0
  33. package/dist/commands/improve/consolidate.js +631 -111
  34. package/dist/commands/improve/dedup.js +482 -0
  35. package/dist/commands/improve/distill.js +163 -69
  36. package/dist/commands/improve/encoding-salience.js +205 -0
  37. package/dist/commands/improve/extract-cli.js +115 -1
  38. package/dist/commands/improve/extract-prompt.js +39 -2
  39. package/dist/commands/improve/extract-watch.js +140 -0
  40. package/dist/commands/improve/extract.js +403 -40
  41. package/dist/commands/improve/feedback-valence.js +54 -0
  42. package/dist/commands/improve/homeostatic.js +467 -0
  43. package/dist/commands/improve/improve-auto-accept.js +113 -6
  44. package/dist/commands/improve/improve-profiles.js +12 -0
  45. package/dist/commands/improve/improve.js +2042 -612
  46. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  47. package/dist/commands/improve/outcome-loop.js +256 -0
  48. package/dist/commands/improve/proactive-maintenance.js +115 -0
  49. package/dist/commands/improve/procedural.js +418 -0
  50. package/dist/commands/improve/recombine.js +602 -0
  51. package/dist/commands/improve/reflect-noise.js +0 -0
  52. package/dist/commands/improve/reflect.js +46 -4
  53. package/dist/commands/improve/related-sessions.js +120 -0
  54. package/dist/commands/improve/salience.js +438 -0
  55. package/dist/commands/improve/triage.js +93 -0
  56. package/dist/commands/lint/agent-linter.js +19 -24
  57. package/dist/commands/lint/base-linter.js +173 -60
  58. package/dist/commands/lint/command-linter.js +19 -24
  59. package/dist/commands/lint/env-key-rules.js +34 -1
  60. package/dist/commands/lint/fact-linter.js +39 -0
  61. package/dist/commands/lint/index.js +31 -13
  62. package/dist/commands/lint/memory-linter.js +1 -1
  63. package/dist/commands/lint/registry.js +7 -2
  64. package/dist/commands/lint/task-linter.js +3 -3
  65. package/dist/commands/lint/workflow-linter.js +26 -1
  66. package/dist/commands/proposal/drain-policies.js +5 -0
  67. package/dist/commands/proposal/drain.js +17 -1
  68. package/dist/commands/proposal/proposal.js +5 -0
  69. package/dist/commands/proposal/propose.js +5 -0
  70. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  71. package/dist/commands/proposal/validators/proposals.js +187 -57
  72. package/dist/commands/read/curate.js +344 -80
  73. package/dist/commands/read/search-cli.js +7 -0
  74. package/dist/commands/read/search.js +1 -0
  75. package/dist/commands/read/show.js +67 -2
  76. package/dist/commands/sources/init.js +36 -9
  77. package/dist/commands/sources/installed-stashes.js +5 -1
  78. package/dist/commands/sources/schema-repair.js +13 -1
  79. package/dist/commands/sources/stash-cli.js +19 -3
  80. package/dist/commands/sources/stash-skeleton.js +23 -8
  81. package/dist/core/asset/asset-registry.js +2 -0
  82. package/dist/core/asset/asset-spec.js +14 -0
  83. package/dist/core/asset/frontmatter.js +166 -167
  84. package/dist/core/asset/markdown.js +8 -0
  85. package/dist/core/authoring-rules.js +83 -0
  86. package/dist/core/config/config-schema.js +274 -2
  87. package/dist/core/config/config.js +2 -2
  88. package/dist/core/logs-db.js +4 -3
  89. package/dist/core/paths.js +3 -0
  90. package/dist/core/standards/resolve-standards-context.js +87 -0
  91. package/dist/core/standards/resolve-stash-standards.js +99 -0
  92. package/dist/core/standards/resolve-type-conventions.js +66 -0
  93. package/dist/core/state-db.js +691 -30
  94. package/dist/indexer/db/db.js +364 -38
  95. package/dist/indexer/db/graph-db.js +129 -86
  96. package/dist/indexer/ensure-index.js +152 -17
  97. package/dist/indexer/graph/graph-boost.js +51 -41
  98. package/dist/indexer/graph/graph-extraction.js +203 -3
  99. package/dist/indexer/index-writer-lock.js +99 -0
  100. package/dist/indexer/indexer.js +114 -111
  101. package/dist/indexer/passes/memory-inference.js +10 -3
  102. package/dist/indexer/passes/staleness-detect.js +2 -5
  103. package/dist/indexer/search/db-search.js +15 -4
  104. package/dist/indexer/search/ranking-contributors.js +22 -0
  105. package/dist/indexer/search/ranking.js +4 -0
  106. package/dist/indexer/walk/matchers.js +9 -0
  107. package/dist/integrations/agent/prompts.js +33 -0
  108. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  109. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  110. package/dist/integrations/session-logs/index.js +16 -0
  111. package/dist/llm/client.js +23 -4
  112. package/dist/llm/embedder.js +27 -3
  113. package/dist/llm/embedders/local.js +66 -2
  114. package/dist/llm/feature-gate.js +8 -4
  115. package/dist/llm/graph-extract.js +2 -1
  116. package/dist/llm/memory-infer.js +4 -8
  117. package/dist/llm/metadata-enhance.js +9 -1
  118. package/dist/output/renderers.js +73 -1
  119. package/dist/output/shapes/curate.js +14 -2
  120. package/dist/output/text/helpers.js +16 -1
  121. package/dist/runtime.js +25 -1
  122. package/dist/scripts/migrate-storage.js +1378 -599
  123. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +479 -270
  124. package/dist/setup/setup.js +3 -3
  125. package/dist/sources/providers/tar-utils.js +16 -8
  126. package/dist/storage/sqlite-pragmas.js +146 -0
  127. package/dist/wiki/wiki.js +37 -0
  128. package/dist/workflows/db.js +3 -4
  129. package/dist/workflows/validate-summary.js +2 -7
  130. package/docs/data-and-telemetry.md +1 -0
  131. package/package.json +8 -6
@@ -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
+ }
@@ -0,0 +1,115 @@
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 { computeSalience } from "./salience.js";
5
+ /** One day in milliseconds. */
6
+ const DAY_MS = 86_400_000;
7
+ /** Default staleness gate: an asset is due when last reflected > this many days ago (or never). */
8
+ export const DEFAULT_DUE_DAYS = 30;
9
+ /** Default bound on how many assets the selector surfaces per run. */
10
+ export const DEFAULT_MAX_PER_RUN = 25;
11
+ /** Lower bound on size used in the cost denominator so tiny files don't divide by ~0. */
12
+ const SIZE_FLOOR_BYTES = 200;
13
+ /** Parse the bare asset type out of a `type:name` ref. Returns "" when unparseable. */
14
+ function refType(ref) {
15
+ const i = ref.indexOf(":");
16
+ return i > 0 ? ref.slice(0, i) : "";
17
+ }
18
+ /**
19
+ * Score and select due assets for proactive maintenance.
20
+ *
21
+ * Priority: delegates to `computeSalience(...).rankScore` (WS-1 unified salience
22
+ * vector). The ranking key is therefore identical to every other selector that
23
+ * uses salience — a single formula governs attention across the whole improve loop.
24
+ *
25
+ * DUE gate: an asset is eligible only if it was never reflected OR last
26
+ * reflected/distilled more than `dueDays` ago. The same gate doubles as the
27
+ * ROTATION cooldown — a freshly-reflected asset is excluded until it ages back
28
+ * past `dueDays`, so successive runs rotate through the due pool rather than
29
+ * re-selecting the same heads. Non-due assets never enter the selection.
30
+ */
31
+ export function selectProactiveMaintenanceRefs(params) {
32
+ const now = params.now ?? Date.now();
33
+ const dueDays = params.dueDays ?? DEFAULT_DUE_DAYS;
34
+ const maxPerRun = params.maxPerRun ?? DEFAULT_MAX_PER_RUN;
35
+ const scored = [];
36
+ for (const candidate of params.candidates) {
37
+ const ref = candidate.ref;
38
+ const type = refType(ref);
39
+ // Staleness from the most recent of reflect/distill — either one touching
40
+ // the asset resets its maintenance clock.
41
+ const reflectIso = params.lastReflectTs.get(ref);
42
+ const distillIso = params.lastDistillTs.get(ref);
43
+ let lastTouchMs = 0;
44
+ if (reflectIso)
45
+ lastTouchMs = Math.max(lastTouchMs, Date.parse(reflectIso) || 0);
46
+ if (distillIso)
47
+ lastTouchMs = Math.max(lastTouchMs, Date.parse(distillIso) || 0);
48
+ const neverReflected = lastTouchMs === 0;
49
+ const staleDays = neverReflected ? Number.POSITIVE_INFINITY : (now - lastTouchMs) / DAY_MS;
50
+ // DUE / rotation gate.
51
+ const due = neverReflected || staleDays > dueDays;
52
+ // Retrieval frequency (for salience inputs).
53
+ const retrievalFreq = params.retrievalCounts.get(ref) ?? 0;
54
+ const lastUse = params.lastUseMs?.get(ref) ?? 0;
55
+ // Size proxy (cost): kept for salience input — computeSalience applies
56
+ // the log10 denominator internally.
57
+ let sizeBytes = params.sizeBytesOf?.(candidate) ?? 0;
58
+ if (!sizeBytes || sizeBytes < 0)
59
+ sizeBytes = SIZE_FLOOR_BYTES;
60
+ // Unified priority via WS-1 salience vector (replaces the old inline formula).
61
+ const priority = computeSalience({ ref, type, retrievalFreq, lastUseMs: lastUse, sizeBytes, now }).rankScore;
62
+ scored.push({
63
+ ref: candidate,
64
+ type,
65
+ staleDays,
66
+ neverReflected,
67
+ retrievalFreq,
68
+ sizeBytes,
69
+ priority,
70
+ due,
71
+ });
72
+ }
73
+ const dueScored = scored.filter((s) => s.due);
74
+ const dueTotal = dueScored.length;
75
+ const neverReflected = dueScored.filter((s) => s.neverReflected).length;
76
+ // Rank due assets by composite priority (desc). Ties broken by staleness
77
+ // (older first) then ref string for deterministic ordering.
78
+ const ranked = dueScored.slice().sort((a, b) => {
79
+ if (b.priority !== a.priority)
80
+ return b.priority - a.priority;
81
+ if (b.staleDays !== a.staleDays)
82
+ return b.staleDays - a.staleDays;
83
+ return a.ref.ref < b.ref.ref ? -1 : a.ref.ref > b.ref.ref ? 1 : 0;
84
+ });
85
+ const selected = ranked.slice(0, Math.max(0, maxPerRun)).map((s) => s.ref);
86
+ return { selected, dueTotal, neverReflected, scored };
87
+ }
88
+ /**
89
+ * Post-lock re-filter for proactive refs.
90
+ *
91
+ * Called INSIDE the reflect-distill.lock window with freshly-read timestamp
92
+ * maps so that any `reflect_invoked` events committed by a concurrent run
93
+ * while this run was waiting for the lock are visible. Refs that became
94
+ * non-due (staleDays ≤ dueDays) since planning time are dropped before
95
+ * execution, closing the SELECT-time cooldown race.
96
+ *
97
+ * The logic mirrors the DUE gate in `selectProactiveMaintenanceRefs` — an
98
+ * asset is due only when never touched OR last touched more than `dueDays`
99
+ * ago. The selector and this filter must always agree on the gate predicate.
100
+ */
101
+ export function filterProactiveDue(selected, lastReflectTs, lastDistillTs, dueDays, now) {
102
+ return selected.filter((candidate) => {
103
+ const ref = candidate.ref;
104
+ const reflectIso = lastReflectTs.get(ref);
105
+ const distillIso = lastDistillTs.get(ref);
106
+ let lastTouchMs = 0;
107
+ if (reflectIso)
108
+ lastTouchMs = Math.max(lastTouchMs, Date.parse(reflectIso) || 0);
109
+ if (distillIso)
110
+ lastTouchMs = Math.max(lastTouchMs, Date.parse(distillIso) || 0);
111
+ const neverReflected = lastTouchMs === 0;
112
+ const staleDays = neverReflected ? Number.POSITIVE_INFINITY : (now - lastTouchMs) / DAY_MS;
113
+ return neverReflected || staleDays > dueDays;
114
+ });
115
+ }