akm-cli 0.9.0-rc.2 → 0.9.0-rc.3

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 (88) hide show
  1. package/CHANGELOG.md +34 -8
  2. package/dist/assets/improve-strategies/catchup.json +3 -1
  3. package/dist/assets/improve-strategies/consolidate.json +3 -1
  4. package/dist/assets/improve-strategies/frequent.json +3 -1
  5. package/dist/assets/improve-strategies/graph-refresh.json +3 -1
  6. package/dist/assets/improve-strategies/memory-focus.json +4 -1
  7. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -0
  8. package/dist/assets/improve-strategies/quick.json +3 -1
  9. package/dist/assets/improve-strategies/recombine-only.json +2 -0
  10. package/dist/assets/improve-strategies/reflect-distill.json +1 -0
  11. package/dist/assets/improve-strategies/synthesize.json +2 -0
  12. package/dist/assets/tasks/core/backup.yml +2 -2
  13. package/dist/cli/config-migrate.js +554 -26
  14. package/dist/cli.js +3 -1
  15. package/dist/commands/backup-cli.js +6 -4
  16. package/dist/commands/config-cli.js +10 -3
  17. package/dist/commands/feedback-cli.js +24 -7
  18. package/dist/commands/health/checks.js +94 -15
  19. package/dist/commands/health/surfaces.js +2 -1
  20. package/dist/commands/improve/anti-collapse.js +3 -3
  21. package/dist/commands/improve/collapse-detector.js +5 -5
  22. package/dist/commands/improve/consolidate.js +123 -66
  23. package/dist/commands/improve/distill/promote-memory.js +8 -6
  24. package/dist/commands/improve/distill/quality-gate.js +1 -1
  25. package/dist/commands/improve/distill-guards.js +1 -1
  26. package/dist/commands/improve/distill-promotion-policy.js +32 -26
  27. package/dist/commands/improve/distill.js +52 -36
  28. package/dist/commands/improve/eligibility.js +9 -8
  29. package/dist/commands/improve/extract-prompt.js +2 -2
  30. package/dist/commands/improve/extract.js +43 -11
  31. package/dist/commands/improve/improve-auto-accept.js +14 -28
  32. package/dist/commands/improve/improve-cli.js +4 -2
  33. package/dist/commands/improve/improve-strategies.js +2 -1
  34. package/dist/commands/improve/improve.js +49 -8
  35. package/dist/commands/improve/loop-stages.js +11 -6
  36. package/dist/commands/improve/preparation.js +51 -17
  37. package/dist/commands/improve/procedural.js +10 -6
  38. package/dist/commands/improve/recombine.js +31 -7
  39. package/dist/commands/improve/reflect.js +24 -14
  40. package/dist/commands/improve/salience.js +5 -1
  41. package/dist/commands/improve/source-identity.js +43 -0
  42. package/dist/commands/migrate-cli.js +36 -0
  43. package/dist/commands/mv-cli.js +647 -258
  44. package/dist/commands/proposal/drain.js +16 -4
  45. package/dist/commands/proposal/proposal-cli.js +3 -2
  46. package/dist/commands/proposal/proposal.js +16 -55
  47. package/dist/commands/proposal/repository.js +664 -58
  48. package/dist/commands/read/curate.js +4 -2
  49. package/dist/commands/read/knowledge.js +4 -1
  50. package/dist/commands/read/search.js +6 -2
  51. package/dist/commands/read/show.js +8 -7
  52. package/dist/commands/sources/self-update.js +156 -112
  53. package/dist/commands/sources/sources-cli.js +7 -2
  54. package/dist/core/common.js +36 -3
  55. package/dist/core/config/config-io.js +2 -2
  56. package/dist/core/config/config-schema.js +15 -3
  57. package/dist/core/config/config.js +14 -13
  58. package/dist/core/file-lock.js +2 -2
  59. package/dist/core/migration-backup.js +816 -171
  60. package/dist/core/migration-operation.js +44 -0
  61. package/dist/core/state/migrations.js +4 -7
  62. package/dist/core/state-db.js +22 -7
  63. package/dist/core/write-source.js +86 -18
  64. package/dist/indexer/db/db.js +109 -53
  65. package/dist/indexer/index-writer-lock.js +38 -37
  66. package/dist/indexer/index-written-assets.js +73 -56
  67. package/dist/indexer/indexer.js +5 -1
  68. package/dist/indexer/search/search-source.js +2 -2
  69. package/dist/indexer/usage/usage-events.js +8 -2
  70. package/dist/integrations/agent/engine-resolution.js +14 -7
  71. package/dist/scripts/migrate-storage.js +867 -990
  72. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +405 -566
  73. package/dist/setup/detected-engines.js +7 -13
  74. package/dist/setup/engine-config.js +14 -4
  75. package/dist/setup/setup.js +1 -1
  76. package/dist/setup/steps/connection.js +1 -1
  77. package/dist/setup/steps/tasks.js +1 -1
  78. package/dist/sources/providers/git-stash.js +58 -21
  79. package/dist/sources/providers/git.js +1 -1
  80. package/dist/storage/engines/sqlite-migrations.js +138 -3
  81. package/dist/storage/repositories/events-repository.js +24 -0
  82. package/dist/storage/repositories/proposals-repository.js +14 -0
  83. package/dist/tasks/embedded.js +2 -0
  84. package/dist/workflows/db.js +24 -13
  85. package/dist/workflows/validator.js +24 -2
  86. package/docs/migration/release-notes/0.9.0.md +24 -7
  87. package/docs/migration/v0.8-to-v0.9.md +124 -19
  88. package/package.json +1 -1
