akm-cli 0.9.0-beta.54 → 0.9.0-beta.55

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 (101) hide show
  1. package/dist/cli.js +5 -3
  2. package/dist/commands/agent/contribute-cli.js +2 -3
  3. package/dist/commands/env/env-cli.js +187 -202
  4. package/dist/commands/env/secret-cli.js +109 -121
  5. package/dist/commands/feedback-cli.js +152 -155
  6. package/dist/commands/health/advisories.js +151 -0
  7. package/dist/commands/health/improve-metrics.js +754 -0
  8. package/dist/commands/health/llm-usage.js +65 -0
  9. package/dist/commands/health/md-report.js +103 -0
  10. package/dist/commands/health/metrics.js +278 -0
  11. package/dist/commands/health/task-runs.js +135 -0
  12. package/dist/commands/health/types.js +18 -0
  13. package/dist/commands/health/windows.js +196 -0
  14. package/dist/commands/health.js +14 -1624
  15. package/dist/commands/improve/anti-collapse.js +170 -0
  16. package/dist/commands/improve/collapse-detector.js +3 -2
  17. package/dist/commands/improve/consolidate.js +636 -633
  18. package/dist/commands/improve/dedup.js +1 -1
  19. package/dist/commands/improve/distill/content-repair.js +202 -0
  20. package/dist/commands/improve/distill/promote-memory.js +228 -0
  21. package/dist/commands/improve/distill/quality-gate.js +233 -0
  22. package/dist/commands/improve/distill-guards.js +127 -0
  23. package/dist/commands/improve/distill.js +49 -575
  24. package/dist/commands/improve/extract-cli.js +74 -76
  25. package/dist/commands/improve/extract.js +6 -4
  26. package/dist/commands/improve/hot-probation.js +45 -0
  27. package/dist/commands/improve/improve-auto-accept.js +3 -2
  28. package/dist/commands/improve/improve-cli.js +14 -13
  29. package/dist/commands/improve/improve-result-file.js +2 -1
  30. package/dist/commands/improve/improve.js +6 -5
  31. package/dist/commands/improve/loop-stages.js +19 -21
  32. package/dist/commands/improve/preparation.js +4 -2
  33. package/dist/commands/improve/procedural.js +10 -31
  34. package/dist/commands/improve/recombine.js +19 -43
  35. package/dist/commands/improve/reflect.js +1 -1
  36. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  37. package/dist/commands/improve/shared.js +48 -0
  38. package/dist/commands/observability-cli.js +4 -4
  39. package/dist/commands/proposal/drain-policies.js +2 -2
  40. package/dist/commands/proposal/drain.js +1 -1
  41. package/dist/commands/proposal/legacy-import.js +115 -0
  42. package/dist/commands/proposal/proposal-cli.js +3 -3
  43. package/dist/commands/proposal/proposal.js +2 -1
  44. package/dist/commands/proposal/propose.js +1 -1
  45. package/dist/commands/proposal/repository.js +829 -0
  46. package/dist/commands/proposal/validators/proposals.js +5 -920
  47. package/dist/commands/read/remember-cli.js +132 -137
  48. package/dist/commands/read/search-cli.js +1 -1
  49. package/dist/commands/registry-cli.js +76 -87
  50. package/dist/commands/sources/add-cli.js +90 -94
  51. package/dist/commands/sources/history.js +1 -1
  52. package/dist/commands/sources/schema-repair.js +1 -1
  53. package/dist/commands/sources/sources-cli.js +3 -3
  54. package/dist/commands/sources/stash-cli.js +1 -1
  55. package/dist/commands/tasks/tasks-cli.js +1 -2
  56. package/dist/commands/wiki-cli.js +2 -3
  57. package/dist/core/common.js +3 -3
  58. package/dist/core/config/config-schema.js +6 -0
  59. package/dist/core/deep-merge.js +38 -0
  60. package/dist/core/events.js +2 -1
  61. package/dist/core/logs-db.js +8 -13
  62. package/dist/core/paths.js +14 -14
  63. package/dist/core/state-db.js +13 -1140
  64. package/dist/indexer/db/db.js +66 -709
  65. package/dist/indexer/db/entry-mapper.js +41 -0
  66. package/dist/indexer/db/schema.js +516 -0
  67. package/dist/indexer/feedback/utility-policy.js +85 -0
  68. package/dist/indexer/graph/graph-extraction.js +2 -1
  69. package/dist/indexer/index-writer-lock.js +9 -0
  70. package/dist/indexer/indexer.js +78 -23
  71. package/dist/indexer/search/fts-query.js +51 -0
  72. package/dist/integrations/agent/spawn.js +15 -66
  73. package/dist/output/text/helpers.js +13 -0
  74. package/dist/scripts/migrate-storage.js +6891 -7436
  75. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +44 -43
  76. package/dist/setup/legacy-config.js +106 -0
  77. package/dist/setup/prompt.js +57 -0
  78. package/dist/setup/providers.js +14 -0
  79. package/dist/setup/semantic-assets.js +124 -0
  80. package/dist/setup/setup.js +24 -1607
  81. package/dist/setup/steps/connection.js +734 -0
  82. package/dist/setup/steps/output.js +31 -0
  83. package/dist/setup/steps/platforms.js +124 -0
  84. package/dist/setup/steps/semantic.js +27 -0
  85. package/dist/setup/steps/sources.js +222 -0
  86. package/dist/setup/steps/stashdir.js +42 -0
  87. package/dist/setup/steps/tasks.js +152 -0
  88. package/dist/storage/repositories/canaries-repository.js +107 -0
  89. package/dist/storage/repositories/consolidation-repository.js +38 -0
  90. package/dist/storage/repositories/embeddings-repository.js +72 -0
  91. package/dist/storage/repositories/events-repository.js +187 -0
  92. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  93. package/dist/storage/repositories/improve-runs-repository.js +130 -0
  94. package/dist/storage/repositories/index-db.js +4 -7
  95. package/dist/storage/repositories/proposals-repository.js +220 -0
  96. package/dist/storage/repositories/recombine-repository.js +213 -0
  97. package/dist/storage/repositories/task-history-repository.js +93 -0
  98. package/dist/storage/sqlite-pragmas.js +3 -3
  99. package/dist/tasks/runner.js +2 -1
  100. package/package.json +1 -1
  101. package/dist/commands/improve/homeostatic.js +0 -497
