akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
156 lines (155 loc) • 7.85 kB
JavaScript
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/**
* Upsert salience scores for one asset into state.db.
*
* Idempotent: safe to call every run; updates the outcome / retrieval / rank
* columns on conflict.
*
* #644 — encoding provenance guard: the `encoding_salience` + `encoding_source`
* columns are NOT lowered from a real content-derived score to a type-weight
* stub. When the stored row is `encoding_source = 'content'` and the incoming
* vector is a `type-stub` fallback, the stored encoding score and its provenance
* are preserved (only the other sub-scores and `rank_score` advance). A `content`
* write always wins; a `type-stub` write only seeds a row that has no content
* score yet. This stops the improve loop's type-weight fallback re-asserting the
* stub over a distill-written score on every run.
*
* NOTE: when the guard preserves the stored encoding score, the incoming
* `vector.rankScore` (computed from the stub encoding) is still written. Callers
* that want the rank_score to reflect the preserved content score should pass the
* stored content score back in as `inputs.encodingSalience` to `computeSalience`
* — which the improve loop does. The guard here is the defensive backstop.
*/
export function upsertAssetSalience(db, ref, vector, now) {
const ts = now ?? Date.now();
db.prepare(`INSERT INTO asset_salience
(asset_ref, encoding_salience, outcome_salience, retrieval_salience, rank_score, consecutive_no_ops, updated_at, encoding_source)
VALUES (?, ?, ?, ?, ?, 0, ?, ?)
ON CONFLICT(asset_ref) DO UPDATE SET
-- #644: never lower a real content-derived score to a type-weight stub.
-- Keep the stored encoding score + provenance when the stored row is
-- 'content' and the incoming write is a 'type-stub' fallback.
encoding_salience = CASE
WHEN asset_salience.encoding_source = 'content' AND excluded.encoding_source = 'type-stub'
THEN asset_salience.encoding_salience
ELSE excluded.encoding_salience
END,
encoding_source = CASE
WHEN asset_salience.encoding_source = 'content' AND excluded.encoding_source = 'type-stub'
THEN asset_salience.encoding_source
ELSE excluded.encoding_source
END,
outcome_salience = excluded.outcome_salience,
retrieval_salience = excluded.retrieval_salience,
rank_score = excluded.rank_score,
updated_at = excluded.updated_at`).run(ref, vector.encoding, vector.outcome, vector.retrieval, vector.rankScore, ts, vector.encodingSource ?? "type-stub");
}
/**
* Load the salience row for one asset, or undefined if not yet computed.
*/
export function getAssetSalience(db, ref) {
const row = db
.prepare(`SELECT asset_ref, encoding_salience, outcome_salience, retrieval_salience,
rank_score, consecutive_no_ops, updated_at, encoding_source
FROM asset_salience WHERE asset_ref = ?`)
.get(ref);
// Bun SQLite returns null (not undefined) when no row found.
return row == null ? undefined : row;
}
// ── Plasticity helpers ────────────────────────────────────────────────────────
/**
* Increment `consecutive_no_ops` for an asset. Called after a no-op reflect/distill.
* Has NO effect on `rank_score` — the plasticity counter only dampens consolidation
* selection, not retrieval ranking. See plan §WS-1 step 8.
*
* Invariant: recordNoOp must never originate rank_score semantics. If the asset has
* no salience row yet (persistence's best-effort try/catch may have swallowed an
* error), we do nothing — a no-op counter is meaningless without a rank_score row,
* and a synthetic INSERT would fabricate a rank_score=0 entry with nothing behind it.
*/
export function recordNoOp(db, ref) {
db.prepare(`UPDATE asset_salience SET consecutive_no_ops = consecutive_no_ops + 1, updated_at = ? WHERE asset_ref = ?`).run(Date.now(), ref);
// If changes === 0 the asset has no salience row yet — leave the table unchanged.
}
/**
* Reset `consecutive_no_ops` to 0 when an asset produces an accepted change.
* Call after a successful proposal acceptance or detected mutation.
*/
export function resetConsecutiveNoOps(db, ref) {
db.prepare(`UPDATE asset_salience SET consecutive_no_ops = 0, updated_at = ? WHERE asset_ref = ?`).run(Date.now(), ref);
}
/**
* Return the `consecutive_no_ops` count for one asset. 0 when unknown.
*/
export function getConsecutiveNoOps(db, ref) {
const row = db.prepare(`SELECT consecutive_no_ops FROM asset_salience WHERE asset_ref = ?`).get(ref);
return row?.consecutive_no_ops ?? 0;
}
// ── New in #672 part 2 ────────────────────────────────────────────────────────
/**
* Load every observed retrieval-salience value for a currently resolvable
* asset. Zero is the no-observation floor and is excluded so corpus sparsity
* does not masquerade as score entrenchment.
*
* Health computes its distribution diagnostic over this full observed set.
* Sampling the top-N rows by `rank_score` first truncates the distribution and
* can make a healthy corpus look artificially uniform.
*/
export function getObservedRetrievalSalience(db) {
return db
.prepare(`SELECT retrieval_salience
FROM asset_salience
WHERE retrieval_salience > 0
AND missing_since IS NULL
ORDER BY retrieval_salience ASC`)
.all();
}
/** List every `asset_salience` ref with its current `missing_since` marker. */
export function listAssetSalienceMissingState(db) {
return db.prepare(`SELECT asset_ref, missing_since FROM asset_salience`).all();
}
/**
* Stamp `missing_since = now` for the given refs, but ONLY where it is
* currently NULL — a ref already stamped keeps its original timestamp so the
* grace window measures from first sighting, not last. Refs with no row at
* all are silently skipped (0 rows affected). Returns the number of rows
* actually stamped.
*/
export function stampAssetSalienceMissing(db, refs, now) {
if (refs.length === 0)
return 0;
const stmt = db.prepare(`UPDATE asset_salience SET missing_since = ? WHERE asset_ref = ? AND missing_since IS NULL`);
let changed = 0;
for (const ref of refs)
changed += Number(stmt.run(now, ref).changes);
return changed;
}
/**
* Clear `missing_since` for refs that resolved again. Returns the number of
* rows actually cleared.
*/
export function clearAssetSalienceMissing(db, refs) {
if (refs.length === 0)
return 0;
const stmt = db.prepare(`UPDATE asset_salience SET missing_since = NULL WHERE asset_ref = ? AND missing_since IS NOT NULL`);
let changed = 0;
for (const ref of refs)
changed += Number(stmt.run(ref).changes);
return changed;
}
/**
* Delete `asset_salience` rows whose `missing_since` is older than
* `cutoffMs`. A single DELETE — SQLite runs it as one implicit transaction.
* Returns the number of rows deleted.
*/
export function deleteAssetSalienceMissingBefore(db, cutoffMs) {
return Number(db.prepare(`DELETE FROM asset_salience WHERE missing_since IS NOT NULL AND missing_since < ?`).run(cutoffMs)
.changes);
}
/** Count `asset_salience` rows currently marked missing (the live backlog size). */
export function countAssetSalienceMissing(db) {
const row = db.prepare(`SELECT COUNT(*) AS n FROM asset_salience WHERE missing_since IS NOT NULL`).get();
return row.n;
}