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

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 (103) 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 +96 -723
  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 +75 -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/llm/embedders/cache.js +3 -1
  74. package/dist/output/text/helpers.js +13 -0
  75. package/dist/registry/resolve.js +5 -0
  76. package/dist/scripts/migrate-storage.js +6908 -7447
  77. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +44 -43
  78. package/dist/setup/legacy-config.js +106 -0
  79. package/dist/setup/prompt.js +57 -0
  80. package/dist/setup/providers.js +14 -0
  81. package/dist/setup/semantic-assets.js +124 -0
  82. package/dist/setup/setup.js +24 -1607
  83. package/dist/setup/steps/connection.js +734 -0
  84. package/dist/setup/steps/output.js +31 -0
  85. package/dist/setup/steps/platforms.js +124 -0
  86. package/dist/setup/steps/semantic.js +27 -0
  87. package/dist/setup/steps/sources.js +222 -0
  88. package/dist/setup/steps/stashdir.js +42 -0
  89. package/dist/setup/steps/tasks.js +152 -0
  90. package/dist/storage/repositories/canaries-repository.js +107 -0
  91. package/dist/storage/repositories/consolidation-repository.js +38 -0
  92. package/dist/storage/repositories/embeddings-repository.js +72 -0
  93. package/dist/storage/repositories/events-repository.js +187 -0
  94. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  95. package/dist/storage/repositories/improve-runs-repository.js +130 -0
  96. package/dist/storage/repositories/index-db.js +4 -7
  97. package/dist/storage/repositories/proposals-repository.js +220 -0
  98. package/dist/storage/repositories/recombine-repository.js +213 -0
  99. package/dist/storage/repositories/task-history-repository.js +93 -0
  100. package/dist/storage/sqlite-pragmas.js +3 -3
  101. package/dist/tasks/runner.js +2 -1
  102. package/package.json +1 -1
  103. package/dist/commands/improve/homeostatic.js +0 -497
