akm-cli 0.9.0-beta.36 → 0.9.0-beta.37

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.
@@ -54,7 +54,7 @@ import { DEFAULT_DUE_DAYS, DEFAULT_MAX_PER_RUN, filterProactiveDue, selectProact
54
54
  import { akmProcedural } from "./procedural.js";
55
55
  import { akmRecombine } from "./recombine.js";
56
56
  import { akmReflect } from "./reflect.js";
57
- import { buildRankChangeReport, computeSalience, getAllRankScores, getAssetSalience, getConsecutiveNoOps, getLastUseMsByRef, recordNoOp, resetConsecutiveNoOps, SALIENCE_NO_OP_DAMPEN_FACTOR, SALIENCE_NO_OP_DAMPEN_THRESHOLD, upsertAssetSalience, } from "./salience.js";
57
+ import { buildRankChangeReport, computeSalience, getAllRankScores, getAssetSalience, getConsecutiveNoOps, getLastUseMsByRef, isContentEncodingRow, recordNoOp, resetConsecutiveNoOps, SALIENCE_NO_OP_DAMPEN_FACTOR, SALIENCE_NO_OP_DAMPEN_THRESHOLD, upsertAssetSalience, } from "./salience.js";
58
58
  // #607 Lock Decomposition: fine-grained per-process locks replace the single
59
59
  // `improve.lock`. Three independent locks allow concurrent improve runs when
60
60
  // they touch different subsystems (e.g. quick-shredder consolidate can run
@@ -2733,6 +2733,37 @@ async function runImprovePreparationStage(args) {
2733
2733
  const outcomeWeightEnabled = salienceConfig?.outcomeWeightEnabled === true;
2734
2734
  const salienceMap = new Map();
2735
2735
  const nowForSalience = Date.now();
2736
+ // #644 — preserve content-derived encoding scores across runs.
2737
+ //
2738
+ // Before computing the salience vector, load each ref's stored encoding score
2739
+ // and its provenance. When the stored row carries a genuine content-derived
2740
+ // score (written by the distill path via `scoreEncodingSalience`), pass that
2741
+ // value back in as `inputs.encodingSalience` so `computeSalience` does NOT fall
2742
+ // back to the type-weight stub — keeping both the persisted `encoding_salience`
2743
+ // AND the derived `rank_score` keyed on real novelty/magnitude/prediction-error.
2744
+ // Refs that have never been content-scored keep the type-weight stub fallback.
2745
+ const storedEncodingByRef = new Map();
2746
+ {
2747
+ let dbForStoredEncoding;
2748
+ try {
2749
+ dbForStoredEncoding = openStateDatabase(eventsCtx?.dbPath);
2750
+ for (const r of mergedRefs) {
2751
+ const type = r.ref.includes(":") ? r.ref.slice(0, r.ref.indexOf(":")) : "";
2752
+ const row = getAssetSalience(dbForStoredEncoding, r.ref);
2753
+ if (row && isContentEncodingRow(row, type)) {
2754
+ storedEncodingByRef.set(r.ref, row.encoding_salience);
2755
+ }
2756
+ }
2757
+ }
2758
+ catch (err) {
2759
+ rethrowIfTestIsolationError(err);
2760
+ // best-effort: if DB unavailable, fall back to type-weight stub (prior behaviour)
2761
+ }
2762
+ finally {
2763
+ if (dbForStoredEncoding)
2764
+ dbForStoredEncoding.close();
2765
+ }
2766
+ }
2736
2767
  for (const r of mergedRefs) {
2737
2768
  const type = r.ref.includes(":") ? r.ref.slice(0, r.ref.indexOf(":")) : "";
2738
2769
  const sizeBytes = (() => {
@@ -2746,9 +2777,13 @@ async function runImprovePreparationStage(args) {
2746
2777
  return undefined;
2747
2778
  }
2748
2779
  })();
2780
+ const storedEncoding = storedEncodingByRef.get(r.ref);
2749
2781
  const vector = computeSalience({
2750
2782
  ref: r.ref,
2751
2783
  type,
2784
+ // #644: pass the stored content-derived score (if any) so the type-weight
2785
+ // stub is NOT re-asserted over a real distill-written encoding score.
2786
+ ...(storedEncoding !== undefined ? { encodingSalience: storedEncoding } : {}),
2752
2787
  retrievalFreq: retrievalCounts.get(r.ref) ?? 0,
2753
2788
  lastUseMs: lastUseMsByRef.get(r.ref),
2754
2789
  utilityScore: utilityMap.get(r.ref),
@@ -126,6 +126,7 @@ export function computeSalience(inputs) {
126
126
  // Fall back to the type-importance stub only when the caller has not yet
127
127
  // computed a content-based score (e.g. on older assets before the first
128
128
  // staleness refresh runs).
129
+ const encodingSource = inputs.encodingSalience !== undefined ? "content" : "type-stub";
129
130
  const encoding = inputs.encodingSalience !== undefined
130
131
  ? Math.min(1, Math.max(0, inputs.encodingSalience))
131
132
  : (DEFAULT_TYPE_ENCODING_WEIGHTS[inputs.type] ?? DEFAULT_ENCODING_SALIENCE);
@@ -213,24 +214,75 @@ export function computeSalience(inputs) {
213
214
  }
214
215
  const rawRankScore = (we * encoding + wo * outcome + wr * retrieval) * sizePenalty;
215
216
  const rankScore = Math.min(1, Math.max(0, rawRankScore));
216
- return { encoding, outcome, retrieval, rankScore };
217
+ return { encoding, outcome, retrieval, rankScore, encodingSource };
218
+ }
219
+ /**
220
+ * Does this row carry a genuine content-derived `encoding_salience` (#644)?
221
+ *
222
+ * Returns true when the provenance flag is `"content"`. For legacy rows
223
+ * (`encoding_source === null`, written before migration 015) we apply a
224
+ * conservative heuristic: treat the stored value as content-derived only when it
225
+ * does NOT equal the pure type-weight stub for the asset's type — because before
226
+ * the #644 fix every run overwrote real scores with the stub, a value that still
227
+ * differs from the stub must have been content-written and never re-clobbered.
228
+ * When the type cannot be determined (no `type` given) a null-provenance row is
229
+ * treated as a stub (the safe default).
230
+ */
231
+ export function isContentEncodingRow(row, type) {
232
+ if (row.encoding_source === "content")
233
+ return true;
234
+ if (row.encoding_source === "type-stub")
235
+ return false;
236
+ // Legacy NULL provenance: differ-from-stub heuristic.
237
+ if (!type)
238
+ return false;
239
+ const stub = DEFAULT_TYPE_ENCODING_WEIGHTS[type] ?? DEFAULT_ENCODING_SALIENCE;
240
+ return Math.abs(row.encoding_salience - stub) > 1e-9;
217
241
  }
218
242
  /**
219
243
  * Upsert salience scores for one asset into state.db.
220
244
  *
221
- * Idempotent: safe to call every run; updates all columns on conflict.
245
+ * Idempotent: safe to call every run; updates the outcome / retrieval / rank
246
+ * columns on conflict.
247
+ *
248
+ * #644 — encoding provenance guard: the `encoding_salience` + `encoding_source`
249
+ * columns are NOT lowered from a real content-derived score to a type-weight
250
+ * stub. When the stored row is `encoding_source = 'content'` and the incoming
251
+ * vector is a `type-stub` fallback, the stored encoding score and its provenance
252
+ * are preserved (only the other sub-scores and `rank_score` advance). A `content`
253
+ * write always wins; a `type-stub` write only seeds a row that has no content
254
+ * score yet. This stops the improve loop's type-weight fallback re-asserting the
255
+ * stub over a distill-written score on every run.
256
+ *
257
+ * NOTE: when the guard preserves the stored encoding score, the incoming
258
+ * `vector.rankScore` (computed from the stub encoding) is still written. Callers
259
+ * that want the rank_score to reflect the preserved content score should pass the
260
+ * stored content score back in as `inputs.encodingSalience` to `computeSalience`
261
+ * — which the improve loop does. The guard here is the defensive backstop.
222
262
  */
223
263
  export function upsertAssetSalience(db, ref, vector, now) {
224
264
  const ts = now ?? Date.now();
225
265
  db.prepare(`INSERT INTO asset_salience
226
- (asset_ref, encoding_salience, outcome_salience, retrieval_salience, rank_score, consecutive_no_ops, updated_at)
227
- VALUES (?, ?, ?, ?, ?, 0, ?)
266
+ (asset_ref, encoding_salience, outcome_salience, retrieval_salience, rank_score, consecutive_no_ops, updated_at, encoding_source)
267
+ VALUES (?, ?, ?, ?, ?, 0, ?, ?)
228
268
  ON CONFLICT(asset_ref) DO UPDATE SET
229
- encoding_salience = excluded.encoding_salience,
269
+ -- #644: never lower a real content-derived score to a type-weight stub.
270
+ -- Keep the stored encoding score + provenance when the stored row is
271
+ -- 'content' and the incoming write is a 'type-stub' fallback.
272
+ encoding_salience = CASE
273
+ WHEN asset_salience.encoding_source = 'content' AND excluded.encoding_source = 'type-stub'
274
+ THEN asset_salience.encoding_salience
275
+ ELSE excluded.encoding_salience
276
+ END,
277
+ encoding_source = CASE
278
+ WHEN asset_salience.encoding_source = 'content' AND excluded.encoding_source = 'type-stub'
279
+ THEN asset_salience.encoding_source
280
+ ELSE excluded.encoding_source
281
+ END,
230
282
  outcome_salience = excluded.outcome_salience,
231
283
  retrieval_salience = excluded.retrieval_salience,
232
284
  rank_score = excluded.rank_score,
233
- updated_at = excluded.updated_at`).run(ref, vector.encoding, vector.outcome, vector.retrieval, vector.rankScore, ts);
285
+ updated_at = excluded.updated_at`).run(ref, vector.encoding, vector.outcome, vector.retrieval, vector.rankScore, ts, vector.encodingSource ?? "type-stub");
234
286
  }
235
287
  /**
236
288
  * Load the salience row for one asset, or undefined if not yet computed.
@@ -238,7 +290,7 @@ export function upsertAssetSalience(db, ref, vector, now) {
238
290
  export function getAssetSalience(db, ref) {
239
291
  const row = db
240
292
  .prepare(`SELECT asset_ref, encoding_salience, outcome_salience, retrieval_salience,
241
- rank_score, consecutive_no_ops, updated_at
293
+ rank_score, consecutive_no_ops, updated_at, encoding_source
242
294
  FROM asset_salience WHERE asset_ref = ?`)
243
295
  .get(ref);
244
296
  // Bun SQLite returns null (not undefined) when no row found.
@@ -791,6 +791,31 @@ const MIGRATIONS = [
791
791
  ON recombine_hypotheses(last_seen_at);
792
792
  `,
793
793
  },
794
+ // ── Migration 015 — asset_salience: encoding_source provenance column (#644) ──
795
+ //
796
+ // Records HOW the stored `encoding_salience` was derived so an improve run's
797
+ // type-weight fallback can no longer clobber a real content-derived score:
798
+ //
799
+ // "content" — written by the distill path from `scoreEncodingSalience`
800
+ // (novelty·0.40 + magnitude·0.35 + predictionError·0.25).
801
+ // "type-stub" — written by `computeSalience`'s `DEFAULT_TYPE_ENCODING_WEIGHTS`
802
+ // fallback (no content-based score available for this ref yet).
803
+ // NULL — legacy row written before this migration; provenance unknown.
804
+ // Treated as a stub by readers UNLESS its stored value differs
805
+ // from the pure type-weight (best-effort heuristic for old data).
806
+ //
807
+ // Why a column rather than inference: before #644, every improve run overwrote
808
+ // the distill-written content score with the type-weight stub, so the stored
809
+ // value alone cannot distinguish a real score from a stub. The provenance flag
810
+ // is the single source of truth going forward; `upsertAssetSalience` refuses to
811
+ // lower a "content" row to a "type-stub" so the high-salience gate (#608) keys
812
+ // on genuine novelty/magnitude/prediction-error, not the asset type.
813
+ {
814
+ id: "015-asset-salience-encoding-source",
815
+ up: `
816
+ ALTER TABLE asset_salience ADD COLUMN encoding_source TEXT DEFAULT NULL;
817
+ `,
818
+ },
794
819
  ];
795
820
  /**
796
821
  * Apply every pending migration in a single transaction per migration.
@@ -9978,6 +9978,12 @@ var init_state_db = __esm(() => {
9978
9978
  CREATE INDEX IF NOT EXISTS idx_recombine_hypotheses_last_seen
9979
9979
  ON recombine_hypotheses(last_seen_at);
9980
9980
  `
9981
+ },
9982
+ {
9983
+ id: "015-asset-salience-encoding-source",
9984
+ up: `
9985
+ ALTER TABLE asset_salience ADD COLUMN encoding_source TEXT DEFAULT NULL;
9986
+ `
9981
9987
  }
9982
9988
  ];
9983
9989
  });
@@ -9108,6 +9108,12 @@ var MIGRATIONS = [
9108
9108
  CREATE INDEX IF NOT EXISTS idx_recombine_hypotheses_last_seen
9109
9109
  ON recombine_hypotheses(last_seen_at);
9110
9110
  `
9111
+ },
9112
+ {
9113
+ id: "015-asset-salience-encoding-source",
9114
+ up: `
9115
+ ALTER TABLE asset_salience ADD COLUMN encoding_source TEXT DEFAULT NULL;
9116
+ `
9111
9117
  }
9112
9118
  ];
9113
9119
  function runMigrations2(db) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.9.0-beta.36",
3
+ "version": "0.9.0-beta.37",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
6
6
  "keywords": [