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.
Files changed (36) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/STABILITY.md +4 -0
  3. package/dist/assets/hints/cli-hints-full.md +3 -0
  4. package/dist/assets/hints/cli-hints-short.md +1 -0
  5. package/dist/assets/templates/html/metrics.html +977 -0
  6. package/dist/cli/shared.js +5 -4
  7. package/dist/cli.js +11 -1
  8. package/dist/commands/health/accept-rate.js +8 -4
  9. package/dist/commands/health/html-report.js +3 -8
  10. package/dist/commands/health/llm-usage.js +17 -6
  11. package/dist/commands/health/renderers.js +4 -4
  12. package/dist/commands/improve/improve-report.js +4 -2
  13. package/dist/commands/improve/improve.js +22 -10
  14. package/dist/commands/metrics/collect.js +439 -0
  15. package/dist/commands/metrics/html-report.js +82 -0
  16. package/dist/commands/metrics/md-report.js +44 -0
  17. package/dist/commands/metrics/metrics-cli.js +213 -0
  18. package/dist/commands/metrics/report-view.js +243 -0
  19. package/dist/commands/metrics/types.js +4 -0
  20. package/dist/commands/read/search.js +5 -0
  21. package/dist/indexer/indexer.js +64 -14
  22. package/dist/indexer/usage/usage-events.js +3 -1
  23. package/dist/integrations/session-logs/pre-filter.js +1 -0
  24. package/dist/llm/usage-persist.js +22 -11
  25. package/dist/llm/usage-telemetry.js +4 -0
  26. package/dist/output/html-render.js +15 -8
  27. package/dist/output/shapes/passthrough.js +1 -0
  28. package/dist/output/text/metrics.js +39 -0
  29. package/dist/output/text.js +2 -0
  30. package/dist/scripts/akm-migrate-node.js +281 -8
  31. package/dist/scripts/akm-migrate.js +281 -8
  32. package/dist/storage/repositories/index-utility-repository.js +24 -0
  33. package/dist/storage/repositories/metrics-repository.js +80 -0
  34. package/docs/reference/cli.md +63 -4
  35. package/docs/reference/data-and-telemetry.md +40 -7
  36. 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 clears the sink — call it in a
63
- * `finally` block so per-run wiring does not leak across runs (and so the
64
- * test-isolation harness sees a clean sink between tests).
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
- clearLlmUsageSink();
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 standalone entry points (`akm proposal drain`)
100
- * that may also run as a sub-step of `akm improve`. When invoked inside an
101
- * enclosing run the existing per-run sink keeps ownership; the returned
102
- * disposer then does nothing, so the enclosing run's `finally` still clears it.
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` is
9
- * the only command with a bespoke `<command>.html` template (substituted here
10
- * via plain `%%TOKEN%%` string replacement — no template engine, by design);
11
- * `src/cli/shared.ts`'s `output()` calls it directly for `command === "health"`
12
- * and every other command falls back to `renderGenericHtml` (`./generic-render`).
13
- * `--format html` therefore works for every command (D7) — this module's
14
- * template path is just health's opt-in bespoke rendering, not a format-wide
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('"', "&quot;")
98
100
  .replaceAll("'", "&#39;");
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.
@@ -50,6 +50,7 @@ const PASSTHROUGH_COMMANDS = [
50
50
  "info",
51
51
  "lint",
52
52
  "list",
53
+ "metrics",
53
54
  "models",
54
55
  "proposal-accept-batch",
55
56
  "proposal-drain",
@@ -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
+ ];
@@ -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(fixed) {
30327
- if (fixed === "failed")
30564
+ function glyphFor(fixed2) {
30565
+ if (fixed2 === "failed")
30328
30566
  return { glyph: "✗", severityRank: 0 };
30329
- if (fixed === true)
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 fixed = Array.isArray(r.fixed) ? r.fixed : [];
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 ?? fixed.length} flagged=${summary?.flagged ?? flagged.length}` + ` warnings=${summary?.warnings ?? warnings.length}`);
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", 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(ms) {
36820
- sleepSync(ms);
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,