akm-cli 0.9.28-alpha.1 → 0.9.28-alpha.2
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 +43 -0
- package/STABILITY.md +4 -0
- package/dist/assets/hints/cli-hints-full.md +3 -0
- package/dist/assets/hints/cli-hints-short.md +1 -0
- package/dist/assets/templates/html/metrics.html +977 -0
- package/dist/cli/shared.js +5 -4
- package/dist/cli.js +11 -1
- package/dist/commands/health/accept-rate.js +8 -4
- package/dist/commands/health/html-report.js +3 -8
- package/dist/commands/health/llm-usage.js +17 -6
- package/dist/commands/health/renderers.js +4 -4
- package/dist/commands/improve/improve-report.js +4 -2
- package/dist/commands/improve/improve.js +22 -10
- package/dist/commands/metrics/collect.js +439 -0
- package/dist/commands/metrics/html-report.js +82 -0
- package/dist/commands/metrics/md-report.js +44 -0
- package/dist/commands/metrics/metrics-cli.js +213 -0
- package/dist/commands/metrics/report-view.js +243 -0
- package/dist/commands/metrics/types.js +4 -0
- package/dist/commands/read/search.js +5 -0
- package/dist/indexer/indexer.js +64 -14
- package/dist/indexer/usage/usage-events.js +3 -1
- package/dist/integrations/session-logs/pre-filter.js +1 -0
- package/dist/llm/usage-persist.js +22 -11
- package/dist/llm/usage-telemetry.js +4 -0
- package/dist/output/html-render.js +15 -8
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/output/text/metrics.js +39 -0
- package/dist/output/text.js +2 -0
- package/dist/scripts/akm-migrate-node.js +281 -8
- package/dist/scripts/akm-migrate.js +281 -8
- package/dist/storage/repositories/index-utility-repository.js +24 -0
- package/dist/storage/repositories/metrics-repository.js +80 -0
- package/docs/reference/cli.md +63 -4
- package/docs/reference/data-and-telemetry.md +40 -7
- package/package.json +1 -1
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* swallowing in `emitLlmUsage`, telemetry can never break a real run.
|
|
21
21
|
*/
|
|
22
22
|
import { appendEvent } from "../core/events.js";
|
|
23
|
-
import { clearLlmUsageSink, hasLlmUsageSink, setLlmUsageSink } from "./usage-telemetry.js";
|
|
23
|
+
import { clearLlmUsageSink, getLlmUsageSink, hasLlmUsageSink, setLlmUsageSink, } from "./usage-telemetry.js";
|
|
24
24
|
/** Event type for persisted per-call LLM usage telemetry. */
|
|
25
25
|
export const LLM_USAGE_EVENT = "llm_usage";
|
|
26
26
|
/** Event type for the owning sink's terminal-record count marker. */
|
|
@@ -59,9 +59,12 @@ function toEventMetadata(record) {
|
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
61
|
* Install a usage sink that persists each LLM call as an `llm_usage` event via
|
|
62
|
-
* `appendEvent`. Returns a disposer that
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* `appendEvent`. Returns a disposer that restores the sink that was installed
|
|
63
|
+
* before this one (clearing it when there was none) — call it in a `finally`
|
|
64
|
+
* block so per-run wiring does not leak across runs (and so the test-isolation
|
|
65
|
+
* harness sees a clean sink between tests). Restoring rather than clearing lets
|
|
66
|
+
* a per-run sink (`akm improve`) sit on top of the process-wide one `runCli`
|
|
67
|
+
* installs, which then resumes when the run ends.
|
|
65
68
|
*
|
|
66
69
|
* `ctx` should carry the same long-lived `state.db` handle the caller already
|
|
67
70
|
* opened for its other events. A getter is resolved for every append so a
|
|
@@ -73,9 +76,10 @@ function toEventMetadata(record) {
|
|
|
73
76
|
* it to know the run is no longer silent, without this module taking on any
|
|
74
77
|
* dependency of its own on improve's lifecycle.
|
|
75
78
|
*/
|
|
76
|
-
export function installLlmUsagePersistence(ctx, onRecord) {
|
|
79
|
+
export function installLlmUsagePersistence(ctx, onRecord, options = {}) {
|
|
77
80
|
let expectedTerminalRecords = 0;
|
|
78
81
|
let disposed = false;
|
|
82
|
+
const previous = getLlmUsageSink();
|
|
79
83
|
setLlmUsageSink((record) => {
|
|
80
84
|
expectedTerminalRecords += 1;
|
|
81
85
|
onRecord?.(record);
|
|
@@ -85,7 +89,12 @@ export function installLlmUsagePersistence(ctx, onRecord) {
|
|
|
85
89
|
if (disposed)
|
|
86
90
|
return;
|
|
87
91
|
disposed = true;
|
|
88
|
-
|
|
92
|
+
if (previous)
|
|
93
|
+
setLlmUsageSink(previous);
|
|
94
|
+
else
|
|
95
|
+
clearLlmUsageSink();
|
|
96
|
+
if (options.skipEmptySummary && expectedTerminalRecords === 0)
|
|
97
|
+
return;
|
|
89
98
|
try {
|
|
90
99
|
appendEvent({ eventType: LLM_USAGE_SUMMARY_EVENT, metadata: { expectedTerminalRecords } }, typeof ctx === "function" ? ctx() : ctx);
|
|
91
100
|
}
|
|
@@ -96,13 +105,15 @@ export function installLlmUsagePersistence(ctx, onRecord) {
|
|
|
96
105
|
}
|
|
97
106
|
/**
|
|
98
107
|
* Like {@link installLlmUsagePersistence}, but a no-op when a sink is already
|
|
99
|
-
* installed — used by
|
|
100
|
-
* that may also run as a sub-step of
|
|
101
|
-
* enclosing run the existing
|
|
102
|
-
* disposer then does nothing
|
|
108
|
+
* installed — used by `runCli` for the process-wide sink and by standalone
|
|
109
|
+
* entry points (`akm proposal drain`) that may also run as a sub-step of
|
|
110
|
+
* `akm improve`. When invoked inside an enclosing run the existing sink keeps
|
|
111
|
+
* ownership; the returned disposer then does nothing. A sink installed here
|
|
112
|
+
* writes no summary marker when it saw no call, so commands that never reach an
|
|
113
|
+
* LLM leave no event behind.
|
|
103
114
|
*/
|
|
104
115
|
export function installLlmUsagePersistenceIfAbsent(ctx) {
|
|
105
116
|
if (hasLlmUsageSink())
|
|
106
117
|
return () => { };
|
|
107
|
-
return installLlmUsagePersistence(ctx);
|
|
118
|
+
return installLlmUsagePersistence(ctx, undefined, { skipEmptySummary: true });
|
|
108
119
|
}
|
|
@@ -49,6 +49,10 @@ export function currentLlmStage() {
|
|
|
49
49
|
export function setLlmUsageSink(sink) {
|
|
50
50
|
usageSink = sink;
|
|
51
51
|
}
|
|
52
|
+
/** The currently installed sink, or `undefined` when none is. Lets an owner restore it after installing its own. */
|
|
53
|
+
export function getLlmUsageSink() {
|
|
54
|
+
return usageSink;
|
|
55
|
+
}
|
|
52
56
|
/** Remove the installed sink so subsequent calls emit nowhere. Idempotent. */
|
|
53
57
|
export function clearLlmUsageSink() {
|
|
54
58
|
usageSink = undefined;
|
|
@@ -5,18 +5,19 @@
|
|
|
5
5
|
* `--format html` rendering primitives (#582).
|
|
6
6
|
*
|
|
7
7
|
* Templates live in `src/assets/templates/html/` (mirrored to
|
|
8
|
-
* `dist/assets/templates/html/` by `scripts/copy-assets.ts`). `akm health`
|
|
9
|
-
* the only
|
|
10
|
-
* via plain `%%TOKEN%%` string replacement — no template
|
|
11
|
-
* `src/cli/shared.ts`'s `output()` calls
|
|
12
|
-
* and every other command falls back to `renderGenericHtml`
|
|
13
|
-
* `--format html` therefore works for every command (D7)
|
|
14
|
-
* template path is just
|
|
15
|
-
* gate.
|
|
8
|
+
* `dist/assets/templates/html/` by `scripts/copy-assets.ts`). `akm health` and
|
|
9
|
+
* `akm metrics` are the only commands with a bespoke `<command>.html` template
|
|
10
|
+
* (substituted here via plain `%%TOKEN%%` string replacement — no template
|
|
11
|
+
* engine, by design); `src/cli/shared.ts`'s `output()` calls their renderers
|
|
12
|
+
* directly and every other command falls back to `renderGenericHtml`
|
|
13
|
+
* (`./generic-render`). `--format html` therefore works for every command (D7)
|
|
14
|
+
* — this module's template path is just the opt-in bespoke rendering, not a
|
|
15
|
+
* format-wide gate.
|
|
16
16
|
*/
|
|
17
17
|
import fs from "node:fs";
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import healthTemplate from "../assets/templates/html/health.html" with { type: "text" };
|
|
20
|
+
import metricsTemplate from "../assets/templates/html/metrics.html" with { type: "text" };
|
|
20
21
|
import { getDirname } from "../runtime.js";
|
|
21
22
|
import { writeStdout } from "./stdout.js";
|
|
22
23
|
const TEMPLATES_DIR = path.join(getDirname(import.meta.url), "../assets/templates/html");
|
|
@@ -38,6 +39,7 @@ const EMBEDDED_TEMPLATES = {
|
|
|
38
39
|
// the file's contents as a string on every runtime. The cast reconciles the
|
|
39
40
|
// ambient declaration with the actual import attribute.
|
|
40
41
|
health: healthTemplate,
|
|
42
|
+
metrics: metricsTemplate,
|
|
41
43
|
};
|
|
42
44
|
/**
|
|
43
45
|
* Resolve the on-disk template path for a command's bespoke `<command>.html`.
|
|
@@ -97,6 +99,11 @@ export function escapeHtml(value) {
|
|
|
97
99
|
.replaceAll('"', """)
|
|
98
100
|
.replaceAll("'", "'");
|
|
99
101
|
}
|
|
102
|
+
/** Emit a <time> element that the browser's JS will reformat to the viewer's local timezone. */
|
|
103
|
+
export function isoTimeTag(iso) {
|
|
104
|
+
const fallback = iso.slice(0, 16).replace("T", " ");
|
|
105
|
+
return `<time data-iso="${escapeHtml(iso)}">${escapeHtml(fallback)}</time>`;
|
|
106
|
+
}
|
|
100
107
|
/**
|
|
101
108
|
* Deliver a rendered document: write to `outputPath` when set (`--output`),
|
|
102
109
|
* otherwise print to stdout.
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
* `akm metrics --format text`: aligned sections with top-N tables. Lists are
|
|
6
|
+
* cut to 5 at `--detail brief` and show everything the top-20 cap kept otherwise.
|
|
7
|
+
*/
|
|
8
|
+
import { buildMetricsView, isMetricsResult } from "../../commands/metrics/report-view.js";
|
|
9
|
+
const flat = (s) => s.replace(/\s*\r?\n\s*/g, " ");
|
|
10
|
+
function renderTable(table) {
|
|
11
|
+
const all = [table.headers, ...table.rows].map((r) => r.map(flat));
|
|
12
|
+
const widths = table.headers.map((_, i) => Math.max(...all.map((r) => (r[i] ?? "").length)));
|
|
13
|
+
const line = (r) => ` ${r.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")}`.trimEnd();
|
|
14
|
+
return [`${table.title}:`, ...all.map(line)];
|
|
15
|
+
}
|
|
16
|
+
export function formatMetricsPlain(result, detail) {
|
|
17
|
+
if (!isMetricsResult(result))
|
|
18
|
+
return null;
|
|
19
|
+
const view = buildMetricsView(result, detail);
|
|
20
|
+
const lines = [`akm metrics ${view.window}`, `filters: ${view.filters}`];
|
|
21
|
+
for (const section of view.sections) {
|
|
22
|
+
lines.push("", section.title.toUpperCase());
|
|
23
|
+
const labelWidth = Math.max(0, ...section.facts.map(([label]) => label.length));
|
|
24
|
+
for (const [label, value] of section.facts)
|
|
25
|
+
lines.push(` ${label.padEnd(labelWidth)} ${value}`);
|
|
26
|
+
for (const table of section.tables) {
|
|
27
|
+
if (table.rows.length > 0)
|
|
28
|
+
lines.push("", ...renderTable(table));
|
|
29
|
+
else if (!table.hideWhenEmpty)
|
|
30
|
+
lines.push("", `${table.title}: (none)`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (view.notes.length > 0)
|
|
34
|
+
lines.push("", "NOTES", ...view.notes.map((n) => ` - ${flat(n)}`));
|
|
35
|
+
return lines.join("\n");
|
|
36
|
+
}
|
|
37
|
+
export const metricsFormatters = [
|
|
38
|
+
{ command: "metrics", handler: (r, detail) => formatMetricsPlain(r, detail) },
|
|
39
|
+
];
|
package/dist/output/text.js
CHANGED
|
@@ -19,6 +19,7 @@ import { indexFormatters } from "./text/index.js";
|
|
|
19
19
|
import { infoFormatters } from "./text/info.js";
|
|
20
20
|
import { lintFormatters } from "./text/lint.js";
|
|
21
21
|
import { listFormatters } from "./text/list.js";
|
|
22
|
+
import { metricsFormatters } from "./text/metrics.js";
|
|
22
23
|
import { migrateFormatters } from "./text/migrate.js";
|
|
23
24
|
import { modelsFormatters } from "./text/models.js";
|
|
24
25
|
import { proposalProducerFormatters } from "./text/proposal/producer.js";
|
|
@@ -59,6 +60,7 @@ const BUILT_IN_TEXT_FORMATTERS = [
|
|
|
59
60
|
...proposalProducerFormatters,
|
|
60
61
|
...infoFormatters,
|
|
61
62
|
...healthFormatters,
|
|
63
|
+
...metricsFormatters,
|
|
62
64
|
...improveReportFormatters,
|
|
63
65
|
...lintFormatters,
|
|
64
66
|
...configFormatters,
|
|
@@ -28449,6 +28449,243 @@ registerMdRenderer("health", (result) => {
|
|
|
28449
28449
|
return null;
|
|
28450
28450
|
});
|
|
28451
28451
|
|
|
28452
|
+
// src/commands/metrics/report-view.ts
|
|
28453
|
+
var BRIEF_LIST_LIMIT = 5;
|
|
28454
|
+
function isMetricsResult(value) {
|
|
28455
|
+
if (value === null || typeof value !== "object")
|
|
28456
|
+
return false;
|
|
28457
|
+
const r = value;
|
|
28458
|
+
return r.schemaVersion === 1 && !!r.window && !!r.usage && !!r.feedback && !!r.llm;
|
|
28459
|
+
}
|
|
28460
|
+
var int = (n) => String(n);
|
|
28461
|
+
var dash = (v) => v === undefined || v === "" ? "-" : v;
|
|
28462
|
+
var pct = (n) => n === null ? "n/a" : `${(n * 100).toFixed(1)}%`;
|
|
28463
|
+
var ms = (n) => n === null ? "n/a" : `${Math.round(n)} ms`;
|
|
28464
|
+
var fixed = (n, digits) => n === null ? "n/a" : n.toFixed(digits);
|
|
28465
|
+
function sourceFacts(counts) {
|
|
28466
|
+
const entries = Object.entries(counts);
|
|
28467
|
+
return entries.length === 0 ? "-" : entries.map(([k, v]) => `${k}=${v}`).join(" ");
|
|
28468
|
+
}
|
|
28469
|
+
function llmRows(group) {
|
|
28470
|
+
return Object.entries(group).map(([name, a]) => [
|
|
28471
|
+
name,
|
|
28472
|
+
int(a.calls),
|
|
28473
|
+
int(a.failures),
|
|
28474
|
+
int(a.promptTokens),
|
|
28475
|
+
int(a.completionTokens),
|
|
28476
|
+
int(a.totalTokens),
|
|
28477
|
+
ms(a.totalDurationMs)
|
|
28478
|
+
]);
|
|
28479
|
+
}
|
|
28480
|
+
var LLM_HEADERS = ["name", "calls", "failed", "prompt", "completion", "total", "time"];
|
|
28481
|
+
function buildMetricsView(r, detail) {
|
|
28482
|
+
const brief = detail === "brief";
|
|
28483
|
+
const cut = (items) => brief ? items.slice(0, BRIEF_LIST_LIMIT) : items;
|
|
28484
|
+
const { usage, feedback, utility, llm } = r;
|
|
28485
|
+
const searches = usage.totals.searches;
|
|
28486
|
+
const sections = [
|
|
28487
|
+
{
|
|
28488
|
+
title: "Usage",
|
|
28489
|
+
facts: [
|
|
28490
|
+
["searches", int(searches)],
|
|
28491
|
+
["shows", int(usage.totals.shows)],
|
|
28492
|
+
["curates", int(usage.totals.curates)],
|
|
28493
|
+
["selects", int(usage.totals.selects)],
|
|
28494
|
+
["select rate", pct(usage.selectRate)],
|
|
28495
|
+
[
|
|
28496
|
+
"zero-result searches",
|
|
28497
|
+
`${usage.totals.zeroResultSearches} (${pct(searches === 0 ? null : usage.totals.zeroResultSearches / searches)})`
|
|
28498
|
+
],
|
|
28499
|
+
["search median", ms(usage.searchMedianMs)],
|
|
28500
|
+
["distinct assets", int(usage.totals.distinctAssets)],
|
|
28501
|
+
["distinct queries", int(usage.totals.distinctQueries)],
|
|
28502
|
+
["by source", sourceFacts(usage.bySource)]
|
|
28503
|
+
],
|
|
28504
|
+
tables: [
|
|
28505
|
+
{
|
|
28506
|
+
title: "Top assets",
|
|
28507
|
+
headers: ["ref", "shows", "search hits", "selects", "+", "-", "last used"],
|
|
28508
|
+
rows: cut(usage.topAssets).map((a) => [
|
|
28509
|
+
a.ref,
|
|
28510
|
+
int(a.shows),
|
|
28511
|
+
int(a.searchHits),
|
|
28512
|
+
int(a.selects),
|
|
28513
|
+
int(a.positive),
|
|
28514
|
+
int(a.negative),
|
|
28515
|
+
dash(a.lastUsedAt)
|
|
28516
|
+
])
|
|
28517
|
+
},
|
|
28518
|
+
{
|
|
28519
|
+
title: "Top queries",
|
|
28520
|
+
headers: ["query", "count", "avg results", "last"],
|
|
28521
|
+
rows: cut(usage.topQueries).map((q) => [q.query, int(q.count), fixed(q.avgResults, 1), q.lastAt])
|
|
28522
|
+
},
|
|
28523
|
+
{
|
|
28524
|
+
title: "Zero-result queries",
|
|
28525
|
+
headers: ["query", "count", "last"],
|
|
28526
|
+
rows: cut(usage.zeroResultQueries).map((q) => [q.query, int(q.count), q.lastAt])
|
|
28527
|
+
},
|
|
28528
|
+
{
|
|
28529
|
+
title: "Daily",
|
|
28530
|
+
headers: ["day", "search", "show", "curate", "feedback"],
|
|
28531
|
+
rows: brief ? [] : usage.daily.map((d) => [d.day, int(d.search), int(d.show), int(d.curate), int(d.feedback)]),
|
|
28532
|
+
hideWhenEmpty: true
|
|
28533
|
+
}
|
|
28534
|
+
]
|
|
28535
|
+
},
|
|
28536
|
+
{
|
|
28537
|
+
title: "Feedback",
|
|
28538
|
+
facts: [
|
|
28539
|
+
["positive", int(feedback.totals.positive)],
|
|
28540
|
+
["negative", int(feedback.totals.negative)]
|
|
28541
|
+
],
|
|
28542
|
+
tables: [
|
|
28543
|
+
{
|
|
28544
|
+
title: "By asset",
|
|
28545
|
+
headers: ["ref", "+", "-", "valence", "last"],
|
|
28546
|
+
rows: cut(feedback.byAsset).map((a) => [
|
|
28547
|
+
a.ref,
|
|
28548
|
+
int(a.positive),
|
|
28549
|
+
int(a.negative),
|
|
28550
|
+
fixed(a.valence, 2),
|
|
28551
|
+
a.lastAt
|
|
28552
|
+
])
|
|
28553
|
+
},
|
|
28554
|
+
{
|
|
28555
|
+
title: "By tag",
|
|
28556
|
+
headers: ["tag", "+", "-"],
|
|
28557
|
+
rows: cut(Object.entries(feedback.byTag)).map(([tag, c]) => [tag, int(c.positive), int(c.negative)]),
|
|
28558
|
+
hideWhenEmpty: true
|
|
28559
|
+
},
|
|
28560
|
+
{
|
|
28561
|
+
title: "Recent negative",
|
|
28562
|
+
headers: ["ref", "at", "reason", "tags"],
|
|
28563
|
+
rows: cut(feedback.recentNegative).map((n) => [n.ref, n.at, dash(n.reason), dash(n.tags?.join(", "))])
|
|
28564
|
+
}
|
|
28565
|
+
]
|
|
28566
|
+
},
|
|
28567
|
+
{
|
|
28568
|
+
title: "Utility",
|
|
28569
|
+
facts: [
|
|
28570
|
+
["scored assets", int(utility.count)],
|
|
28571
|
+
["never used", int(utility.neverUsed)]
|
|
28572
|
+
],
|
|
28573
|
+
tables: [
|
|
28574
|
+
{
|
|
28575
|
+
title: "Histogram",
|
|
28576
|
+
headers: ["bucket", "count"],
|
|
28577
|
+
rows: utility.histogram.map((h) => [h.bucket, int(h.count)]),
|
|
28578
|
+
hideWhenEmpty: true
|
|
28579
|
+
},
|
|
28580
|
+
{ title: "Lowest", headers: utilityHeaders(), rows: cut(utility.lowest).map(utilityRow) },
|
|
28581
|
+
{ title: "Highest", headers: utilityHeaders(), rows: cut(utility.highest).map(utilityRow) },
|
|
28582
|
+
{
|
|
28583
|
+
title: "Lowest outcome",
|
|
28584
|
+
headers: ["ref", "outcome", "retrievals", "negative", "accepted changes"],
|
|
28585
|
+
rows: cut(r.outcomes.lowestOutcome).map((o) => [
|
|
28586
|
+
o.ref,
|
|
28587
|
+
fixed(o.outcomeScore, 2),
|
|
28588
|
+
int(o.retrievalCount),
|
|
28589
|
+
int(o.negativeFeedbackCount),
|
|
28590
|
+
int(o.acceptedChangeCount)
|
|
28591
|
+
]),
|
|
28592
|
+
hideWhenEmpty: true
|
|
28593
|
+
}
|
|
28594
|
+
]
|
|
28595
|
+
},
|
|
28596
|
+
{
|
|
28597
|
+
title: "LLM",
|
|
28598
|
+
facts: [
|
|
28599
|
+
["calls", int(llm.calls)],
|
|
28600
|
+
["failed", int(llm.failures)],
|
|
28601
|
+
["prompt tokens", int(llm.promptTokens)],
|
|
28602
|
+
["completion tokens", int(llm.completionTokens)],
|
|
28603
|
+
["reasoning tokens", int(llm.reasoningTokens)],
|
|
28604
|
+
["total tokens", int(llm.totalTokens)],
|
|
28605
|
+
["time", ms(llm.totalDurationMs)]
|
|
28606
|
+
],
|
|
28607
|
+
tables: [
|
|
28608
|
+
{ title: "By engine", headers: LLM_HEADERS, rows: cut(llmRows(llm.byEngine)), hideWhenEmpty: true },
|
|
28609
|
+
{ title: "By process", headers: LLM_HEADERS, rows: cut(llmRows(llm.byProcess)), hideWhenEmpty: true },
|
|
28610
|
+
{ title: "By stage", headers: LLM_HEADERS, rows: cut(llmRows(llm.byStage)), hideWhenEmpty: true }
|
|
28611
|
+
]
|
|
28612
|
+
},
|
|
28613
|
+
{
|
|
28614
|
+
title: "Index",
|
|
28615
|
+
facts: [
|
|
28616
|
+
["runs", int(r.index.runs)],
|
|
28617
|
+
["median time", ms(r.index.medianMs)]
|
|
28618
|
+
],
|
|
28619
|
+
tables: [
|
|
28620
|
+
{
|
|
28621
|
+
title: "Recent runs",
|
|
28622
|
+
headers: ["at", "mode", "time"],
|
|
28623
|
+
rows: brief ? [] : r.index.recent.map((i) => [i.at, i.mode, ms(i.totalMs)]),
|
|
28624
|
+
hideWhenEmpty: true
|
|
28625
|
+
}
|
|
28626
|
+
]
|
|
28627
|
+
},
|
|
28628
|
+
{
|
|
28629
|
+
title: "Tasks",
|
|
28630
|
+
facts: [
|
|
28631
|
+
["runs", int(r.tasks.runs)],
|
|
28632
|
+
["failed", int(r.tasks.failed)],
|
|
28633
|
+
["fail rate", pct(r.tasks.failRate)]
|
|
28634
|
+
],
|
|
28635
|
+
tables: [
|
|
28636
|
+
{
|
|
28637
|
+
title: "By task",
|
|
28638
|
+
headers: ["task", "runs", "failed", "median"],
|
|
28639
|
+
rows: cut(r.tasks.byTask).map((t) => [t.taskId, int(t.runs), int(t.failed), ms(t.medianMs)]),
|
|
28640
|
+
hideWhenEmpty: true
|
|
28641
|
+
}
|
|
28642
|
+
]
|
|
28643
|
+
},
|
|
28644
|
+
{
|
|
28645
|
+
title: "Proposals",
|
|
28646
|
+
facts: [["by status", sourceFacts(r.proposals.byStatus)]],
|
|
28647
|
+
tables: [
|
|
28648
|
+
{
|
|
28649
|
+
title: "Accept rate by source",
|
|
28650
|
+
headers: ["source", "total", "accepted", "rejected", "pending", "accept rate"],
|
|
28651
|
+
rows: r.proposals.acceptRateBySource.map((p) => [
|
|
28652
|
+
p.source,
|
|
28653
|
+
int(p.total),
|
|
28654
|
+
int(p.accepted),
|
|
28655
|
+
int(p.rejected),
|
|
28656
|
+
int(p.pending),
|
|
28657
|
+
pct(p.acceptRate)
|
|
28658
|
+
]),
|
|
28659
|
+
hideWhenEmpty: true
|
|
28660
|
+
}
|
|
28661
|
+
]
|
|
28662
|
+
},
|
|
28663
|
+
{
|
|
28664
|
+
title: "Workflows",
|
|
28665
|
+
facts: [
|
|
28666
|
+
["runs", int(r.workflows.runs)],
|
|
28667
|
+
["by status", sourceFacts(r.workflows.byStatus)],
|
|
28668
|
+
["tokens", int(r.workflows.tokens)],
|
|
28669
|
+
["tokens by model", sourceFacts(r.workflows.byModel)]
|
|
28670
|
+
],
|
|
28671
|
+
tables: []
|
|
28672
|
+
}
|
|
28673
|
+
];
|
|
28674
|
+
const f = r.filters;
|
|
28675
|
+
return {
|
|
28676
|
+
window: `${r.window.since} to ${r.window.until}`,
|
|
28677
|
+
filters: `source=${f.source}`,
|
|
28678
|
+
sections,
|
|
28679
|
+
notes: r.notes
|
|
28680
|
+
};
|
|
28681
|
+
}
|
|
28682
|
+
function utilityHeaders() {
|
|
28683
|
+
return ["ref", "utility", "shows", "searches", "select rate", "last used"];
|
|
28684
|
+
}
|
|
28685
|
+
function utilityRow(u) {
|
|
28686
|
+
return [u.ref, fixed(u.utility, 2), int(u.showCount), int(u.searchCount), pct(u.selectRate), dash(u.lastUsedAt)];
|
|
28687
|
+
}
|
|
28688
|
+
|
|
28452
28689
|
// src/output/generic-render.ts
|
|
28453
28690
|
var ENVELOPE_META_KEYS = new Set(["shape", "schemaVersion"]);
|
|
28454
28691
|
function flattenForText(value, path11, lines) {
|
|
@@ -29049,6 +29286,7 @@ var PASSTHROUGH_COMMANDS = [
|
|
|
29049
29286
|
"info",
|
|
29050
29287
|
"lint",
|
|
29051
29288
|
"list",
|
|
29289
|
+
"metrics",
|
|
29052
29290
|
"models",
|
|
29053
29291
|
"proposal-accept-batch",
|
|
29054
29292
|
"proposal-drain",
|
|
@@ -30323,10 +30561,10 @@ function formatHealthPlain(r, detail) {
|
|
|
30323
30561
|
`).trim();
|
|
30324
30562
|
}
|
|
30325
30563
|
// src/output/text/lint-format.ts
|
|
30326
|
-
function glyphFor(
|
|
30327
|
-
if (
|
|
30564
|
+
function glyphFor(fixed2) {
|
|
30565
|
+
if (fixed2 === "failed")
|
|
30328
30566
|
return { glyph: "✗", severityRank: 0 };
|
|
30329
|
-
if (
|
|
30567
|
+
if (fixed2 === true)
|
|
30330
30568
|
return { glyph: "✓", severityRank: 2 };
|
|
30331
30569
|
return { glyph: "⚠", severityRank: 1 };
|
|
30332
30570
|
}
|
|
@@ -30345,17 +30583,17 @@ function renderIssueSection(title, issues) {
|
|
|
30345
30583
|
function formatLintPlain(r) {
|
|
30346
30584
|
if (r === null || typeof r !== "object")
|
|
30347
30585
|
return null;
|
|
30348
|
-
const
|
|
30586
|
+
const fixed2 = Array.isArray(r.fixed) ? r.fixed : [];
|
|
30349
30587
|
const flagged = Array.isArray(r.flagged) ? r.flagged : [];
|
|
30350
30588
|
const warnings = Array.isArray(r.warnings) ? r.warnings : [];
|
|
30351
30589
|
const summary = r.summary;
|
|
30352
30590
|
const lines = [];
|
|
30353
30591
|
if (typeof r.ok === "boolean")
|
|
30354
30592
|
lines.push(`ok: ${r.ok}`);
|
|
30355
|
-
lines.push(`summary: fixed=${summary?.fixed ??
|
|
30593
|
+
lines.push(`summary: fixed=${summary?.fixed ?? fixed2.length} flagged=${summary?.flagged ?? flagged.length}` + ` warnings=${summary?.warnings ?? warnings.length}`);
|
|
30356
30594
|
lines.push("", ...renderIssueSection("flagged", flagged));
|
|
30357
30595
|
lines.push("", ...renderIssueSection("warnings", warnings));
|
|
30358
|
-
lines.push("", ...renderIssueSection("fixed",
|
|
30596
|
+
lines.push("", ...renderIssueSection("fixed", fixed2));
|
|
30359
30597
|
return lines.join(`
|
|
30360
30598
|
`).trim();
|
|
30361
30599
|
}
|
|
@@ -36816,8 +37054,8 @@ function getConfigLockPath() {
|
|
|
36816
37054
|
}
|
|
36817
37055
|
var CONFIG_LOCK_MAX_RETRIES = 40;
|
|
36818
37056
|
var CONFIG_LOCK_RETRY_DELAY_MS = 50;
|
|
36819
|
-
function sleepSyncMs(
|
|
36820
|
-
sleepSync(
|
|
37057
|
+
function sleepSyncMs(ms2) {
|
|
37058
|
+
sleepSync(ms2);
|
|
36821
37059
|
}
|
|
36822
37060
|
function acquireConfigLock() {
|
|
36823
37061
|
const lockPath = getConfigLockPath();
|
|
@@ -53175,6 +53413,40 @@ var lintFormatters = [{ command: "lint", handler: (r) => formatLintPlain(r) }];
|
|
|
53175
53413
|
// src/output/text/list.ts
|
|
53176
53414
|
var listFormatters = [{ command: "list", handler: (r) => formatListPlain(r) }];
|
|
53177
53415
|
|
|
53416
|
+
// src/output/text/metrics.ts
|
|
53417
|
+
var flat = (s) => s.replace(/\s*\r?\n\s*/g, " ");
|
|
53418
|
+
function renderTable2(table) {
|
|
53419
|
+
const all = [table.headers, ...table.rows].map((r) => r.map(flat));
|
|
53420
|
+
const widths = table.headers.map((_, i) => Math.max(...all.map((r) => (r[i] ?? "").length)));
|
|
53421
|
+
const line = (r) => ` ${r.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")}`.trimEnd();
|
|
53422
|
+
return [`${table.title}:`, ...all.map(line)];
|
|
53423
|
+
}
|
|
53424
|
+
function formatMetricsPlain(result, detail) {
|
|
53425
|
+
if (!isMetricsResult(result))
|
|
53426
|
+
return null;
|
|
53427
|
+
const view = buildMetricsView(result, detail);
|
|
53428
|
+
const lines = [`akm metrics ${view.window}`, `filters: ${view.filters}`];
|
|
53429
|
+
for (const section of view.sections) {
|
|
53430
|
+
lines.push("", section.title.toUpperCase());
|
|
53431
|
+
const labelWidth = Math.max(0, ...section.facts.map(([label]) => label.length));
|
|
53432
|
+
for (const [label, value] of section.facts)
|
|
53433
|
+
lines.push(` ${label.padEnd(labelWidth)} ${value}`);
|
|
53434
|
+
for (const table of section.tables) {
|
|
53435
|
+
if (table.rows.length > 0)
|
|
53436
|
+
lines.push("", ...renderTable2(table));
|
|
53437
|
+
else if (!table.hideWhenEmpty)
|
|
53438
|
+
lines.push("", `${table.title}: (none)`);
|
|
53439
|
+
}
|
|
53440
|
+
}
|
|
53441
|
+
if (view.notes.length > 0)
|
|
53442
|
+
lines.push("", "NOTES", ...view.notes.map((n) => ` - ${flat(n)}`));
|
|
53443
|
+
return lines.join(`
|
|
53444
|
+
`);
|
|
53445
|
+
}
|
|
53446
|
+
var metricsFormatters = [
|
|
53447
|
+
{ command: "metrics", handler: (r, detail) => formatMetricsPlain(r, detail) }
|
|
53448
|
+
];
|
|
53449
|
+
|
|
53178
53450
|
// src/output/text/migrate.ts
|
|
53179
53451
|
function planGlyph(status) {
|
|
53180
53452
|
switch (status) {
|
|
@@ -53309,6 +53581,7 @@ var BUILT_IN_TEXT_FORMATTERS = [
|
|
|
53309
53581
|
...proposalProducerFormatters,
|
|
53310
53582
|
...infoFormatters,
|
|
53311
53583
|
...healthFormatters,
|
|
53584
|
+
...metricsFormatters,
|
|
53312
53585
|
...improveReportFormatters,
|
|
53313
53586
|
...lintFormatters,
|
|
53314
53587
|
...configFormatters,
|