@@ -2,8 +2,7 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
- import { defineCommand } from "citty";
6
- import { output, parseAllFlagValues, runWithJsonErrors } from "../cli/shared.js";
5
+ import { defineJsonCommand, output, parseAllFlagValues } from "../cli/shared.js";
7
6
  import { parseAssetRef } from "../core/asset/asset-ref.js";
8
7
  import { assembleAsset } from "../core/asset/asset-serialize.js";
9
8
  import { parseFrontmatter, parseFrontmatterBlock } from "../core/asset/frontmatter.js";
@@ -108,7 +107,7 @@ function appendLessonStrength(type, name, feedbackRef) {
108
107
  return { strength: strengthList.length };
109
108
  }
110
109
  // ── Command definition ────────────────────────────────────────────────────────
111
- export const feedbackCommand = defineCommand({
110
+ export const feedbackCommand = defineJsonCommand({
112
111
  meta: {
113
112
  name: "feedback",
114
113
  description: "Record positive or negative feedback for any indexed stash asset.\n\n" +
@@ -152,173 +151,171 @@ export const feedbackCommand = defineCommand({
152
151
  "`lessonStrength[]` frontmatter array (dedup, idempotent). Ignored on non-lesson targets.",
153
152
  },
154
153
  },
155
- run({ args }) {
156
- return runWithJsonErrors(async () => {
157
- const ref = (args.ref ?? "").trim();
158
- if (!ref) {
159
- 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.");
154
+ async run({ args }) {
155
+ const ref = (args.ref ?? "").trim();
156
+ if (!ref) {
157
+ 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
+ parseAssetRef(ref);
160
+ if (args.positive && args.negative) {
161
+ throw new UsageError("Specify either --positive or --negative, not both.");
162
+ }
163
+ if (!args.positive && !args.negative) {
164
+ throw new UsageError("Specify --positive or --negative.");
165
+ }
166
+ const signal = args.positive ? "positive" : "negative";
167
+ const reason = args.reason;
168
+ // F-3 / #384: Validate --failure-mode against the curated enum.
169
+ const failureMode = args["failure-mode"]?.trim() || undefined;
170
+ if (failureMode) {
171
+ if (args.positive) {
172
+ throw new UsageError("--failure-mode is only valid for negative feedback.", "INVALID_FLAG_VALUE", "Remove --failure-mode or switch to --negative.");
160
173
  }
161
- parseAssetRef(ref);
162
- if (args.positive && args.negative) {
163
- throw new UsageError("Specify either --positive or --negative, not both.");
174
+ const cfg = loadConfig();
175
+ const allowedModes = cfg.feedback?.allowedFailureModes ?? FEEDBACK_FAILURE_MODES;
176
+ if (allowedModes.length > 0 && !allowedModes.includes(failureMode)) {
177
+ throw new UsageError(`Invalid --failure-mode "${failureMode}". Accepted values: ${allowedModes.join(", ")}.`, "INVALID_FLAG_VALUE", `Use one of: ${allowedModes.join(", ")}`);
164
178
  }
165
- if (!args.positive && !args.negative) {
166
- throw new UsageError("Specify --positive or --negative.");
179
+ }
180
+ if (args.negative === true && !reason?.trim()) {
181
+ // F-3 / #384: Default requireReason is now true. Load config to allow
182
+ // operators to opt out via feedback.requireReason: false in akm.json.
183
+ const cfg = loadConfig();
184
+ const requireReason = cfg.feedback?.requireReason ?? true; // Default: true (F-3 / #384)
185
+ if (requireReason) {
186
+ throw new UsageError("Negative feedback requires --reason (structured failure signals are needed for distillation). " +
187
+ "Use --failure-mode for a curated taxonomy or --reason for free text. " +
188
+ "Set feedback.requireReason: false in akm.json to downgrade to a warning.", "MISSING_REQUIRED_ARGUMENT", `Hint: akm feedback ${ref} --negative --reason "..." [--failure-mode incorrect|outdated|dangerous|incomplete|redundant]`);
167
189
  }
168
- const signal = args.positive ? "positive" : "negative";
169
- const reason = args.reason;
170
- // F-3 / #384: Validate --failure-mode against the curated enum.
171
- const failureMode = args["failure-mode"]?.trim() || undefined;
172
- if (failureMode) {
173
- if (args.positive) {
174
- throw new UsageError("--failure-mode is only valid for negative feedback.", "INVALID_FLAG_VALUE", "Remove --failure-mode or switch to --negative.");
175
- }
176
- const cfg = loadConfig();
177
- const allowedModes = cfg.feedback?.allowedFailureModes ?? FEEDBACK_FAILURE_MODES;
178
- if (allowedModes.length > 0 && !allowedModes.includes(failureMode)) {
179
- throw new UsageError(`Invalid --failure-mode "${failureMode}". Accepted values: ${allowedModes.join(", ")}.`, "INVALID_FLAG_VALUE", `Use one of: ${allowedModes.join(", ")}`);
180
- }
190
+ else {
191
+ warn("Warning: negative feedback without --reason provides less distillation signal.");
181
192
  }
182
- if (args.negative === true && !reason?.trim()) {
183
- // F-3 / #384: Default requireReason is now true. Load config to allow
184
- // operators to opt out via feedback.requireReason: false in akm.json.
185
- const cfg = loadConfig();
186
- const requireReason = cfg.feedback?.requireReason ?? true; // Default: true (F-3 / #384)
187
- if (requireReason) {
188
- throw new UsageError("Negative feedback requires --reason (structured failure signals are needed for distillation). " +
189
- "Use --failure-mode for a curated taxonomy or --reason for free text. " +
190
- "Set feedback.requireReason: false in akm.json to downgrade to a warning.", "MISSING_REQUIRED_ARGUMENT", `Hint: akm feedback ${ref} --negative --reason "..." [--failure-mode incorrect|outdated|dangerous|incomplete|redundant]`);
191
- }
192
- else {
193
- warn("Warning: negative feedback without --reason provides less distillation signal.");
194
- }
193
+ }
194
+ const rawTags = parseAllFlagValues("--tag");
195
+ const validatedTags = validateFeedbackTags(rawTags);
196
+ const metadataObj = {
197
+ signal,
198
+ ...(reason?.trim() ? { reason: reason.trim() } : {}),
199
+ ...(failureMode ? { failureMode } : {}),
200
+ ...(validatedTags.length > 0 ? { tags: validatedTags } : {}),
201
+ };
202
+ const metadataStr = Object.keys(metadataObj).length > 1 ? JSON.stringify(metadataObj) : undefined;
203
+ // Feedback only needs the index to exist, not to be current. A stale index
204
+ // is fine the ref lookup works against any populated DB. We do NOT call
205
+ // ensureIndex here: it either blocks (3+ min inline reindex) or spawns a
206
+ // background process that holds the writer lock, causing the feedback write
207
+ // to spin-wait for the full reindex duration. If the DB is absent we give a
208
+ // clear error below rather than silently triggering a rebuild.
209
+ if (!fs.existsSync(getDbPath())) {
210
+ throw new UsageError("Index not found. Run 'akm index' first to build the index before recording feedback.", "MISSING_REQUIRED_ARGUMENT", "akm index");
211
+ }
212
+ // Feedback writes exactly 2 rows (usage_events + utility_score). SQLite
213
+ // WAL mode + busy_timeout=30s handles concurrent access with an ongoing
214
+ // `akm improve` run without needing the application-level writer lock.
215
+ // The lock was originally needed to prevent feedback from racing a
216
+ // background reindex it spawned — now that ensureIndex is removed, holding
217
+ // the lock only causes feedback to block for the full improve run duration.
218
+ let utilityResult;
219
+ const db = openExistingDatabase();
220
+ try {
221
+ const entryId = findEntryIdByRef(db, ref);
222
+ if (entryId === undefined) {
223
+ throw new UsageError(`Ref "${ref}" is not in the index. ` +
224
+ "Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.");
195
225
  }
196
- const rawTags = parseAllFlagValues("--tag");
197
- const validatedTags = validateFeedbackTags(rawTags);
198
- const metadataObj = {
226
+ // Persist the feedback signal into usage_events. For positive signals,
227
+ // the EMA utility score is updated immediately on the next read path.
228
+ // For negative signals, the score is adjusted the next time `akm index`
229
+ // runs — the signal is durable in the DB but does NOT suppress ranking
230
+ // in search results until after reindexing.
231
+ insertUsageEvent(db, {
232
+ event_type: "feedback",
233
+ entry_ref: ref,
234
+ entry_id: entryId,
199
235
  signal,
200
- ...(reason?.trim() ? { reason: reason.trim() } : {}),
201
- ...(failureMode ? { failureMode } : {}),
202
- ...(validatedTags.length > 0 ? { tags: validatedTags } : {}),
203
- };
204
- const metadataStr = Object.keys(metadataObj).length > 1 ? JSON.stringify(metadataObj) : undefined;
205
- // Feedback only needs the index to exist, not to be current. A stale index
206
- // is fine the ref lookup works against any populated DB. We do NOT call
207
- // ensureIndex here: it either blocks (3+ min inline reindex) or spawns a
208
- // background process that holds the writer lock, causing the feedback write
209
- // to spin-wait for the full reindex duration. If the DB is absent we give a
210
- // clear error below rather than silently triggering a rebuild.
211
- if (!fs.existsSync(getDbPath())) {
212
- throw new UsageError("Index not found. Run 'akm index' first to build the index before recording feedback.", "MISSING_REQUIRED_ARGUMENT", "akm index");
236
+ metadata: metadataStr,
237
+ });
238
+ // Apply feedback-derived utility score adjustment immediately so that
239
+ // positive/negative signals influence search ranking without requiring
240
+ // a full reindex. We query the total accumulated feedback counts from
241
+ // usage_events so the delta reflects the entire signal history.
242
+ // Uses MemRL bounded-step EMA (F-5 / #386, arXiv:2601.03192).
243
+ try {
244
+ const { pos, neg } = countFeedbackSignals(db, entryId);
245
+ utilityResult = applyFeedbackToUtilityScore(db, entryId, pos, neg);
213
246
  }
214
- // Feedback writes exactly 2 rows (usage_events + utility_score). SQLite
215
- // WAL mode + busy_timeout=30s handles concurrent access with an ongoing
216
- // `akm improve` run without needing the application-level writer lock.
217
- // The lock was originally needed to prevent feedback from racing a
218
- // background reindex it spawned — now that ensureIndex is removed, holding
219
- // the lock only causes feedback to block for the full improve run duration.
220
- let utilityResult;
221
- const db = openExistingDatabase();
247
+ catch {
248
+ // best-effort feedback recording succeeds even if utility update fails
249
+ }
250
+ }
251
+ finally {
252
+ closeDatabase(db);
253
+ }
254
+ appendEvent({
255
+ eventType: "feedback",
256
+ ref,
257
+ metadata: metadataObj,
258
+ });
259
+ // F-5 / #386: When a high-utility asset crosses below the review threshold,
260
+ // auto-create a review-needed escalation proposal so a human can confirm
261
+ // whether the negative feedback is valid before the asset falls out of
262
+ // the improve loop. Best-effort — failure is logged but does not fail the
263
+ // feedback command.
264
+ // Emit a structured event rather than a proposal so the review-needed
265
+ // signal is queryable via `akm events list --type improve_review_needed`
266
+ // without risking accidental asset overwrite if the proposal is accepted.
267
+ if (utilityResult?.crossedReviewThreshold) {
222
268
  try {
223
- const entryId = findEntryIdByRef(db, ref);
224
- if (entryId === undefined) {
225
- throw new UsageError(`Ref "${ref}" is not in the index. ` +
226
- "Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.");
227
- }
228
- // Persist the feedback signal into usage_events. For positive signals,
229
- // the EMA utility score is updated immediately on the next read path.
230
- // For negative signals, the score is adjusted the next time `akm index`
231
- // runs — the signal is durable in the DB but does NOT suppress ranking
232
- // in search results until after reindexing.
233
- insertUsageEvent(db, {
234
- event_type: "feedback",
235
- entry_ref: ref,
236
- entry_id: entryId,
237
- signal,
238
- metadata: metadataStr,
269
+ appendEvent({
270
+ eventType: "improve_review_needed",
271
+ ref,
272
+ metadata: {
273
+ previousUtility: utilityResult.previousUtility,
274
+ nextUtility: utilityResult.nextUtility,
275
+ reason: reason?.trim() ?? null,
276
+ failureMode: failureMode ?? null,
277
+ },
239
278
  });
240
- // Apply feedback-derived utility score adjustment immediately so that
241
- // positive/negative signals influence search ranking without requiring
242
- // a full reindex. We query the total accumulated feedback counts from
243
- // usage_events so the delta reflects the entire signal history.
244
- // Uses MemRL bounded-step EMA (F-5 / #386, arXiv:2601.03192).
245
- try {
246
- const { pos, neg } = countFeedbackSignals(db, entryId);
247
- utilityResult = applyFeedbackToUtilityScore(db, entryId, pos, neg);
248
- }
249
- catch {
250
- // best-effort — feedback recording succeeds even if utility update fails
251
- }
252
279
  }
253
- finally {
254
- closeDatabase(db);
255
- }
256
- appendEvent({
257
- eventType: "feedback",
258
- ref,
259
- metadata: metadataObj,
260
- });
261
- // F-5 / #386: When a high-utility asset crosses below the review threshold,
262
- // auto-create a review-needed escalation proposal so a human can confirm
263
- // whether the negative feedback is valid before the asset falls out of
264
- // the improve loop. Best-effort — failure is logged but does not fail the
265
- // feedback command.
266
- // Emit a structured event rather than a proposal so the review-needed
267
- // signal is queryable via `akm events list --type improve_review_needed`
268
- // without risking accidental asset overwrite if the proposal is accepted.
269
- if (utilityResult?.crossedReviewThreshold) {
270
- try {
271
- appendEvent({
272
- eventType: "improve_review_needed",
273
- ref,
274
- metadata: {
275
- previousUtility: utilityResult.previousUtility,
276
- nextUtility: utilityResult.nextUtility,
277
- reason: reason?.trim() ?? null,
278
- failureMode: failureMode ?? null,
279
- },
280
- });
281
- }
282
- catch (escalationErr) {
283
- warn(`[feedback] Could not emit review-needed event for ${ref}: ${escalationErr instanceof Error ? escalationErr.message : String(escalationErr)}`);
284
- }
280
+ catch (escalationErr) {
281
+ warn(`[feedback] Could not emit review-needed event for ${ref}: ${escalationErr instanceof Error ? escalationErr.message : String(escalationErr)}`);
285
282
  }
286
- // Phase 7A / Advantage D4b: --applied-to credits a lesson. When the
287
- // target is a `lesson:<name>` ref and the signal is positive, append
288
- // the feedback ref to the target lesson's `lessonStrength[]`
289
- // frontmatter array (dedup, idempotent). Non-lesson targets are
290
- // ignored. Failures here are warnings feedback recording is the
291
- // primary contract and must not regress on lesson-write errors.
292
- const appliedToRaw = args["applied-to"]?.trim();
293
- let appliedToResult = null;
294
- if (appliedToRaw && signal === "positive") {
295
- try {
296
- const parsedApplied = parseAssetRef(appliedToRaw);
297
- if (parsedApplied.type === "lesson") {
298
- const updated = appendLessonStrength(parsedApplied.type, parsedApplied.name, ref);
299
- if (updated) {
300
- appliedToResult = { lessonRef: appliedToRaw, strength: updated.strength };
301
- }
283
+ }
284
+ // Phase 7A / Advantage D4b: --applied-to credits a lesson. When the
285
+ // target is a `lesson:<name>` ref and the signal is positive, append
286
+ // the feedback ref to the target lesson's `lessonStrength[]`
287
+ // frontmatter array (dedup, idempotent). Non-lesson targets are
288
+ // ignored. Failures here are warnings feedback recording is the
289
+ // primary contract and must not regress on lesson-write errors.
290
+ const appliedToRaw = args["applied-to"]?.trim();
291
+ let appliedToResult = null;
292
+ if (appliedToRaw && signal === "positive") {
293
+ try {
294
+ const parsedApplied = parseAssetRef(appliedToRaw);
295
+ if (parsedApplied.type === "lesson") {
296
+ const updated = appendLessonStrength(parsedApplied.type, parsedApplied.name, ref);
297
+ if (updated) {
298
+ appliedToResult = { lessonRef: appliedToRaw, strength: updated.strength };
302
299
  }
303
300
  }
304
- catch (err) {
305
- warn(`[feedback] --applied-to failed for ${appliedToRaw}: ${err instanceof Error ? err.message : String(err)}`);
306
- }
307
301
  }
308
- else if (appliedToRaw && signal !== "positive") {
309
- warn("[feedback] --applied-to is ignored without --positive; lesson credit is only recorded on positive signals.");
302
+ catch (err) {
303
+ warn(`[feedback] --applied-to failed for ${appliedToRaw}: ${err instanceof Error ? err.message : String(err)}`);
310
304
  }
311
- output("feedback", {
312
- ok: true,
313
- ref,
314
- signal,
315
- reason: reason?.trim() ?? null,
316
- failureMode: failureMode ?? null,
317
- tags: validatedTags,
318
- ...(appliedToResult
319
- ? { appliedTo: { ref: appliedToResult.lessonRef, lessonStrength: appliedToResult.strength } }
320
- : {}),
321
- });
305
+ }
306
+ else if (appliedToRaw && signal !== "positive") {
307
+ warn("[feedback] --applied-to is ignored without --positive; lesson credit is only recorded on positive signals.");
308
+ }
309
+ output("feedback", {
310
+ ok: true,
311
+ ref,
312
+ signal,
313
+ reason: reason?.trim() ?? null,
314
+ failureMode: failureMode ?? null,
315
+ tags: validatedTags,
316
+ ...(appliedToResult
317
+ ? { appliedTo: { ref: appliedToResult.lessonRef, lessonStrength: appliedToResult.strength } }
318
+ : {}),
322
319
  });
323
320
  },
324
321
  });
@@ -0,0 +1,151 @@
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
+ /**
5
+ * Improve-pipeline advisories for `akm health`: projects the computed
6
+ * {@link ImproveHealthMetrics} plus a few direct event reads into the
7
+ * ordered advisory list.
8
+ */
9
+ import { readEvents } from "../../core/events.js";
10
+ import { getLatestCycleMetrics } from "../../storage/repositories/canaries-repository.js";
11
+ import { ENRICHMENT_MINTED_FAIL_SHARE, ENRICHMENT_MINTED_WARN_SHARE, } from "./types.js";
12
+ /**
13
+ * Build the improve-pipeline advisories for the health window from the already
14
+ * computed {@link ImproveHealthMetrics} plus a few direct event reads. Pure
15
+ * projection of state → advisories; emission order is preserved so the health
16
+ * report is byte-identical to the previous inline construction.
17
+ */
18
+ export function collectImproveAdvisories(db, stateDbPath, since, improveSummary) {
19
+ const advisories = [];
20
+ // WS-2 proxy-adequacy tripwire: surface any outcome_proxy_inverted events
21
+ // in the health window as an advisory so operators know when the 0.10+
22
+ // rich in-session signal is no longer deferrable.
23
+ const proxyInvertedEvents = readEvents({ since, type: "outcome_proxy_inverted" }, { dbPath: stateDbPath }).events;
24
+ if (proxyInvertedEvents.length > 0) {
25
+ const lastEvent = proxyInvertedEvents[proxyInvertedEvents.length - 1];
26
+ const correlation = typeof lastEvent.metadata?.correlation === "number" ? lastEvent.metadata.correlation.toFixed(3) : "unknown";
27
+ advisories.push({
28
+ name: "outcome-proxy-adequacy",
29
+ status: "warn",
30
+ kind: "deterministic",
31
+ confidence: "high",
32
+ message: `WS-2 outcome proxy inverted (${proxyInvertedEvents.length} event(s) in window). ` +
33
+ `corr(outcome_score, accepted_change_rate) = ${correlation} < −0.3. ` +
34
+ "Popular assets are also the most-needing-improvement assets — " +
35
+ "the retrieval-based proxy is inverted. " +
36
+ "The 0.10+ rich in-session outcome signal is no longer deferrable. See plan §WS-2.",
37
+ });
38
+ }
39
+ // Two-tailed companion: a proxy that decays to noise (|corr| < 0.1 at scale)
40
+ // is as much a failure as an inverted one — it just fails silently.
41
+ const proxyDeadEvents = readEvents({ since, type: "outcome_proxy_dead" }, { dbPath: stateDbPath, db }).events;
42
+ if (proxyDeadEvents.length > 0) {
43
+ const lastEvent = proxyDeadEvents[proxyDeadEvents.length - 1];
44
+ const correlation = typeof lastEvent.metadata?.correlation === "number" ? lastEvent.metadata.correlation.toFixed(3) : "unknown";
45
+ advisories.push({
46
+ name: "outcome-proxy-dead",
47
+ status: "warn",
48
+ kind: "deterministic",
49
+ confidence: "high",
50
+ message: `WS-2 outcome proxy is DEAD (${proxyDeadEvents.length} event(s) in window). ` +
51
+ `|corr(outcome_score, accepted_change_rate)| = ${correlation} < 0.1 at n ≥ 500. ` +
52
+ "outcome_score is statistically unrelated to improvement outcomes — " +
53
+ "treat outcome-derived rank contributions as noise until a real usage/outcome signal lands.",
54
+ });
55
+ }
56
+ // Salience-distribution collapse: Gini below the uniform baseline means
57
+ // ranking no longer discriminates between assets.
58
+ if (improveSummary.degradation?.salienceUniformityFlagged) {
59
+ advisories.push({
60
+ name: "salience-uniformity-collapse",
61
+ status: "warn",
62
+ kind: "deterministic",
63
+ confidence: "high",
64
+ message: `Salience distribution collapsed toward uniform: top-100 retrieval_salience Gini = ` +
65
+ `${improveSummary.degradation.corpusCentroidDistance} < 0.08 (uniform baseline ≈ 0.1). ` +
66
+ "Ranking currently carries little to no discrimination between assets.",
67
+ });
68
+ }
69
+ // Enrichment-vs-minting policy: enrichment lanes edit existing assets;
70
+ // a rising minted share means a lane is generating new content instead.
71
+ const minting = improveSummary.enrichmentMinting;
72
+ if (minting && Number.isFinite(minting.share) && minting.share > ENRICHMENT_MINTED_WARN_SHARE) {
73
+ advisories.push({
74
+ name: "enrichment-lane-minting",
75
+ status: minting.share > ENRICHMENT_MINTED_FAIL_SHARE ? "fail" : "warn",
76
+ kind: "deterministic",
77
+ confidence: "high",
78
+ message: `Enrichment lanes minted ${minting.minted} NEW asset(s) vs ${minting.updated} update(s) ` +
79
+ `(${Math.round(minting.share * 100)}% minted, threshold ${Math.round(ENRICHMENT_MINTED_WARN_SHARE * 100)}%). ` +
80
+ "Enrichment-classed lanes (proactive/high-salience/high-retrieval/signal-delta) are ratified to edit " +
81
+ "existing assets only — new-asset generation belongs to the signal-gated minting lanes.",
82
+ });
83
+ }
84
+ // Churn: accepted proposals far exceeding distinct touched refs means the
85
+ // loop is repeatedly rewriting the same assets, not covering the corpus.
86
+ if (Number.isFinite(improveSummary.coverage.churnRatio) && improveSummary.coverage.churnRatio > 1.5) {
87
+ advisories.push({
88
+ name: "improve-churn-ratio",
89
+ status: "warn",
90
+ kind: "deterministic",
91
+ confidence: "high",
92
+ message: `Improve churn ratio ${improveSummary.coverage.churnRatio} > 1.5: ` +
93
+ `${improveSummary.coverage.acceptedProposals} accepted proposals touched only ` +
94
+ `${improveSummary.coverage.distinctRefs} distinct assets in the window — ` +
95
+ "repeated rewrites of the same refs count as churn, not coverage.",
96
+ });
97
+ }
98
+ // R5 collapse/churn detector: surface any collapse_detector_alert events
99
+ // in the health window, plus the latest cycle row's headline numbers so
100
+ // the operator can act without opening the DB. `unknown` when the detector
101
+ // has never produced a cycle row (no consolidate/recombine work yet).
102
+ try {
103
+ // Reuse the already-open state.db handle (readEvents supports a
104
+ // borrowed connection) — no extra open/migrate/close per health call.
105
+ const collapseAlertEvents = readEvents({ since, type: "collapse_detector_alert" }, { dbPath: stateDbPath, db }).events;
106
+ const latestCycle = getLatestCycleMetrics(db);
107
+ const cycleSummary = latestCycle
108
+ ? `Latest cycle (${latestCycle.ts}, ${latestCycle.pass}): mean canary recall ${latestCycle.mean_recall.toFixed(3)}, ` +
109
+ `distinct-content ratio ${latestCycle.distinct_content_ratio.toFixed(3)}, ` +
110
+ `${latestCycle.accepted_actions} accepted action(s).`
111
+ : "";
112
+ if (collapseAlertEvents.length > 0) {
113
+ const kinds = [...new Set(collapseAlertEvents.map((e) => String(e.metadata?.kind ?? "unknown")))];
114
+ const collapseKinds = kinds.filter((k) => k.startsWith("collapse"));
115
+ advisories.push({
116
+ name: "collapse-churn-detector",
117
+ status: "warn",
118
+ kind: "deterministic",
119
+ // Collapse kinds are measured, not inferred; churn/merge-floor
120
+ // volume thresholds are still being tuned (design doc §7).
121
+ confidence: collapseKinds.length > 0 ? "high" : "medium",
122
+ message: `R5 detector fired ${collapseAlertEvents.length} alert(s) in window (kinds: ${kinds.join(", ")}). ` +
123
+ `${cycleSummary} See docs/design/improve-collapse-churn-detector-design.md §6.3 runbook queries.`,
124
+ });
125
+ }
126
+ else if (latestCycle) {
127
+ advisories.push({
128
+ name: "collapse-churn-detector",
129
+ status: "pass",
130
+ kind: "deterministic",
131
+ confidence: "high",
132
+ message: `No collapse/churn alerts in window. ${cycleSummary}`,
133
+ });
134
+ }
135
+ else {
136
+ advisories.push({
137
+ name: "collapse-churn-detector",
138
+ status: "unknown",
139
+ kind: "deterministic",
140
+ confidence: "high",
141
+ message: "No detector cycle rows yet — the collapse/churn detector runs only on improve cycles " +
142
+ "where consolidate/recombine did work (synthesis lanes may be idle).",
143
+ });
144
+ }
145
+ }
146
+ catch {
147
+ // Table may predate migration 016 in odd mixed-version setups — advisory
148
+ // is best-effort and must never fail the health command.
149
+ }
150
+ return advisories;
151
+ }