@@ -0,0 +1,65 @@
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
+ * Aggregate `llm_usage` events (#576) into the window total + per-stage
6
+ * breakdown reported by `akm health`.
7
+ */
8
+ import { readEvents } from "../../core/events.js";
9
+ import { LLM_USAGE_EVENT } from "../../llm/usage-persist.js";
10
+ import { toFiniteNumber } from "./improve-metrics.js";
11
+ /** Stage key used for `llm_usage` events recorded outside any stage scope. */
12
+ const UNATTRIBUTED_STAGE = "unattributed";
13
+ function emptyLlmUsageStageAggregate() {
14
+ return {
15
+ calls: 0,
16
+ totalDurationMs: 0,
17
+ promptTokens: 0,
18
+ completionTokens: 0,
19
+ totalTokens: 0,
20
+ reasoningTokens: 0,
21
+ };
22
+ }
23
+ function emptyLlmUsageAggregate() {
24
+ return { ...emptyLlmUsageStageAggregate(), byStage: {} };
25
+ }
26
+ /**
27
+ * Aggregate `llm_usage` events (#576) into a window total plus a per-stage
28
+ * breakdown of call count, wall-time, and token usage. Token fields absent from
29
+ * a best-effort record contribute 0. Calls with no `stage` land under
30
+ * {@link UNATTRIBUTED_STAGE}.
31
+ */
32
+ export function summarizeLlmUsage(events) {
33
+ const aggregate = emptyLlmUsageAggregate();
34
+ for (const event of events) {
35
+ const meta = event.metadata ?? {};
36
+ const stageKey = typeof meta.stage === "string" && meta.stage ? meta.stage : UNATTRIBUTED_STAGE;
37
+ let stage = aggregate.byStage[stageKey];
38
+ if (!stage) {
39
+ stage = emptyLlmUsageStageAggregate();
40
+ aggregate.byStage[stageKey] = stage;
41
+ }
42
+ const durationMs = toFiniteNumber(meta.durationMs);
43
+ const promptTokens = toFiniteNumber(meta.promptTokens);
44
+ const completionTokens = toFiniteNumber(meta.completionTokens);
45
+ const totalTokens = toFiniteNumber(meta.totalTokens);
46
+ const reasoningTokens = toFiniteNumber(meta.reasoningTokens);
47
+ for (const target of [aggregate, stage]) {
48
+ target.calls += 1;
49
+ target.totalDurationMs += durationMs;
50
+ target.promptTokens += promptTokens;
51
+ target.completionTokens += completionTokens;
52
+ target.totalTokens += totalTokens;
53
+ target.reasoningTokens += reasoningTokens;
54
+ }
55
+ }
56
+ return aggregate;
57
+ }
58
+ export function readLlmUsageAggregate(stateDbPath, since, until) {
59
+ const events = readEvents({ since, type: LLM_USAGE_EVENT }, { dbPath: stateDbPath }).events.filter((event) => {
60
+ if (until === undefined)
61
+ return true;
62
+ return new Date(event.ts ?? since).getTime() < new Date(until).getTime();
63
+ });
64
+ return summarizeLlmUsage(events);
65
+ }
@@ -0,0 +1,103 @@
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
+ import { INTERESTING_DELTA_PATHS, readNumericPath } from "./windows.js";
5
+ function padRight(s, width) {
6
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
7
+ }
8
+ function renderTable(headers, rows) {
9
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
10
+ const lines = [];
11
+ lines.push(headers.map((h, i) => padRight(h, widths[i] ?? 0)).join(" "));
12
+ for (const row of rows) {
13
+ lines.push(row.map((cell, i) => padRight(cell ?? "", widths[i] ?? 0)).join(" "));
14
+ }
15
+ return lines.join("\n");
16
+ }
17
+ /**
18
+ * Render `--detail per-run` rows as a TSV-ish aligned table. The column
19
+ * shape was originally inherited from the retired
20
+ * `scripts/improve-stats/runs-detail` bash helper; keep the same shape
21
+ * so operator muscle memory carries over.
22
+ *
23
+ * Columns: ts | ok | actions | refl_ok/fail/cd/skip |
24
+ * distill_q/llm-fail/qrej/cfg/skip | cons_proc/promo/merge/del |
25
+ * mem_cons/written/skip | graph_f/e/r | orphans | lint_f/fl
26
+ */
27
+ export function renderRunsDetailMd(runs) {
28
+ const headers = [
29
+ "ts",
30
+ "ok",
31
+ "actions",
32
+ "refl_ok/fail/cd/skip",
33
+ "distill_q/llm-fail/qrej/cfg/skip",
34
+ "cons_proc/promo/merge/del",
35
+ "mem_cons/written/skip",
36
+ "graph_f/e/r",
37
+ "orphans",
38
+ "lint_f/fl",
39
+ ];
40
+ const rows = runs.map((r) => {
41
+ const totalActions = r.actions.reflect.ok +
42
+ r.actions.reflect.failed +
43
+ r.actions.reflect.cooldown +
44
+ r.actions.reflect.skipped +
45
+ r.actions.distill.queued +
46
+ r.actions.distill.llmFailed +
47
+ r.actions.distill.qualityRejected +
48
+ r.actions.distill.configDisabled +
49
+ r.actions.distill.skipped +
50
+ r.actions.memoryPrune +
51
+ r.actions.memoryInference +
52
+ r.actions.graphExtraction +
53
+ r.actions.error;
54
+ return [
55
+ r.startedAt,
56
+ String(r.ok),
57
+ String(totalActions),
58
+ `${r.actions.reflect.ok}/${r.actions.reflect.failed}/${r.actions.reflect.cooldown}/${r.actions.reflect.skipped}`,
59
+ `${r.actions.distill.queued}/${r.actions.distill.llmFailed}/${r.actions.distill.qualityRejected}/${r.actions.distill.configDisabled}/${r.actions.distill.skipped}`,
60
+ `${r.consolidation.processed}/${r.consolidation.promoted}/${r.consolidation.merged}/${r.consolidation.deleted}`,
61
+ `${r.memoryInference.considered}/${r.memoryInference.written}/${r.memoryInference.skippedNoFacts}`,
62
+ `${r.graphExtraction.extractedFiles}/${r.graphExtraction.entities}/${r.graphExtraction.relations}`,
63
+ String(r.orphansPurged),
64
+ `${r.lintFixed}/${r.lintFlagged}`,
65
+ ];
66
+ });
67
+ return renderTable(headers, rows);
68
+ }
69
+ /**
70
+ * Render a window-compare comparison as a side-by-side metric table with a
71
+ * delta column. Bad-direction deltas (e.g. +pct on failed counts) get a `!`
72
+ * marker prefix.
73
+ */
74
+ export function renderWindowCompareMd(windows, deltas) {
75
+ if (windows.length === 0)
76
+ return "";
77
+ const headers = ["metric", ...windows.map((w) => w.name), "delta"];
78
+ const badIfPositive = new Set([
79
+ "improve.actions.reflect.failed",
80
+ "improve.actions.distill.llmFailed",
81
+ "improve.graphExtraction.failures",
82
+ "improve.graphExtraction.nonArrayBatchFailures",
83
+ "improve.wallTime.medianMs",
84
+ "improve.wallTime.p95Ms",
85
+ "improve.memoryInference.skippedNoFacts",
86
+ ]);
87
+ const rows = [];
88
+ for (const path of INTERESTING_DELTA_PATHS) {
89
+ const values = windows.map((w) => String(readNumericPath(w, path)));
90
+ const delta = deltas?.[path];
91
+ let deltaStr = "—";
92
+ if (delta) {
93
+ const pct = delta.pctChange;
94
+ const num = typeof pct === "number" ? pct : pct;
95
+ const sign = typeof num === "number" && num > 0 ? "+" : "";
96
+ const formatted = typeof num === "number" ? `${sign}${num}%` : String(num);
97
+ const marker = badIfPositive.has(path) && typeof num === "number" && num > 0 ? "!" : "";
98
+ deltaStr = marker + formatted;
99
+ }
100
+ rows.push([path, ...values, deltaStr]);
101
+ }
102
+ return renderTable(headers, rows);
103
+ }
@@ -0,0 +1,278 @@
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
+ * State.db-backed health metrics for `akm health`: the round-trip probe,
6
+ * auto-accept calibration, denominator-fixed coverage, the enrichment-vs-
7
+ * minting rollup, and the WS-5 per-run degradation metrics.
8
+ */
9
+ import { appendEvent, readEvents } from "../../core/events.js";
10
+ import { queryImproveRuns } from "../../storage/repositories/improve-runs-repository.js";
11
+ import { listProposalGateDecisions, listStateProposals } from "../../storage/repositories/proposals-repository.js";
12
+ import { gateDecisionsToSamples, summarizeCalibration } from "../improve/calibration.js";
13
+ import { roundRate, toFiniteNumber } from "./improve-metrics.js";
14
+ import { ENRICHMENT_LANES, } from "./types.js";
15
+ /** Event type appended + read back by the state.db round-trip probe. */
16
+ const HEALTH_PROBE_EVENT = "health_probe";
17
+ export function probeStateDbRoundTrip(stateDbPath) {
18
+ const before = readEvents({}, { dbPath: stateDbPath }).nextOffset;
19
+ const started = Date.now();
20
+ appendEvent({ eventType: HEALTH_PROBE_EVENT, ref: "health:probe", metadata: { source: "akm health" } }, { dbPath: stateDbPath });
21
+ const after = readEvents({ sinceOffset: before, type: HEALTH_PROBE_EVENT, ref: "health:probe" }, { dbPath: stateDbPath });
22
+ const durationMs = Date.now() - started;
23
+ if (after.events.length === 0 || after.nextOffset <= before) {
24
+ return { ok: false, durationMs, error: "probe event was not readable after append" };
25
+ }
26
+ return { ok: true, durationMs };
27
+ }
28
+ /**
29
+ * Read the auto-accept gate calibration summary (#612) over `[since, until)`.
30
+ * Reads every proposal's `gateDecision` from the open state.db, projects the
31
+ * acted-on (auto-accepted / auto-rejected) decisions into calibration samples
32
+ * within the window, and aggregates them deterministically.
33
+ */
34
+ export function readCalibration(db, since, until) {
35
+ const decisions = listProposalGateDecisions(db);
36
+ const samples = gateDecisionsToSamples(decisions, { since, ...(until !== undefined ? { until } : {}) });
37
+ return summarizeCalibration(samples);
38
+ }
39
+ // ── WS-5 Observability helpers ───────────────────────────────────────────────
40
+ /**
41
+ * Compute WS-5 denominator-fixed coverage metrics.
42
+ *
43
+ * `coverage = accepted_proposals / total_assets` (Part V §3).
44
+ * The denominator is the TOTAL stash size (not the moving eligible set) so
45
+ * more-inclusive WS-1 ranking cannot spuriously inflate coverage.
46
+ * `eligibleFraction = eligible_assets / total_assets` is reported separately.
47
+ *
48
+ * Proposals are counted only when their `updatedAt` falls within `[since, until)`
49
+ * so the rate is genuinely window-scoped (matching the JSDoc on the type).
50
+ *
51
+ * @param db - Open state.db connection.
52
+ * @param totalAssets - Total stash asset count (eligible + derived) from the
53
+ * most recent run's memorySummary. 0 = denominator unknown, returns NaN rates.
54
+ * @param eligibleAssets - Eligible (non-derived) asset count from the most recent run.
55
+ * @param since - Window start (ISO-8601). Proposals accepted before this are excluded.
56
+ * @param until - Window end (ISO-8601, exclusive). Absent = open-ended (up to now).
57
+ * @param stashDir - Optional: scope accepted proposals to one stash. Absent = all stashes.
58
+ */
59
+ export function computeDenominatorFixedCoverage(db, totalAssets, eligibleAssets, since, until, stashDir) {
60
+ let acceptedProposals = 0;
61
+ let distinctRefs = 0;
62
+ try {
63
+ const proposals = listStateProposals(db, {
64
+ status: "accepted",
65
+ ...(stashDir ? { stashDir } : {}),
66
+ }).filter((p) => {
67
+ const updatedAt = p.updatedAt ?? "";
68
+ if (updatedAt < since)
69
+ return false;
70
+ if (until !== undefined && updatedAt >= until)
71
+ return false;
72
+ return true;
73
+ });
74
+ acceptedProposals = proposals.length;
75
+ // Coverage counts DISTINCT refs: N accepted rewrites of one asset are
76
+ // churn, not coverage. The raw proposal count is kept alongside so the
77
+ // churn ratio (proposals ÷ distinct refs) stays visible.
78
+ distinctRefs = new Set(proposals.map((p) => p.ref)).size;
79
+ }
80
+ catch {
81
+ // Fail open: table may not exist on older installs.
82
+ }
83
+ const churnRatio = distinctRefs > 0 ? roundRate(acceptedProposals / distinctRefs) : Number.NaN;
84
+ if (totalAssets === 0) {
85
+ return {
86
+ rate: Number.NaN,
87
+ eligibleFraction: Number.NaN,
88
+ acceptedProposals,
89
+ distinctRefs,
90
+ churnRatio,
91
+ totalAssets: 0,
92
+ };
93
+ }
94
+ return {
95
+ rate: roundRate(distinctRefs / totalAssets),
96
+ eligibleFraction: roundRate(eligibleAssets / totalAssets),
97
+ acceptedProposals,
98
+ distinctRefs,
99
+ churnRatio,
100
+ totalAssets,
101
+ };
102
+ }
103
+ /**
104
+ * Compute the enrichment-vs-minting rollup over the window's accepted,
105
+ * lane-attributed proposals (reporting-only; see {@link EnrichmentMintingRollup}).
106
+ *
107
+ * SQL-side `json_extract` keeps the (potentially large) `backupContent` blobs
108
+ * out of process memory. Pre-Phase-6C rows without an `eligibilitySource`
109
+ * cannot be lane-classified and are excluded. Fails open (undefined) when the
110
+ * proposals table is absent.
111
+ */
112
+ export function computeEnrichmentMintingRollup(db, since, until) {
113
+ try {
114
+ const rows = db
115
+ .prepare(`SELECT
116
+ json_extract(metadata_json, '$.eligibilitySource') AS lane,
117
+ CASE WHEN json_extract(metadata_json, '$.backupContent') IS NULL THEN 1 ELSE 0 END AS is_minted,
118
+ COUNT(*) AS cnt
119
+ FROM proposals
120
+ WHERE status = 'accepted'
121
+ AND updated_at >= ?
122
+ AND (? IS NULL OR updated_at < ?)
123
+ AND json_extract(metadata_json, '$.eligibilitySource') IS NOT NULL
124
+ AND json_extract(metadata_json, '$.eligibilitySource') != ''
125
+ GROUP BY lane, is_minted`)
126
+ .all(since, until ?? null, until ?? null);
127
+ if (rows.length === 0)
128
+ return undefined;
129
+ const byLane = {};
130
+ for (const row of rows) {
131
+ byLane[row.lane] ??= { minted: 0, updated: 0 };
132
+ const entry = byLane[row.lane];
133
+ if (row.is_minted === 1)
134
+ entry.minted += row.cnt;
135
+ else
136
+ entry.updated += row.cnt;
137
+ }
138
+ let minted = 0;
139
+ let updated = 0;
140
+ for (const lane of ENRICHMENT_LANES) {
141
+ const entry = byLane[lane];
142
+ if (!entry)
143
+ continue;
144
+ minted += entry.minted;
145
+ updated += entry.updated;
146
+ }
147
+ const decided = minted + updated;
148
+ return {
149
+ minted,
150
+ updated,
151
+ share: decided > 0 ? roundRate(minted / decided) : Number.NaN,
152
+ byLane,
153
+ };
154
+ }
155
+ catch {
156
+ // Fail open: proposals table may not exist on older installs.
157
+ return undefined;
158
+ }
159
+ }
160
+ /**
161
+ * Compute WS-5 per-run degradation metrics (Part V §4).
162
+ *
163
+ * Health VIEWS only — reads from state.db tables populated by prior improve
164
+ * runs. Gracefully returns partial data when tables are absent (pre-WS-1/2).
165
+ *
166
+ * @param db - Open state.db connection.
167
+ * @param since - Window start (ISO-8601).
168
+ * @param until - Window end (ISO-8601).
169
+ */
170
+ export function computeDegradationMetrics(db, since, until) {
171
+ // (a) Corpus diversity — salience rank distribution of the top-100 assets.
172
+ // We use the Gini coefficient of retrieval_salience scores as an intra-corpus
173
+ // diversity proxy. A Gini close to 1 = highly concentrated (entrenched top
174
+ // assets), Gini near 0 = flat/diverse. This is a single-snapshot metric;
175
+ // consecutive-run centroid distance requires cross-run history not yet stored.
176
+ let corpusCentroidDistance = Number.NaN;
177
+ let entrenchmentFlagged;
178
+ let salienceUniformityFlagged;
179
+ try {
180
+ const rows = db
181
+ .prepare(`SELECT retrieval_salience FROM asset_salience
182
+ ORDER BY rank_score DESC LIMIT 100`)
183
+ .all();
184
+ if (rows.length >= 5) {
185
+ const vals = rows.map((r) => r.retrieval_salience).sort((a, b) => a - b);
186
+ const n = vals.length;
187
+ const sumAbsDiff = vals.reduce((acc, xi, i) => {
188
+ return acc + vals.slice(i + 1).reduce((a, xj) => a + Math.abs(xi - xj), 0);
189
+ }, 0);
190
+ const mean = vals.reduce((a, b) => a + b, 0) / n;
191
+ // Gini = (sum |xi - xj|) / (2 n^2 mean); 0 = perfect equality, 1 = perfect inequality.
192
+ const gini = mean > 0 ? sumAbsDiff / (2 * n * n * mean) : 0;
193
+ // Re-express as a diversity proxy in [0,1]: high gini = low diversity.
194
+ // corpusCentroidDistance approximation: gini is "distance from uniform".
195
+ // Note: retrieval_salience values are in [0,1], so the max achievable Gini
196
+ // with this formula is ~0.5 (when one asset dominates and others are near 0).
197
+ // Two-tailed: >0.35 flags entrenchment (robustly above the ~0.1 uniform
198
+ // baseline); <0.08 flags uniformity collapse — the distribution no longer
199
+ // discriminates between assets (live 2026-07 value 0.040 sat unflagged
200
+ // in this tail under the old one-tailed check).
201
+ corpusCentroidDistance = roundRate(gini);
202
+ entrenchmentFlagged = gini > 0.35;
203
+ salienceUniformityFlagged = gini < 0.08;
204
+ }
205
+ }
206
+ catch {
207
+ // Table not present (pre-WS-1 install) — leave NaN.
208
+ }
209
+ // (b) Merge fidelity — fraction of consolidate accepted proposals in the window
210
+ // whose ref also has a consolidate skip-reason of "contradict_target_missing"
211
+ // or an event indicating contradiction. Uses the improve_runs result_json
212
+ // consolidation.contradicted count as a proxy.
213
+ // Simple implementation: contradictionRate = total_contradicted / max(1, total_processed)
214
+ // sourced from the window's consolidation envelope.
215
+ // (The full "merge proposal → later contradiction" correlation requires cross-run
216
+ // history; this is the available proxy.)
217
+ let mergeFidelityContradictionRate = 0;
218
+ try {
219
+ const runs = queryImproveRuns(db, since, until);
220
+ let totalContradicted = 0;
221
+ let totalProcessed = 0;
222
+ for (const row of runs) {
223
+ try {
224
+ const result = JSON.parse(row.result_json);
225
+ const cons = result.consolidation;
226
+ if (cons) {
227
+ totalContradicted += toFiniteNumber(cons.contradicted);
228
+ totalProcessed += toFiniteNumber(cons.processed);
229
+ }
230
+ }
231
+ catch {
232
+ // Skip malformed rows.
233
+ }
234
+ }
235
+ if (totalProcessed > 0) {
236
+ mergeFidelityContradictionRate = roundRate(totalContradicted / totalProcessed);
237
+ }
238
+ }
239
+ catch {
240
+ // Fail open.
241
+ }
242
+ // (c) highGenerationFraction was DELETED (meta-review 05 DRIFT-3): it
243
+ // approximated "LLM-merge generations" from consecutive_no_ops — which counts
244
+ // the opposite condition (cycles where nothing was changed) — and its own
245
+ // in-code TODO admitted the proxy. Display-only, never actionable; removed
246
+ // rather than instrumented.
247
+ // (d) Oracle spot-check — up to 5 recently accepted proposals in the window.
248
+ const oracleSpotCheck = [];
249
+ try {
250
+ const accepted = listStateProposals(db, { status: "accepted" }).filter((p) => {
251
+ const updatedAt = p.updatedAt ?? "";
252
+ return updatedAt >= since && updatedAt < until;
253
+ });
254
+ // Sample up to 5: pick evenly spaced (not just the first 5).
255
+ const step = Math.max(1, Math.floor(accepted.length / 5));
256
+ for (let i = 0; i < accepted.length && oracleSpotCheck.length < 5; i += step) {
257
+ const p = accepted[i];
258
+ if (p) {
259
+ oracleSpotCheck.push({
260
+ proposalId: p.id,
261
+ ref: p.ref,
262
+ source: p.source ?? "unknown",
263
+ acceptedAt: p.updatedAt ?? p.createdAt ?? "",
264
+ });
265
+ }
266
+ }
267
+ }
268
+ catch {
269
+ // Fail open.
270
+ }
271
+ return {
272
+ corpusCentroidDistance,
273
+ entrenchmentFlagged,
274
+ salienceUniformityFlagged,
275
+ mergeFidelityContradictionRate,
276
+ oracleSpotCheck,
277
+ };
278
+ }
@@ -0,0 +1,135 @@
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
+ import { queryImproveRuns } from "../../storage/repositories/improve-runs-repository.js";
5
+ import { queryCompletedTaskIntervals, queryTaskHistory } from "../../storage/repositories/task-history-repository.js";
6
+ import { projectImproveRunSummary } from "./improve-metrics.js";
7
+ /**
8
+ * Load task_history intervals for `task_id='akm-improve'` in the window.
9
+ * Returned sorted by startMs ascending so containment lookups can use a
10
+ * linear scan (typical N is ~24/day; not worth a tree).
11
+ *
12
+ * The window filter is widened by 5 minutes on each side because the cron
13
+ * task wraps `akm improve` — the task `started_at` fires at e.g. :07:01
14
+ * while `recordImproveRun` writes the matching `improve_runs.started_at`
15
+ * later (after config load, planning, etc.), so the improve_runs row can
16
+ * be inside the window even when its enclosing task_history row started
17
+ * just before the window opened.
18
+ */
19
+ function loadTaskIntervals(db, since, until) {
20
+ const sinceMs = new Date(since).getTime();
21
+ const untilMs = until ? new Date(until).getTime() : Number.POSITIVE_INFINITY;
22
+ const widenedSince = new Date(sinceMs - 5 * 60 * 1000).toISOString();
23
+ const widenedUntil = Number.isFinite(untilMs) ? new Date(untilMs + 5 * 60 * 1000).toISOString() : undefined;
24
+ const rows = queryCompletedTaskIntervals(db, widenedSince, widenedUntil);
25
+ const intervals = [];
26
+ for (const row of rows) {
27
+ const startMs = new Date(row.started_at).getTime();
28
+ const endMs = new Date(row.completed_at).getTime();
29
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs)
30
+ continue;
31
+ intervals.push({ startMs, endMs, durationMs: endMs - startMs });
32
+ }
33
+ return intervals;
34
+ }
35
+ /**
36
+ * Find the task_history interval that contains the given timestamp. The
37
+ * task wraps `akm improve`, so `improve_runs.started_at` (when
38
+ * `recordImproveRun` writes) always falls inside the enclosing task's
39
+ * [started_at, completed_at]. Returns undefined when no interval
40
+ * contains the timestamp (which happens for manually-invoked improve
41
+ * runs not driven by the `akm-improve` task).
42
+ *
43
+ * Linear scan because N is small. We tolerate a 1s slop on the upper
44
+ * bound to handle clock skew between the wrapper's `completed_at` write
45
+ * and recordImproveRun's `started_at` write.
46
+ */
47
+ function findContainingTaskInterval(timestampMs, intervals) {
48
+ const SLOP_MS = 1000;
49
+ for (const interval of intervals) {
50
+ if (timestampMs >= interval.startMs && timestampMs <= interval.endMs + SLOP_MS) {
51
+ return interval;
52
+ }
53
+ }
54
+ return undefined;
55
+ }
56
+ /**
57
+ * Load `task_history` rows whose `task_id` begins `akm-improve` (the scheduled
58
+ * improve tasks: `akm-improve-frequent`, `akm-improve-proactive-weekly`, …) in
59
+ * the window, widened ±5 min so a task that fired just before the window opened
60
+ * still matches a run inside it. Used to attribute each improve run to the task
61
+ * that launched it.
62
+ */
63
+ function loadImproveTaskRuns(db, since, until) {
64
+ const sinceMs = new Date(since).getTime();
65
+ const untilMs = until ? new Date(until).getTime() : undefined;
66
+ const widenedSince = new Date(sinceMs - 5 * 60 * 1000).toISOString();
67
+ const widenedUntil = untilMs !== undefined ? new Date(untilMs + 5 * 60 * 1000).toISOString() : undefined;
68
+ const runs = [];
69
+ for (const row of queryTaskHistory(db, { since: widenedSince, until: widenedUntil })) {
70
+ if (!row.task_id.startsWith("akm-improve"))
71
+ continue;
72
+ const startMs = new Date(row.started_at).getTime();
73
+ if (!Number.isFinite(startMs))
74
+ continue;
75
+ const endIso = row.completed_at ?? row.failed_at;
76
+ const endMs = endIso ? new Date(endIso).getTime() : Number.NaN;
77
+ runs.push({ taskId: row.task_id, startMs, endMs });
78
+ }
79
+ return runs;
80
+ }
81
+ /**
82
+ * Attribute an improve run to the scheduled task that launched it by matching
83
+ * start times within ±5 min, scored by start delta (plus end delta when both
84
+ * ends are known). Port of the health-report skill's `match_task_id`. Returns
85
+ * `"manual"` when no scheduled improve task matches.
86
+ */
87
+ export function matchImproveTaskId(startedAt, completedAt, taskRuns) {
88
+ const startMs = new Date(startedAt).getTime();
89
+ if (!Number.isFinite(startMs))
90
+ return "manual";
91
+ const endMs = completedAt ? new Date(completedAt).getTime() : Number.NaN;
92
+ let best;
93
+ let bestScore = Number.POSITIVE_INFINITY;
94
+ for (const task of taskRuns) {
95
+ const startDelta = Math.abs(task.startMs - startMs);
96
+ if (startDelta > 5 * 60 * 1000)
97
+ continue;
98
+ let score = startDelta;
99
+ if (Number.isFinite(endMs) && Number.isFinite(task.endMs))
100
+ score += Math.abs(task.endMs - endMs);
101
+ if (score < bestScore) {
102
+ bestScore = score;
103
+ best = task.taskId;
104
+ }
105
+ }
106
+ return best ?? "manual";
107
+ }
108
+ export function buildPerRunSummaries(db, since, until) {
109
+ const rows = queryImproveRuns(db, since, until);
110
+ const taskIntervals = loadTaskIntervals(db, since, until);
111
+ const improveTaskRuns = loadImproveTaskRuns(db, since, until);
112
+ const summaries = [];
113
+ for (const row of rows) {
114
+ const startMs = new Date(row.started_at).getTime();
115
+ const endMs = new Date(row.completed_at).getTime();
116
+ // Prefer the improve_runs row's own (completed_at - started_at) delta:
117
+ // recordImproveRun now persists distinct start/end timestamps, so the
118
+ // row's own delta is the authoritative per-run wall time even for
119
+ // manually-invoked `akm improve` runs with no enclosing task_history.
120
+ // Only fall back to the task_history containing-interval join for legacy/
121
+ // backfill rows where started_at == completed_at (row delta is 0).
122
+ const hasRowDelta = Number.isFinite(startMs) && Number.isFinite(endMs) && endMs > startMs;
123
+ let wallTimeMs;
124
+ if (hasRowDelta) {
125
+ wallTimeMs = endMs - startMs;
126
+ }
127
+ else {
128
+ const interval = Number.isFinite(startMs) ? findContainingTaskInterval(startMs, taskIntervals) : undefined;
129
+ wallTimeMs = interval?.durationMs ?? 0;
130
+ }
131
+ const taskId = matchImproveTaskId(row.started_at, row.completed_at, improveTaskRuns);
132
+ summaries.push(projectImproveRunSummary(row, wallTimeMs, taskId));
133
+ }
134
+ return summaries;
135
+ }
@@ -0,0 +1,18 @@
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
+ * Lanes ratified as ENRICHMENT-ONLY: they may propose edits to existing
6
+ * assets (metadata, relations, content refresh) but must not mint new ones.
7
+ * New-asset generation belongs to the signal-gated minting lanes
8
+ * (extract/distill/memory-inference/recombine).
9
+ */
10
+ export const ENRICHMENT_LANES = ["proactive", "high-salience", "high-retrieval", "signal-delta"];
11
+ /** Minted share of enrichment-lane accepts that triggers a WARN advisory. */
12
+ export const ENRICHMENT_MINTED_WARN_SHARE = 0.05;
13
+ /** Minted share of enrichment-lane accepts that triggers a FAIL advisory. */
14
+ export const ENRICHMENT_MINTED_FAIL_SHARE = 0.15;
15
+ /** Event type recorded on each completed improve run. */
16
+ export const IMPROVE_COMPLETED_EVENT = "improve_completed";
17
+ /** An active task older than this (ms) is treated as stuck. */
18
+ export const ACTIVE_RUN_WARN_MS = 15 * 60 * 1000;