akm-cli 0.9.1-beta.2 → 0.9.1
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.
- package/CHANGELOG.md +16 -0
- package/dist/commands/health/advisories.js +5 -5
- package/dist/commands/health/html-report.js +2 -2
- package/dist/commands/health/metrics.js +38 -22
- package/dist/commands/health/report-view-model.js +1 -1
- package/dist/commands/improve/consolidate.js +61 -9
- package/dist/commands/lint/base-linter.js +93 -20
- package/dist/indexer/indexer.js +17 -3
- package/dist/storage/repositories/salience-repository.js +13 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.1] - 2026-08-18
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Preserve multiline frontmatter descriptions when `akm lint --fix` quotes
|
|
14
|
+
colons, recover already-malformed quoted descriptions, and report a fix only
|
|
15
|
+
when the file actually changes.
|
|
16
|
+
- Skip consolidation promotion proposals whose body already exists in a live
|
|
17
|
+
knowledge asset, preventing exact-content duplicates from recurring in the
|
|
18
|
+
proposal backlog.
|
|
19
|
+
- Derive utility `last_used_at` values only from real user retrieval events
|
|
20
|
+
(`search`, `show`, and `curate`) instead of stamping assets with index time.
|
|
21
|
+
- Compute the salience-distribution health metric over every positive,
|
|
22
|
+
non-missing salience value rather than a top-ranked 100-row slice, and report
|
|
23
|
+
the evaluated sample size.
|
|
24
|
+
|
|
9
25
|
## [0.9.1-beta.2] - 2026-08-17
|
|
10
26
|
|
|
11
27
|
### Breaking changes & migration
|
|
@@ -53,17 +53,17 @@ export function collectImproveAdvisories(db, stateDbPath, since, improveSummary)
|
|
|
53
53
|
"treat outcome-derived rank contributions as noise until a real usage/outcome signal lands.",
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
|
-
// Salience-distribution collapse
|
|
57
|
-
// ranking no longer discriminates between assets.
|
|
56
|
+
// Salience-distribution collapse across all assets with retrieval evidence.
|
|
58
57
|
if (improveSummary.degradation?.salienceUniformityFlagged) {
|
|
58
|
+
const sampleSize = improveSummary.degradation.retrievalSalienceSampleSize;
|
|
59
59
|
advisories.push({
|
|
60
60
|
name: "salience-uniformity-collapse",
|
|
61
61
|
status: "warn",
|
|
62
62
|
kind: "deterministic",
|
|
63
63
|
confidence: "high",
|
|
64
|
-
message: `Salience distribution collapsed toward uniform:
|
|
65
|
-
`${improveSummary.degradation.corpusCentroidDistance} < 0.08
|
|
66
|
-
"Ranking
|
|
64
|
+
message: `Salience distribution collapsed toward uniform: retrieval_salience Gini = ` +
|
|
65
|
+
`${improveSummary.degradation.corpusCentroidDistance} < 0.08 across ${sampleSize} ` +
|
|
66
|
+
"observed, resolvable assets. Ranking carries little discrimination among assets with retrieval evidence.",
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
// Enrichment-vs-minting policy: enrichment lanes edit existing assets;
|
|
@@ -351,8 +351,8 @@ function renderActionItems(vm) {
|
|
|
351
351
|
prio: "P2",
|
|
352
352
|
cls: "warn",
|
|
353
353
|
title: "Salience distribution collapsed: retrieval_salience Gini < 0.08",
|
|
354
|
-
descHtml:
|
|
355
|
-
"ranking
|
|
354
|
+
descHtml: `The ${vm.degradation.retrievalSalienceSampleSize} observed, resolvable salience scores are near-uniform — ` +
|
|
355
|
+
"ranking carries little discrimination among assets with retrieval evidence. " +
|
|
356
356
|
`Corpus diversity proxy: ${esc(String(vm.degradation.corpusCentroidDistance))}.`,
|
|
357
357
|
remedy: "akm health --format json | jq '.improve.degradation'",
|
|
358
358
|
});
|
|
@@ -12,11 +12,23 @@ import { withStateDb } from "../../core/state-db.js";
|
|
|
12
12
|
import { insertEvent } from "../../storage/repositories/events-repository.js";
|
|
13
13
|
import { queryImproveRuns } from "../../storage/repositories/improve-runs-repository.js";
|
|
14
14
|
import { listStateProposals } from "../../storage/repositories/proposals-repository.js";
|
|
15
|
-
import {
|
|
15
|
+
import { getObservedRetrievalSalience } from "../../storage/repositories/salience-repository.js";
|
|
16
16
|
import { roundRate, toFiniteNumber } from "./improve-metrics.js";
|
|
17
17
|
import { ENRICHMENT_LANES, } from "./types.js";
|
|
18
18
|
/** Event type appended + read back by the state.db round-trip probe. */
|
|
19
19
|
const HEALTH_PROBE_EVENT = "health_probe";
|
|
20
|
+
/**
|
|
21
|
+
* Retrieval-salience Gini guardrails.
|
|
22
|
+
*
|
|
23
|
+
* These are distribution-shape thresholds, not quantiles tied to a corpus
|
|
24
|
+
* size. Synthetic anchors pinned in monitor-liveness.test.ts are ~0.01 for a
|
|
25
|
+
* near-uniform two-band distribution, 0.25 for a balanced 0.25/0.75 spread,
|
|
26
|
+
* and ~0.82 for one dominant value among nine 0.01 values. Full-observation
|
|
27
|
+
* production snapshots also sit stably in the neutral band: 0.2613 at n=1,209
|
|
28
|
+
* (2026-07-12) and 0.2506 at n=1,454 (2026-08-17, missing rows excluded).
|
|
29
|
+
*/
|
|
30
|
+
const SALIENCE_UNIFORMITY_GINI_THRESHOLD = 0.08;
|
|
31
|
+
const SALIENCE_ENTRENCHMENT_GINI_THRESHOLD = 0.35;
|
|
20
32
|
/** Synthetic sentinel ref (ref-grammar decision D-R3): a colon-free
|
|
21
33
|
* `<subsystem>/_<marker>` label. `health` has no asset stash-subdir, so
|
|
22
34
|
* `health/_probe` names the subsystem. */
|
|
@@ -204,38 +216,41 @@ export function computeEnrichmentMintingRollup(db, since, until) {
|
|
|
204
216
|
* @param until - Window end (ISO-8601).
|
|
205
217
|
*/
|
|
206
218
|
export function computeDegradationMetrics(db, since, until) {
|
|
207
|
-
// (a) Corpus diversity —
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
219
|
+
// (a) Corpus diversity — distribution of every observed retrieval-salience
|
|
220
|
+
// value for a currently resolvable asset. Zero is the no-observation floor
|
|
221
|
+
// and is excluded: the diagnostic measures discrimination among assets that
|
|
222
|
+
// have a retrieval signal, not corpus coverage.
|
|
223
|
+
//
|
|
224
|
+
// Do not preselect by rank_score here. The old top-100 sample truncated the
|
|
225
|
+
// distribution before measuring it, producing a low Gini even when the full
|
|
226
|
+
// observed corpus had a healthy spread.
|
|
212
227
|
let corpusCentroidDistance = Number.NaN;
|
|
228
|
+
let retrievalSalienceSampleSize = 0;
|
|
213
229
|
let entrenchmentFlagged;
|
|
214
230
|
let salienceUniformityFlagged;
|
|
215
231
|
try {
|
|
216
232
|
// Fail-open: the asset_salience table may not exist yet (pre-WS-1 install).
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const rows = getTopRetrievalSalience(db, 100);
|
|
233
|
+
const rows = getObservedRetrievalSalience(db);
|
|
234
|
+
retrievalSalienceSampleSize = rows.length;
|
|
220
235
|
if (rows.length >= 5) {
|
|
236
|
+
// The repository returns ascending values. Keep a defensive sort so the
|
|
237
|
+
// O(n log n) closed-form Gini remains correct if that contract changes.
|
|
221
238
|
const vals = rows.map((r) => r.retrieval_salience).sort((a, b) => a - b);
|
|
222
239
|
const n = vals.length;
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
const gini = mean > 0 ? sumAbsDiff / (n * n * mean) : 0;
|
|
240
|
+
const sum = vals.reduce((acc, value) => acc + value, 0);
|
|
241
|
+
const weightedSum = vals.reduce((acc, value, index) => acc + (index + 1) * value, 0);
|
|
242
|
+
// Closed-form Gini for sorted non-negative values. This is O(n log n)
|
|
243
|
+
// including the defensive sort; the previous pairwise implementation
|
|
244
|
+
// was O(n²) and only safe because the sample was capped at 100.
|
|
245
|
+
const gini = sum > 0 ? (2 * weightedSum) / (n * sum) - (n + 1) / n : 0;
|
|
230
246
|
// Re-express as a diversity proxy in [0,1]: high gini = low diversity.
|
|
231
247
|
// corpusCentroidDistance approximation: gini is "distance from uniform".
|
|
232
|
-
// Two-tailed:
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
// in this tail under the old one-tailed check).
|
|
248
|
+
// Two-tailed: high concentration flags entrenchment; very low spread
|
|
249
|
+
// flags near-uniformity. Calibration provenance is documented with the
|
|
250
|
+
// constants above and pinned by synthetic distribution tests.
|
|
236
251
|
corpusCentroidDistance = roundRate(gini);
|
|
237
|
-
entrenchmentFlagged = gini >
|
|
238
|
-
salienceUniformityFlagged = gini <
|
|
252
|
+
entrenchmentFlagged = gini > SALIENCE_ENTRENCHMENT_GINI_THRESHOLD;
|
|
253
|
+
salienceUniformityFlagged = gini < SALIENCE_UNIFORMITY_GINI_THRESHOLD;
|
|
239
254
|
}
|
|
240
255
|
}
|
|
241
256
|
catch {
|
|
@@ -305,6 +320,7 @@ export function computeDegradationMetrics(db, since, until) {
|
|
|
305
320
|
}
|
|
306
321
|
return {
|
|
307
322
|
corpusCentroidDistance,
|
|
323
|
+
retrievalSalienceSampleSize,
|
|
308
324
|
entrenchmentFlagged,
|
|
309
325
|
salienceUniformityFlagged,
|
|
310
326
|
mergeFidelityContradictionRate,
|
|
@@ -455,7 +455,7 @@ function buildSummaryRows(aggregates, trend) {
|
|
|
455
455
|
"Corpus diversity (Gini)",
|
|
456
456
|
num(degradation.corpusCentroidDistance),
|
|
457
457
|
degradation.entrenchmentFlagged || degradation.salienceUniformityFlagged ? "down" : "flat",
|
|
458
|
-
|
|
458
|
+
`Gini coefficient of positive retrieval_salience values across ${degradation.retrievalSalienceSampleSize} resolvable assets. Two-tailed: >0.35 = entrenchment risk; <0.08 = collapsed toward uniform.`,
|
|
459
459
|
], [
|
|
460
460
|
"Merge fidelity contradiction rate",
|
|
461
461
|
pct(degradation.mergeFidelityContradictionRate, 1),
|
|
@@ -286,6 +286,46 @@ function loadPendingConsolidateProposalHashes(stashDir) {
|
|
|
286
286
|
}
|
|
287
287
|
return hashes;
|
|
288
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Hash the bodies of live knowledge assets once per consolidation run.
|
|
291
|
+
*
|
|
292
|
+
* Pending-proposal dedup prevents repeated queue entries, but accepted
|
|
293
|
+
* proposals leave that set. Without a live-asset guard, the next run can copy
|
|
294
|
+
* the same memory body into a new knowledge slug indefinitely. Scan the target
|
|
295
|
+
* tree directly (rather than trusting the asynchronously refreshed index) so
|
|
296
|
+
* an already-written asset suppresses recurrence immediately.
|
|
297
|
+
*/
|
|
298
|
+
export function loadExistingKnowledgeBodyHashes(targetRoot) {
|
|
299
|
+
const hashes = new Set();
|
|
300
|
+
const knowledgeRoot = path.join(targetRoot, "knowledge");
|
|
301
|
+
if (!fs.existsSync(knowledgeRoot))
|
|
302
|
+
return hashes;
|
|
303
|
+
const visit = (dir) => {
|
|
304
|
+
let entries;
|
|
305
|
+
try {
|
|
306
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
for (const entry of entries) {
|
|
312
|
+
const entryPath = path.join(dir, entry.name);
|
|
313
|
+
if (entry.isDirectory()) {
|
|
314
|
+
visit(entryPath);
|
|
315
|
+
}
|
|
316
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
317
|
+
try {
|
|
318
|
+
hashes.add(cacheHash(fs.readFileSync(entryPath, "utf8")));
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// An unreadable asset cannot provide reliable duplicate evidence.
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
visit(knowledgeRoot);
|
|
327
|
+
return hashes;
|
|
328
|
+
}
|
|
289
329
|
/** Parse a stored provenance ref and emit its canonical D-R5 display spelling. */
|
|
290
330
|
function canonicalStoredXref(ref) {
|
|
291
331
|
try {
|
|
@@ -976,6 +1016,7 @@ async function akmConsolidateInner(opts, config, stashDir, startMs, warnings, sh
|
|
|
976
1016
|
memoryByRef,
|
|
977
1017
|
promoted,
|
|
978
1018
|
promotedSourceRefs: new Set(),
|
|
1019
|
+
existingKnowledgeBodyHashes: loadExistingKnowledgeBodyHashes(opts.writeTarget.source.path),
|
|
979
1020
|
promotionFailures,
|
|
980
1021
|
warnings,
|
|
981
1022
|
pushSkipReason: accounting.pushSkipReason,
|
|
@@ -1011,8 +1052,26 @@ async function akmConsolidateInner(opts, config, stashDir, startMs, warnings, sh
|
|
|
1011
1052
|
},
|
|
1012
1053
|
});
|
|
1013
1054
|
}
|
|
1055
|
+
/** Reject a promotion when its body already exists in knowledge or the queue. */
|
|
1056
|
+
function shouldSkipPromotionBodyDuplicate(args) {
|
|
1057
|
+
const { bodyHash, op, knowledgeRef, ctx } = args;
|
|
1058
|
+
if (ctx.existingKnowledgeBodyHashes.has(bodyHash)) {
|
|
1059
|
+
ctx.warnings.push(`Skipping promote: identical body already exists in knowledge; skipping duplicate for ${op.ref} → ${knowledgeRef}`);
|
|
1060
|
+
ctx.pushSkipReason("promote", op.ref, "dedup_existing_knowledge");
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
const contentDupProposal = listProposals(ctx.stashDir, { status: "pending" })
|
|
1064
|
+
.filter((proposal) => proposal.source === "consolidate")
|
|
1065
|
+
.find((proposal) => cacheHash(proposalContent(proposal)) === bodyHash);
|
|
1066
|
+
if (!contentDupProposal)
|
|
1067
|
+
return false;
|
|
1068
|
+
ctx.warnings.push(`Skipping promote: identical body already pending as proposal ${contentDupProposal.id} (ref: ${contentDupProposal.ref}); skipping duplicate for ${op.ref} → ${knowledgeRef}`);
|
|
1069
|
+
ctx.pushSkipReason("promote", op.ref, "dedup_pending_proposal");
|
|
1070
|
+
return true;
|
|
1071
|
+
}
|
|
1014
1072
|
/** Execute one reconciled promotion by emitting a reviewable proposal. */
|
|
1015
|
-
|
|
1073
|
+
/** @internal Executes the real proposal-emission path for one promote operation. */
|
|
1074
|
+
export async function emitPromotionProposal(op, ctx) {
|
|
1016
1075
|
const { config, stashDir, sourceRun, target, memoryByRef, warnings, pushSkipReason, promoted, promotedSourceRefs } = ctx;
|
|
1017
1076
|
const entry = memoryByRef.get(op.ref);
|
|
1018
1077
|
if (!entry) {
|
|
@@ -1111,15 +1170,8 @@ async function emitPromotionProposal(op, ctx) {
|
|
|
1111
1170
|
// Use cacheHash (case-preserving stripped body) to match the canonical
|
|
1112
1171
|
// hash domain used by the body-embedding cache and pending-proposal set.
|
|
1113
1172
|
const bodyHash = cacheHash(sourceBody);
|
|
1114
|
-
|
|
1115
|
-
const contentDupProposal = allPendingConsolidateProposals.find((p) => {
|
|
1116
|
-
return cacheHash(proposalContent(p)) === bodyHash;
|
|
1117
|
-
});
|
|
1118
|
-
if (contentDupProposal) {
|
|
1119
|
-
warnings.push(`Skipping promote: identical body already pending as proposal ${contentDupProposal.id} (ref: ${contentDupProposal.ref}); skipping duplicate for ${op.ref} → ${knowledgeRef}`);
|
|
1120
|
-
pushSkipReason("promote", op.ref, "dedup_pending_proposal");
|
|
1173
|
+
if (shouldSkipPromotionBodyDuplicate({ bodyHash, op, knowledgeRef, ctx }))
|
|
1121
1174
|
return;
|
|
1122
|
-
}
|
|
1123
1175
|
try {
|
|
1124
1176
|
// Use LLM-provided description; fall back to memory's own description
|
|
1125
1177
|
// (post-sanitization frontmatter is authoritative).
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
// ----------------------------------------------------------------------------
|
|
34
34
|
import fs from "node:fs";
|
|
35
35
|
import path from "node:path";
|
|
36
|
+
import { isScalar, parseDocument } from "yaml";
|
|
36
37
|
import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
|
|
37
38
|
import { BUNDLE_REF_RE } from "../../core/asset/asset-ref.js";
|
|
38
39
|
import { spliceFrontmatterLine } from "../../core/asset/frontmatter.js";
|
|
@@ -41,26 +42,87 @@ import { typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
|
|
|
41
42
|
import { localDateStamp } from "../../core/common.js";
|
|
42
43
|
import { findFenceRegions } from "./markdown-insertion.js";
|
|
43
44
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
45
|
+
/** Fold physically wrapped prose the same way a YAML plain scalar does. */
|
|
46
|
+
function foldDescriptionLines(lines) {
|
|
47
|
+
let value = "";
|
|
48
|
+
let blankLines = 0;
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (!trimmed) {
|
|
52
|
+
blankLines++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (value)
|
|
56
|
+
value += blankLines > 0 ? "\n".repeat(blankLines) : " ";
|
|
57
|
+
value += trimmed;
|
|
58
|
+
blankLines = 0;
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Recover a description from malformed wrapped YAML.
|
|
64
|
+
*
|
|
65
|
+
* Older producers emitted a valid quoted first physical line followed by
|
|
66
|
+
* indented prose outside the quote. The full document cannot be parsed, but
|
|
67
|
+
* the first line can still be decoded independently and the continuation can
|
|
68
|
+
* be folded without guessing at escape sequences in that first segment.
|
|
69
|
+
*/
|
|
70
|
+
function recoverMalformedDescription(firstSegment, continuation) {
|
|
71
|
+
const firstLine = parseDocument(`description: ${firstSegment}`);
|
|
72
|
+
const firstValue = firstLine.get("description", true);
|
|
73
|
+
const decodedFirst = firstLine.errors.length === 0 && isScalar(firstValue) && typeof firstValue.value === "string"
|
|
74
|
+
? firstValue.value
|
|
75
|
+
: firstSegment.trim();
|
|
76
|
+
const value = foldDescriptionLines([decodedFirst, ...continuation]);
|
|
77
|
+
return value || null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Quote the complete description scalar, including physical continuation
|
|
81
|
+
* lines, and verify that the replacement is valid YAML before returning it.
|
|
82
|
+
*/
|
|
44
83
|
function fixUnquotedColon(raw) {
|
|
84
|
+
const eol = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
45
85
|
const lines = raw.split(/\r?\n/);
|
|
46
86
|
if (lines[0]?.trim() !== "---")
|
|
47
|
-
return
|
|
87
|
+
return null;
|
|
48
88
|
const closeIdx = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
49
89
|
if (closeIdx === -1)
|
|
50
|
-
return
|
|
90
|
+
return null;
|
|
51
91
|
for (let i = 1; i < closeIdx; i++) {
|
|
52
|
-
const m = lines[i]
|
|
92
|
+
const m = lines[i]?.match(/^(description:\s*)(.*)/);
|
|
53
93
|
if (!m)
|
|
54
94
|
continue;
|
|
55
|
-
const prefix = m
|
|
56
|
-
|
|
57
|
-
if ((value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
|
|
58
|
-
(value.startsWith("'") && value.endsWith("'") && value.length >= 2))
|
|
95
|
+
const [, prefix, firstSegment] = m;
|
|
96
|
+
if (prefix === undefined || firstSegment === undefined)
|
|
59
97
|
continue;
|
|
60
|
-
|
|
61
|
-
|
|
98
|
+
let continuationEnd = i + 1;
|
|
99
|
+
while (continuationEnd < closeIdx) {
|
|
100
|
+
const continuationLine = lines[continuationEnd];
|
|
101
|
+
if (continuationLine === undefined || (!/^[ \t]/.test(continuationLine) && continuationLine.trim()))
|
|
102
|
+
break;
|
|
103
|
+
continuationEnd++;
|
|
104
|
+
}
|
|
105
|
+
const frontmatter = lines.slice(1, closeIdx).join("\n");
|
|
106
|
+
const document = parseDocument(frontmatter);
|
|
107
|
+
const description = document.get("description", true);
|
|
108
|
+
const value = document.errors.length === 0 && isScalar(description) && typeof description.value === "string"
|
|
109
|
+
? description.value
|
|
110
|
+
: recoverMalformedDescription(firstSegment.trim(), lines.slice(i + 1, continuationEnd));
|
|
111
|
+
if (value === null)
|
|
112
|
+
return null;
|
|
113
|
+
lines.splice(i, continuationEnd - i, `${prefix}${JSON.stringify(value)}`);
|
|
114
|
+
const candidate = lines.join(eol);
|
|
115
|
+
const candidateCloseIdx = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
116
|
+
const candidateDocument = parseDocument(lines.slice(1, candidateCloseIdx).join("\n"));
|
|
117
|
+
const candidateDescription = candidateDocument.get("description", true);
|
|
118
|
+
if (candidateDocument.errors.length > 0 ||
|
|
119
|
+
!isScalar(candidateDescription) ||
|
|
120
|
+
candidateDescription.value !== value) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return candidate;
|
|
62
124
|
}
|
|
63
|
-
return
|
|
125
|
+
return null;
|
|
64
126
|
}
|
|
65
127
|
function checkMissingUpdated(data, frontmatterText) {
|
|
66
128
|
return frontmatterText !== null && !("updated" in data);
|
|
@@ -473,16 +535,27 @@ export function runBaseChecks(ctx) {
|
|
|
473
535
|
const unquotedColonDetail = checkUnquotedDescriptionColon(ctx.frontmatter);
|
|
474
536
|
if (unquotedColonDetail) {
|
|
475
537
|
if (ctx.fix) {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
538
|
+
const fixedRaw = fixUnquotedColon(currentRaw);
|
|
539
|
+
if (fixedRaw === null) {
|
|
540
|
+
issues.push({
|
|
541
|
+
file: ctx.relPath,
|
|
542
|
+
issue: "unquoted-colon",
|
|
543
|
+
detail: `${unquotedColonDetail} — could not construct a valid YAML replacement`,
|
|
544
|
+
fixed: "failed",
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
currentRaw = fixedRaw;
|
|
549
|
+
modified = true;
|
|
550
|
+
const issue = {
|
|
551
|
+
file: ctx.relPath,
|
|
552
|
+
issue: "unquoted-colon",
|
|
553
|
+
detail: unquotedColonDetail,
|
|
554
|
+
fixed: true,
|
|
555
|
+
};
|
|
556
|
+
issues.push(issue);
|
|
557
|
+
pendingFixes.push(issue);
|
|
558
|
+
}
|
|
486
559
|
}
|
|
487
560
|
else {
|
|
488
561
|
issues.push({
|
package/dist/indexer/indexer.js
CHANGED
|
@@ -1824,7 +1824,12 @@ export function recomputeUtilityScores(db, stateDb) {
|
|
|
1824
1824
|
SUM(CASE WHEN u.event_type = 'show' THEN 1 ELSE 0 END) AS show_count,
|
|
1825
1825
|
SUM(CASE WHEN u.event_type = 'feedback' AND u.signal = 'positive' THEN 1 ELSE 0 END) AS positive_feedback_count,
|
|
1826
1826
|
SUM(CASE WHEN u.event_type = 'feedback' AND u.signal = 'negative' THEN 1 ELSE 0 END) AS negative_feedback_count,
|
|
1827
|
-
MAX(
|
|
1827
|
+
MAX(
|
|
1828
|
+
CASE
|
|
1829
|
+
WHEN u.event_type IN ('search', 'show', 'curate') THEN u.created_at
|
|
1830
|
+
ELSE NULL
|
|
1831
|
+
END
|
|
1832
|
+
) AS last_used_at
|
|
1828
1833
|
FROM usage_events u
|
|
1829
1834
|
WHERE u.entry_id IS NOT NULL
|
|
1830
1835
|
AND u.source = 'user'
|
|
@@ -1839,7 +1844,6 @@ export function recomputeUtilityScores(db, stateDb) {
|
|
|
1839
1844
|
for (const row of scoreRows) {
|
|
1840
1845
|
existingScores.set(row.entry_id, { utility: row.utility, lastUsedAt: row.last_used_at ?? undefined });
|
|
1841
1846
|
}
|
|
1842
|
-
const now = new Date().toISOString();
|
|
1843
1847
|
const entryIds = new Set([...existingScores.keys(), ...usageByEntry.keys()]);
|
|
1844
1848
|
for (const entryId of entryIds) {
|
|
1845
1849
|
const row = usageByEntry.get(entryId) ?? {
|
|
@@ -1857,7 +1861,17 @@ export function recomputeUtilityScores(db, stateDb) {
|
|
|
1857
1861
|
const existing = existingScores.get(row.entry_id);
|
|
1858
1862
|
const prevUtility = existing?.utility ?? 0;
|
|
1859
1863
|
const utility = prevUtility * emaDecay + effectiveRate * emaNew;
|
|
1860
|
-
|
|
1864
|
+
// `utility_scores.last_used_at` is consumed by salience as the timestamp of
|
|
1865
|
+
// the most-recent retrieval. Preserve that meaning by carrying the event's
|
|
1866
|
+
// timestamp through verbatim. The former `effectiveRate > 0.5 ? now : ...`
|
|
1867
|
+
// branch stamped every high-select-rate entry with the index run time,
|
|
1868
|
+
// making unrelated assets look simultaneously fresh and flattening the
|
|
1869
|
+
// recency component of retrieval salience.
|
|
1870
|
+
//
|
|
1871
|
+
// `usage_events` is the source of truth within its retention window. A
|
|
1872
|
+
// missing row therefore clears legacy/index-time stamps on the next index
|
|
1873
|
+
// pass; salience already treats an absent timestamp as long ago.
|
|
1874
|
+
const lastUsedAt = row.last_used_at ?? undefined;
|
|
1861
1875
|
upsertUtilityScore(db, row.entry_id, {
|
|
1862
1876
|
utility,
|
|
1863
1877
|
showCount: row.show_count,
|
|
@@ -107,21 +107,22 @@ export function getConsecutiveNoOps(db, ref) {
|
|
|
107
107
|
}
|
|
108
108
|
// ── New in #672 part 2 ────────────────────────────────────────────────────────
|
|
109
109
|
/**
|
|
110
|
-
* Load
|
|
111
|
-
*
|
|
110
|
+
* Load every observed retrieval-salience value for a currently resolvable
|
|
111
|
+
* asset. Zero is the no-observation floor and is excluded so corpus sparsity
|
|
112
|
+
* does not masquerade as score entrenchment.
|
|
112
113
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* behavior (a named, parameterised repository entry point), not a verbatim
|
|
117
|
-
* move; callers that need fail-open behaviour on a missing table (pre-WS-1
|
|
118
|
-
* installs) must wrap the call in their own try/catch, matching the existing
|
|
119
|
-
* call site.
|
|
114
|
+
* Health computes its distribution diagnostic over this full observed set.
|
|
115
|
+
* Sampling the top-N rows by `rank_score` first truncates the distribution and
|
|
116
|
+
* can make a healthy corpus look artificially uniform.
|
|
120
117
|
*/
|
|
121
|
-
export function
|
|
118
|
+
export function getObservedRetrievalSalience(db) {
|
|
122
119
|
return db
|
|
123
|
-
.prepare(`SELECT retrieval_salience
|
|
124
|
-
|
|
120
|
+
.prepare(`SELECT retrieval_salience
|
|
121
|
+
FROM asset_salience
|
|
122
|
+
WHERE retrieval_salience > 0
|
|
123
|
+
AND missing_since IS NULL
|
|
124
|
+
ORDER BY retrieval_salience ASC`)
|
|
125
|
+
.all();
|
|
125
126
|
}
|
|
126
127
|
/** List every `asset_salience` ref with its current `missing_since` marker. */
|
|
127
128
|
export function listAssetSalienceMissingState(db) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.1
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "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 Cursor.",
|
|
6
6
|
"keywords": [
|