@@ -242,11 +242,18 @@ export const configCommand = defineGroupCommand({
242
242
  migrate: defineJsonCommand({
243
243
  meta: {
244
244
  name: "migrate",
245
- description: "Diagnose whether the user config is already in the current schema. Never writes or translates config.",
245
+ description: "Compatibility alias for the canonical top-level migrate status/apply coordinator.",
246
246
  },
247
- async run() {
247
+ args: {
248
+ config: {
249
+ type: "string",
250
+ description: "Path to an operator-prepared current-version config to validate and install",
251
+ },
252
+ dryRun: { type: "boolean", default: false, description: "Validate and report without changing files" },
253
+ },
254
+ async run({ args }) {
248
255
  const { runConfigMigrate } = await import("../cli/config-migrate.js");
249
- await runConfigMigrate();
256
+ await runConfigMigrate({ preparedConfigPath: args.config, dryRun: args.dryRun });
250
257
  },
251
258
  }),
252
259
  enable: defineJsonCommand({
@@ -3,7 +3,7 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
5
  import { defineJsonCommand, output, parseAllFlagValues } from "../cli/shared.js";
6
- import { parseAssetRef } from "../core/asset/asset-ref.js";
6
+ import { parseAssetRef, refToString } from "../core/asset/asset-ref.js";
7
7
  import { assembleAsset } from "../core/asset/asset-serialize.js";
8
8
  import { parseFrontmatter, parseFrontmatterBlock } from "../core/asset/frontmatter.js";
9
9
  import { writeFileAtomic } from "../core/common.js";
@@ -12,7 +12,8 @@ import { UsageError } from "../core/errors.js";
12
12
  import { appendEvent } from "../core/events.js";
13
13
  import { getDbPath } from "../core/paths.js";
14
14
  import { warn } from "../core/warn.js";
15
- import { applyFeedbackToUtilityScore, closeDatabase, findEntryIdByRef, getEntryFilePathById, openExistingDatabase, } from "../indexer/db/db.js";
15
+ import { applyFeedbackToUtilityScore, closeDatabase, findEntryIdByRef, getEntryById, getEntryFilePathById, openExistingDatabase, } from "../indexer/db/db.js";
16
+ import { resolveSourceEntries } from "../indexer/search/search-source.js";
16
17
  import { countFeedbackSignals, insertUsageEvent } from "../indexer/usage/usage-events.js";
17
18
  // ── Tag validation ────────────────────────────────────────────────────────────
18
19
  const TAG_KEY_RE = /^[a-z_][a-z0-9_]*$/;
@@ -156,7 +157,7 @@ export const feedbackCommand = defineJsonCommand({
156
157
  if (!ref) {
157
158
  throw new UsageError("Asset ref is required. Usage: akm feedback <ref> --positive|--negative", "MISSING_REQUIRED_ARGUMENT", "Pass a ref like `skill:deploy` and either --positive or --negative.");
158
159
  }
159
- parseAssetRef(ref);
160
+ const parsedRef = parseAssetRef(ref);
160
161
  if (args.positive && args.negative) {
161
162
  throw new UsageError("Specify either --positive or --negative, not both.");
162
163
  }
@@ -216,9 +217,19 @@ export const feedbackCommand = defineJsonCommand({
216
217
  // background reindex it spawned — now that ensureIndex is removed, holding
217
218
  // the lock only causes feedback to block for the full improve run duration.
218
219
  let utilityResult;
220
+ let durableRef = ref;
219
221
  const db = openExistingDatabase();
220
222
  try {
221
- const entryId = findEntryIdByRef(db, ref);
223
+ const config = loadConfig();
224
+ const sources = resolveSourceEntries(undefined, config);
225
+ const requestedSource = parsedRef.origin
226
+ ? sources.find((source) => source.registryId === parsedRef.origin ||
227
+ ((parsedRef.origin === "local" || parsedRef.origin === "stash") && source === sources[0]))
228
+ : undefined;
229
+ if (parsedRef.origin && !requestedSource) {
230
+ throw new UsageError(`Source "${parsedRef.origin}" is not configured.`, "INVALID_FLAG_VALUE");
231
+ }
232
+ const entryId = findEntryIdByRef(db, ref, requestedSource?.path);
222
233
  if (entryId === undefined) {
223
234
  throw new UsageError(`Ref "${ref}" is not in the index. ` +
224
235
  "Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.");
@@ -228,9 +239,15 @@ export const feedbackCommand = defineJsonCommand({
228
239
  // For negative signals, the score is adjusted the next time `akm index`
229
240
  // runs — the signal is durable in the DB but does NOT suppress ranking
230
241
  // in search results until after reindexing.
242
+ const indexedEntry = getEntryById(db, entryId);
243
+ const source = sources.find((candidate) => candidate.path === indexedEntry?.stashDir);
244
+ durableRef = refToString({
245
+ ...parsedRef,
246
+ origin: parsedRef.origin ?? source?.registryId ?? "stash",
247
+ });
231
248
  insertUsageEvent(db, {
232
249
  event_type: "feedback",
233
- entry_ref: ref,
250
+ entry_ref: durableRef,
234
251
  entry_id: entryId,
235
252
  signal,
236
253
  metadata: metadataStr,
@@ -253,7 +270,7 @@ export const feedbackCommand = defineJsonCommand({
253
270
  }
254
271
  appendEvent({
255
272
  eventType: "feedback",
256
- ref,
273
+ ref: durableRef,
257
274
  metadata: metadataObj,
258
275
  });
259
276
  // F-5 / #386: When a high-utility asset crosses below the review threshold,
@@ -268,7 +285,7 @@ export const feedbackCommand = defineJsonCommand({
268
285
  try {
269
286
  appendEvent({
270
287
  eventType: "improve_review_needed",
271
- ref,
288
+ ref: durableRef,
272
289
  metadata: {
273
290
  previousUtility: utilityResult.previousUtility,
274
291
  nextUtility: utilityResult.nextUtility,
@@ -6,20 +6,25 @@ import { createRequire } from "node:module";
6
6
  import { loadConfig } from "../../core/config/config.js";
7
7
  import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
8
8
  import { resolveModel } from "../../integrations/agent/model-aliases.js";
9
+ import { resolveImprovePlan } from "../improve/improve-strategies.js";
9
10
  import { TASK_FAIL_RATE_WARN, } from "./types.js";
10
11
  const ACTIVE_RUN_WARN_MS = 15 * 60 * 1000;
11
- export function runDefaultEngineProbe(deps = {}) {
12
- const config = deps.loadConfig?.() ?? loadConfig();
13
- const engineName = config.defaults?.engine ?? config.defaults?.llmEngine;
12
+ function credentialAvailable(credential, env) {
13
+ return !credential?.required || credential.names.some((name) => Boolean(env[name]?.trim()));
14
+ }
15
+ function runConfiguredEngineProbe(checkName, engineName, config, deps) {
14
16
  if (!engineName) {
15
17
  return {
16
- name: "default-engine",
18
+ name: checkName,
17
19
  kind: "deterministic",
18
20
  status: "unknown",
19
21
  confidence: "high",
20
- message: "No default engine is configured.",
22
+ message: checkName === "default-llm-engine"
23
+ ? "No default LLM engine is configured."
24
+ : "No default engine is configured.",
21
25
  };
22
26
  }
27
+ const env = deps.env ?? process.env;
23
28
  const configuredEngine = config.engines?.[engineName];
24
29
  if (configuredEngine?.kind === "agent" && configuredEngine.platform === "opencode-sdk") {
25
30
  let packageAvailable = false;
@@ -36,6 +41,7 @@ export function runDefaultEngineProbe(deps = {}) {
36
41
  const binaryAvailable = (version.status ?? 1) === 0;
37
42
  const fallbackEngine = configuredEngine.llmEngine ?? config.defaults?.llmEngine;
38
43
  let fallback;
44
+ let fallbackCredential;
39
45
  let sdkRunner;
40
46
  const resolve = deps.resolveEngine ?? resolveEngine;
41
47
  try {
@@ -48,12 +54,15 @@ export function runDefaultEngineProbe(deps = {}) {
48
54
  }
49
55
  if (sdkRunner?.fallbackConnection) {
50
56
  fallback = { kind: "llm", connection: sdkRunner.fallbackConnection };
57
+ fallbackCredential = sdkRunner.fallbackCredential;
51
58
  }
52
59
  else if (fallbackEngine) {
53
60
  try {
54
61
  const resolved = resolve(fallbackEngine, config);
55
- if (resolved.kind === "llm")
62
+ if (resolved.kind === "llm") {
56
63
  fallback = resolved;
64
+ fallbackCredential = resolved.credential;
65
+ }
57
66
  }
58
67
  catch {
59
68
  fallback = undefined;
@@ -63,14 +72,15 @@ export function runDefaultEngineProbe(deps = {}) {
63
72
  ? resolveModel(configuredEngine.model, "opencode-sdk", configuredEngine.modelAliases, config.modelAliases)
64
73
  : undefined;
65
74
  const effectiveModel = sdkRunner?.profile.model ?? configuredModel ?? fallback?.connection.model;
75
+ const fallbackCredentialAvailable = credentialAvailable(fallbackCredential, env);
66
76
  const missing = [
67
77
  !packageAvailable ? "@opencode-ai/sdk package" : undefined,
68
78
  !binaryAvailable ? `${binary} binary` : undefined,
69
- !fallback ? "fallback LLM connection" : undefined,
70
- !effectiveModel ? "effective SDK model" : undefined,
79
+ fallbackEngine && !fallback ? "configured fallback LLM connection" : undefined,
80
+ !fallbackCredentialAvailable ? "required fallback credential" : undefined,
71
81
  ].filter((value) => value !== undefined);
72
82
  return {
73
- name: "default-engine",
83
+ name: checkName,
74
84
  kind: "deterministic",
75
85
  status: missing.length === 0 ? "pass" : "warn",
76
86
  confidence: "high",
@@ -91,24 +101,29 @@ export function runDefaultEngineProbe(deps = {}) {
91
101
  fallbackEngine: fallbackEngine ?? null,
92
102
  fallbackEndpoint: fallback?.connection.endpoint ?? null,
93
103
  fallbackModel: fallback?.connection.model ?? null,
104
+ requiredCredentialAvailable: fallbackCredentialAvailable,
94
105
  },
95
106
  };
96
107
  }
97
108
  try {
98
109
  const runner = (deps.resolveEngine ?? resolveEngine)(engineName, config);
99
110
  if (runner.kind === "llm") {
111
+ const requiredCredentialAvailable = credentialAvailable(runner.credential, env);
100
112
  return {
101
- name: "default-engine",
113
+ name: checkName,
102
114
  kind: "deterministic",
103
- status: "pass",
115
+ status: requiredCredentialAvailable ? "pass" : "warn",
104
116
  confidence: "high",
105
- message: `LLM engine "${engineName}" is configured.`,
117
+ message: requiredCredentialAvailable
118
+ ? `LLM engine "${engineName}" is configured.`
119
+ : `LLM engine "${engineName}" is configured, but its required credential is unavailable.`,
106
120
  evidence: {
107
121
  engine: engineName,
108
122
  platform: null,
109
123
  runtimeKind: "llm",
110
124
  model: runner.connection.model,
111
125
  endpoint: runner.connection.endpoint,
126
+ requiredCredentialAvailable,
112
127
  },
113
128
  };
114
129
  }
@@ -118,7 +133,7 @@ export function runDefaultEngineProbe(deps = {}) {
118
133
  const version = (deps.spawnSync ?? spawnSync)(profile.bin, ["--version"], { encoding: "utf8", timeout: 5_000 });
119
134
  if ((version.status ?? 1) !== 0) {
120
135
  return {
121
- name: "default-engine",
136
+ name: checkName,
122
137
  kind: "deterministic",
123
138
  status: "warn",
124
139
  confidence: "medium",
@@ -132,7 +147,7 @@ export function runDefaultEngineProbe(deps = {}) {
132
147
  };
133
148
  }
134
149
  return {
135
- name: "default-engine",
150
+ name: checkName,
136
151
  kind: "deterministic",
137
152
  status: "pass",
138
153
  confidence: "high",
@@ -147,7 +162,7 @@ export function runDefaultEngineProbe(deps = {}) {
147
162
  }
148
163
  catch (error) {
149
164
  return {
150
- name: "default-engine",
165
+ name: checkName,
151
166
  kind: "deterministic",
152
167
  status: "warn",
153
168
  confidence: "high",
@@ -155,6 +170,60 @@ export function runDefaultEngineProbe(deps = {}) {
155
170
  };
156
171
  }
157
172
  }
173
+ export function runDefaultEngineProbe(deps = {}) {
174
+ const config = deps.loadConfig?.() ?? loadConfig();
175
+ return runConfiguredEngineProbe("default-engine", config.defaults?.engine, config, deps);
176
+ }
177
+ export function runDefaultLlmEngineProbe(deps = {}) {
178
+ const config = deps.loadConfig?.() ?? loadConfig();
179
+ return runConfiguredEngineProbe("default-llm-engine", config.defaults?.llmEngine, config, deps);
180
+ }
181
+ export function runActiveImproveStrategyProbe(deps = {}) {
182
+ const config = deps.loadConfig?.() ?? loadConfig();
183
+ const strategyName = config.defaults?.improveStrategy ?? "default";
184
+ try {
185
+ const plan = resolveImprovePlan(strategyName, config);
186
+ const env = deps.env ?? process.env;
187
+ const unavailableProcesses = Object.entries(plan.processes).flatMap(([name, process]) => {
188
+ if (!process.enabled || !process.runner)
189
+ return [];
190
+ return credentialAvailable(process.runner.credential, env) ? [] : [name];
191
+ });
192
+ if (plan.triageJudgment) {
193
+ const judgmentCredential = plan.triageJudgment.kind === "llm"
194
+ ? plan.triageJudgment.credential
195
+ : plan.triageJudgment.kind === "sdk"
196
+ ? plan.triageJudgment.fallbackCredential
197
+ : undefined;
198
+ if (!credentialAvailable(judgmentCredential, env))
199
+ unavailableProcesses.push("triage.judgment");
200
+ }
201
+ return {
202
+ name: "active-improve-strategy",
203
+ kind: "deterministic",
204
+ status: unavailableProcesses.length === 0 ? "pass" : "warn",
205
+ confidence: "high",
206
+ message: unavailableProcesses.length === 0
207
+ ? `Active improve strategy "${plan.strategy.name}" has available process engines.`
208
+ : `Active improve strategy "${plan.strategy.name}" has unavailable required credentials for: ${unavailableProcesses.join(", ")}.`,
209
+ evidence: {
210
+ strategy: plan.strategy.name,
211
+ unavailableProcesses,
212
+ },
213
+ };
214
+ }
215
+ catch (error) {
216
+ const explicitlyConfigured = config.defaults?.improveStrategy !== undefined || Object.keys(config.improve?.strategies ?? {}).length > 0;
217
+ return {
218
+ name: "active-improve-strategy",
219
+ kind: "deterministic",
220
+ status: explicitlyConfigured ? "warn" : "unknown",
221
+ confidence: "high",
222
+ message: `Active improve strategy "${strategyName}" is unavailable: ${error instanceof Error ? error.message : String(error)}`,
223
+ evidence: { strategy: strategyName, unavailableProcesses: [] },
224
+ };
225
+ }
226
+ }
158
227
  /**
159
228
  * The ordered health-check registry. ORDER IS LOAD-BEARING: `akmHealth`
160
229
  * iterates this array and appends to hardChecks/advisories in sequence, so the
@@ -236,6 +305,16 @@ export const HEALTH_CHECKS = [
236
305
  channel: "hard",
237
306
  run: () => runDefaultEngineProbe(),
238
307
  },
308
+ {
309
+ name: "default-llm-engine",
310
+ channel: "hard",
311
+ run: () => runDefaultLlmEngineProbe(),
312
+ },
313
+ {
314
+ name: "active-improve-strategy",
315
+ channel: "hard",
316
+ run: () => runActiveImproveStrategyProbe(),
317
+ },
239
318
  {
240
319
  // C2 (13-bus-factor): the cron task-failure rate was computed and rendered
241
320
  // in the HTML report but never surfaced as an advisory, so a sustained
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import fs from "node:fs";
20
20
  import path from "node:path";
21
+ import { MAX_CONFIG_FILE_BYTES, readTextFileWithLimit } from "../../core/common.js";
21
22
  import { CURRENT_CONFIG_VERSION } from "../../core/config/config-schema.js";
22
23
  import { compareConfigVersion } from "../../core/config/config-version.js";
23
24
  const GROUP_OTHER_BITS = 0o077;
@@ -98,7 +99,7 @@ export function collectSecretPermsAdvisory(input, platform = process.platform) {
98
99
  export function collectConfigSkewAdvisory(configPath) {
99
100
  let raw;
100
101
  try {
101
- raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
102
+ raw = JSON.parse(readTextFileWithLimit(configPath, MAX_CONFIG_FILE_BYTES, "Config file"));
102
103
  }
103
104
  catch {
104
105
  return undefined;
@@ -72,8 +72,8 @@ function distinctTokens(text) {
72
72
  }
73
73
  /**
74
74
  * A merge must strictly increase information (R5 §4.2):
75
- * 1. Provenance: the merged asset's `source_refs` must be a superset of the
76
- * union of all participants' `source_refs` plus the participant refs
75
+ * 1. Provenance: the merged asset's `xrefs` must be a superset of the union of
76
+ * all participants' `xrefs` plus the participant refs
77
77
  * themselves — provenance never shrinks through a merge.
78
78
  * 2. Specificity: distinctTokens(mergedBody) ≥ minSpecificityRetention ×
79
79
  * |union(distinctTokens(participant bodies))| — a merge that only
@@ -111,7 +111,7 @@ export function checkMergeInformationFloor(mergedBody, mergedSourceRefs, partici
111
111
  const specificityOk = specificityRetention >= minRetention;
112
112
  const reasons = [];
113
113
  if (!provenanceOk) {
114
- reasons.push(`provenance shrank: merged source_refs missing ${missing.length} ref(s) (e.g. ${missing[0]})`);
114
+ reasons.push(`provenance shrank: merged xrefs missing ${missing.length} ref(s) (e.g. ${missing[0]})`);
115
115
  }
116
116
  if (!specificityOk) {
117
117
  reasons.push(`specificity retention ${specificityRetention.toFixed(2)} < ${minRetention} (merge genericized/shortened)`);
@@ -175,10 +175,10 @@ export function normHash(text) {
175
175
  return hash.toString(16);
176
176
  }
177
177
  /**
178
- * Score one canary against the live index, merge-following via `source_refs`:
179
- * a hit is the anchor ref itself OR any returned entry whose `source_refs`
180
- * frontmatter contains the anchor (ONE level provenance dropped on a
181
- * second-generation merge is a miss by design; that IS the information loss).
178
+ * Score one canary against the live index, merge-following via canonical
179
+ * `xrefs`. Legacy `source_refs` remains readable for assets written before the
180
+ * xref cutover. A hit is the anchor ref itself OR any returned entry whose
181
+ * provenance contains the anchor.
182
182
  * Returns the 0-based rank of the first hit, or -1.
183
183
  */
184
184
  function scoreCanary(indexDb, canary, k) {
@@ -188,7 +188,7 @@ function scoreCanary(indexDb, canary, k) {
188
188
  const ref = makeAssetRef(r.entry.type, r.entry.name);
189
189
  if (ref === canary.anchor_ref)
190
190
  return i;
191
- if (r.entry.sourceRefs?.includes(canary.anchor_ref))
191
+ if (r.entry.xrefs?.includes(canary.anchor_ref) || r.entry.sourceRefs?.includes(canary.anchor_ref))
192
192
  return i;
193
193
  }
194
194
  return -1;