akm-cli 0.9.0-beta.5 → 0.9.0-beta.51
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 +711 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +6 -2
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +38 -10
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +27 -11
- package/dist/commands/agent/agent-dispatch.js +2 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +17 -4
- package/dist/commands/config-cli.js +18 -2
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +33 -26
- package/dist/commands/env/secret-cli.js +36 -22
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +76 -72
- package/dist/commands/health/checks.js +49 -1
- package/dist/commands/health/html-report.js +422 -80
- package/dist/commands/health.js +386 -9
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +81 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +635 -660
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +159 -69
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +124 -2
- package/dist/commands/improve/extract-prompt.js +39 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +389 -40
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +138 -7
- package/dist/commands/improve/improve-cli.js +36 -61
- package/dist/commands/improve/improve-profiles.js +14 -0
- package/dist/commands/improve/improve-result-file.js +14 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2498
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1083 -0
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/preparation.js +1966 -0
- package/dist/commands/improve/proactive-maintenance.js +115 -0
- package/dist/commands/improve/procedural.js +418 -0
- package/dist/commands/improve/recombine.js +850 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +183 -40
- package/dist/commands/improve/salience.js +438 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +38 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/proposal/drain-policies.js +5 -0
- package/dist/commands/proposal/drain.js +43 -50
- package/dist/commands/proposal/proposal-cli.js +21 -31
- package/dist/commands/proposal/proposal.js +5 -0
- package/dist/commands/proposal/propose.js +7 -2
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +189 -63
- package/dist/commands/read/curate.js +414 -94
- package/dist/commands/read/knowledge.js +6 -3
- package/dist/commands/read/search-cli.js +9 -4
- package/dist/commands/read/search.js +10 -6
- package/dist/commands/read/show.js +86 -7
- package/dist/commands/sources/init.js +49 -17
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +43 -45
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/stash-cli.js +28 -40
- package/dist/commands/sources/stash-skeleton.js +23 -8
- package/dist/commands/tasks/tasks-cli.js +19 -27
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +21 -35
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +18 -2
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +0 -5
- package/dist/core/config/config-migration.js +12 -11
- package/dist/core/config/config-schema.js +340 -56
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +28 -7
- package/dist/core/events.js +3 -7
- package/dist/core/improve-types.js +11 -8
- package/dist/core/logs-db.js +10 -66
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +3 -0
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +714 -0
- package/dist/core/state-db.js +525 -474
- package/dist/indexer/db/db.js +439 -247
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +218 -4
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +123 -221
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +19 -6
- package/dist/indexer/search/ranking-contributors.js +22 -0
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/indexer/search/search-source.js +17 -18
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/integrations/agent/config.js +6 -53
- package/dist/integrations/agent/index.js +2 -18
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/harnesses/claude/session-log.js +11 -1
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +45 -15
- package/dist/llm/embedder.js +42 -3
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/feature-gate.js +8 -4
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +74 -2
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/passthrough.js +0 -1
- package/dist/output/text/helpers.js +16 -1
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +22 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2617 -1961
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +759 -510
- package/dist/setup/setup.js +29 -8
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/registry-cache.js +92 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +5 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +37 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +1 -117
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +3 -2
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +13 -11
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* (#582). Ports the external akm-health-report skill's collect.py + render.py
|
|
7
7
|
* to TypeScript so the report is generated in-process (no python, no
|
|
8
8
|
* shell-out). The template (`src/assets/templates/html/health.html`) is a
|
|
9
|
-
*
|
|
9
|
+
* strict superset of the skill's report.html; this module computes the 17
|
|
10
10
|
* `%%TOKEN%%` replacements it consumes.
|
|
11
11
|
*
|
|
12
12
|
* Determinism: nothing here depends on Date.now()/Math.random(). Runs are
|
|
@@ -17,13 +17,44 @@ import fs from "node:fs";
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import { escapeHtml } from "../../output/html-render.js";
|
|
19
19
|
import { getDirname } from "../../runtime.js";
|
|
20
|
+
import { pkgVersion } from "../../version.js";
|
|
21
|
+
/**
|
|
22
|
+
* Distill skip-reasons hidden from the breakdown chart. `no new signal since
|
|
23
|
+
* last proposal` is the steady-state "nothing changed, nothing to do" outcome —
|
|
24
|
+
* it dominates the histogram and drowns out the actionable reasons, so it is
|
|
25
|
+
* intentionally excluded from the chart (the count still lives in the data).
|
|
26
|
+
*/
|
|
27
|
+
const DISTILL_REASONS_HIDDEN = new Set(["no new signal since last proposal"]);
|
|
20
28
|
const ECHARTS_CDN = "https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";
|
|
21
29
|
const ECHARTS_VENDOR_PATH = path.join(getDirname(import.meta.url), "../../assets/templates/html/vendor/echarts.min.js");
|
|
22
30
|
// ── Small formatters (ports of render.py helpers) ───────────────────────────
|
|
23
31
|
const esc = escapeHtml;
|
|
32
|
+
/** Emit a <time> element that the browser's JS will reformat to the viewer's local timezone. */
|
|
33
|
+
function isoTimeTag(iso) {
|
|
34
|
+
const fallback = iso.slice(0, 16).replace("T", " ");
|
|
35
|
+
return `<time data-iso="${esc(iso)}">${esc(fallback)}</time>`;
|
|
36
|
+
}
|
|
24
37
|
function num(value) {
|
|
25
38
|
return Math.round(value).toLocaleString("en-US");
|
|
26
39
|
}
|
|
40
|
+
/** Compact token/count formatter, e.g. 127345 → "127K", 1_500_000 → "1.5M". */
|
|
41
|
+
function compact(value) {
|
|
42
|
+
const v = Math.round(value);
|
|
43
|
+
if (Math.abs(v) >= 1_000_000)
|
|
44
|
+
return `${(v / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
|
|
45
|
+
if (Math.abs(v) >= 1_000)
|
|
46
|
+
return `${(v / 1_000).toFixed(1).replace(/\.0$/, "")}K`;
|
|
47
|
+
return String(v);
|
|
48
|
+
}
|
|
49
|
+
/** Humanize a camelCase / kebab / snake enum into reader-facing text. */
|
|
50
|
+
function humanize(raw) {
|
|
51
|
+
return raw
|
|
52
|
+
.replace(/[-_]/g, " ")
|
|
53
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
54
|
+
.replace(/\s+/g, " ")
|
|
55
|
+
.trim()
|
|
56
|
+
.replace(/^./, (c) => c.toUpperCase());
|
|
57
|
+
}
|
|
27
58
|
function fmtMs(ms) {
|
|
28
59
|
return ms ? `${(ms / 60000).toFixed(1)}m` : "—";
|
|
29
60
|
}
|
|
@@ -60,6 +91,7 @@ function reshapeRun(r) {
|
|
|
60
91
|
const geMs = ge.durationMs || 0;
|
|
61
92
|
return {
|
|
62
93
|
id: r.id,
|
|
94
|
+
taskId: r.taskId ?? "manual",
|
|
63
95
|
startedAt: r.startedAt,
|
|
64
96
|
completedAt: r.completedAt,
|
|
65
97
|
wallTimeMs: wall,
|
|
@@ -144,11 +176,14 @@ function deltaPill(deltas, key, lowerIsBetter = false) {
|
|
|
144
176
|
const signed = `${raw > 0 ? "+" : ""}${Math.round(raw)}%`;
|
|
145
177
|
return `<span class="trend-pill ${cls}">${arrow} ${signed}</span>`;
|
|
146
178
|
}
|
|
147
|
-
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
179
|
+
const PRIO_RANK = { P1: 0, P2: 1, P3: 2 };
|
|
180
|
+
function actionItemCard(item) {
|
|
181
|
+
const icon = item.cls === "fail" ? "🔴" : item.prio === "P3" ? "🟡" : "⚠️";
|
|
182
|
+
const remedy = item.remedy ? `<div class="remedy">Fix: <code>${esc(item.remedy)}</code></div>` : "";
|
|
183
|
+
return (`<div class="advisory ${item.cls}"><div class="advisory-icon">${icon}</div>` +
|
|
184
|
+
`<div class="advisory-body">` +
|
|
185
|
+
`<div class="title"><span class="prio ${item.prio.toLowerCase()}">${item.prio}</span>${item.title}</div>` +
|
|
186
|
+
`<div class="desc">${item.descHtml}</div>${remedy}</div></div>`);
|
|
152
187
|
}
|
|
153
188
|
function passCard(title, desc) {
|
|
154
189
|
return ('<div class="advisory" style="border-left:3px solid var(--green);">' +
|
|
@@ -168,6 +203,43 @@ function readSemSearch(advisories) {
|
|
|
168
203
|
return { blocked, detail: `${num(entries)} entries, ${num(embeddings)} embeddings` };
|
|
169
204
|
}
|
|
170
205
|
// ── ECharts delivery ─────────────────────────────────────────────────────────
|
|
206
|
+
/** Parse an akm window string (`24h`, `7d`, `30m`, `2w`) to milliseconds; 0 if unparseable. */
|
|
207
|
+
function windowToMs(window) {
|
|
208
|
+
const m = /^(\d+)\s*([mhdw])$/i.exec(window.trim());
|
|
209
|
+
if (!m)
|
|
210
|
+
return 0;
|
|
211
|
+
const n = Number(m[1]);
|
|
212
|
+
const mult = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 }[m[2].toLowerCase()] ?? 0;
|
|
213
|
+
return n * mult;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Build the time-slice `<option>`s for the report's filter bar, DERIVED from the
|
|
217
|
+
* actual report window so the choices always make sense (the old hard-coded
|
|
218
|
+
* 1d–21d list was useless on a 24h or 7d report). "All" is the default; the
|
|
219
|
+
* sub-window options carry their cutoff in milliseconds (consumed by
|
|
220
|
+
* filteredRuns), largest first, only those strictly shorter than the window.
|
|
221
|
+
*/
|
|
222
|
+
function buildSliceOptions(window) {
|
|
223
|
+
const windowMs = windowToMs(window);
|
|
224
|
+
const HOUR = 3_600_000;
|
|
225
|
+
const DAY = 86_400_000;
|
|
226
|
+
const candidates = [
|
|
227
|
+
[6 * HOUR, "6h"],
|
|
228
|
+
[12 * HOUR, "12h"],
|
|
229
|
+
[DAY, "1d"],
|
|
230
|
+
[3 * DAY, "3d"],
|
|
231
|
+
[7 * DAY, "7d"],
|
|
232
|
+
[14 * DAY, "14d"],
|
|
233
|
+
];
|
|
234
|
+
const subs = candidates
|
|
235
|
+
.filter(([ms]) => windowMs > 0 && ms < windowMs)
|
|
236
|
+
.sort((a, b) => b[0] - a[0])
|
|
237
|
+
.slice(0, 4);
|
|
238
|
+
const opts = [`<option value="all" selected>All (${esc(window)})</option>`];
|
|
239
|
+
for (const [ms, label] of subs)
|
|
240
|
+
opts.push(`<option value="${ms}">Last ${label}</option>`);
|
|
241
|
+
return opts.join("\n ");
|
|
242
|
+
}
|
|
171
243
|
function buildEchartsTag(opts) {
|
|
172
244
|
const mode = opts.echarts ?? (process.env.AKM_ECHARTS === "cdn" ? "cdn" : "inline");
|
|
173
245
|
if (mode === "cdn")
|
|
@@ -182,7 +254,8 @@ function buildEchartsTag(opts) {
|
|
|
182
254
|
* Compute all 17 `%%TOKEN%%` replacements for the health HTML template.
|
|
183
255
|
* There is deliberately NO standalone `%%OVERALL_STATUS%%` token — the
|
|
184
256
|
* overall status is embedded in the pre-rendered badge / exec-summary
|
|
185
|
-
* fragments, matching the skill template.
|
|
257
|
+
* fragments, matching the skill template. Advisories and "what to watch" are
|
|
258
|
+
* merged + de-duplicated into a single prioritized `%%ACTION_ITEMS_HTML%%`.
|
|
186
259
|
*/
|
|
187
260
|
export function buildHealthHtmlReplacements(result, opts) {
|
|
188
261
|
const deltas = opts.deltas ?? {};
|
|
@@ -196,6 +269,31 @@ export function buildHealthHtmlReplacements(result, opts) {
|
|
|
196
269
|
const mi = improve.memoryInference;
|
|
197
270
|
const ge = improve.graphExtraction;
|
|
198
271
|
const wallTime = improve.wallTime;
|
|
272
|
+
// WS-5: perf telemetry, coverage, and degradation metrics.
|
|
273
|
+
const perf = improve.perfTelemetry ?? {
|
|
274
|
+
dedupPoolSize: 0,
|
|
275
|
+
llmPoolSize: 0,
|
|
276
|
+
judgedCacheSkipped: 0,
|
|
277
|
+
embedMs: 0,
|
|
278
|
+
embedCacheHits: 0,
|
|
279
|
+
embedCacheMisses: 0,
|
|
280
|
+
overBudgetRuns: 0,
|
|
281
|
+
runsWithTelemetry: 0,
|
|
282
|
+
};
|
|
283
|
+
const coverage = improve.coverage;
|
|
284
|
+
const degradation = improve.degradation;
|
|
285
|
+
// #576: real per-stage LLM token/time accounting (replaces the GPU-time
|
|
286
|
+
// proxy). Optional-guarded so reports built from older health JSON without
|
|
287
|
+
// the aggregate still render.
|
|
288
|
+
const llm = result.metrics.llmUsage ?? {
|
|
289
|
+
calls: 0,
|
|
290
|
+
totalDurationMs: 0,
|
|
291
|
+
promptTokens: 0,
|
|
292
|
+
completionTokens: 0,
|
|
293
|
+
totalTokens: 0,
|
|
294
|
+
reasoningTokens: 0,
|
|
295
|
+
byStage: {},
|
|
296
|
+
};
|
|
199
297
|
const totalRuns = runs.length;
|
|
200
298
|
const failedRuns = runs.filter((r) => !r.ok).length;
|
|
201
299
|
const invoked = improve.invoked || totalRuns;
|
|
@@ -212,11 +310,23 @@ export function buildHealthHtmlReplacements(result, opts) {
|
|
|
212
310
|
// ── Meta (collect.py steps 10-11) ──────────────────────────────────────────
|
|
213
311
|
const sinceIso = result.since;
|
|
214
312
|
const reportDate = sinceIso.slice(0, 10);
|
|
215
|
-
const sinceHuman = sinceIso ? `${sinceIso
|
|
313
|
+
const sinceHuman = sinceIso ? `${isoTimeTag(sinceIso)} → now` : `last ${esc(opts.window)}`;
|
|
216
314
|
const reportTitle = reportDate ? `AKM Health Report — ${reportDate}` : "AKM Health Report";
|
|
217
315
|
const lastRun = runs[runs.length - 1];
|
|
218
316
|
const generatedAt = lastRun ? lastRun.completedAt || lastRun.startedAt || sinceIso : sinceIso;
|
|
219
317
|
const latest = [...runs].reverse().find((r) => r.ok) ?? lastRun;
|
|
318
|
+
// Freshness: surface the newest run's timestamp + the generated-at anchor in
|
|
319
|
+
// the exec card. Staleness is computed deterministically (no Date.now()) from
|
|
320
|
+
// the gap between the window start (`since`) and the newest run we have: if no
|
|
321
|
+
// run landed in the window, or the newest run sits in the first 25% of a
|
|
322
|
+
// window wider than the 6h threshold (i.e. a long idle tail), we flag stale.
|
|
323
|
+
const STALE_MS = 6 * 60 * 60 * 1000;
|
|
324
|
+
const latestRunMs = lastRun ? Date.parse(lastRun.completedAt || lastRun.startedAt) : NaN;
|
|
325
|
+
const sinceMs = Date.parse(sinceIso);
|
|
326
|
+
const generatedMs = Date.parse(generatedAt);
|
|
327
|
+
const idleTailMs = Number.isFinite(latestRunMs) && Number.isFinite(generatedMs) ? Math.max(0, generatedMs - latestRunMs) : 0;
|
|
328
|
+
const isStale = totalRuns === 0 || (Number.isFinite(latestRunMs) && Number.isFinite(sinceMs) && idleTailMs > STALE_MS);
|
|
329
|
+
const latestRunHuman = lastRun ? isoTimeTag(lastRun.completedAt || lastRun.startedAt) : "—";
|
|
220
330
|
// ── Status badges ──────────────────────────────────────────────────────────
|
|
221
331
|
const badgeByStatus = {
|
|
222
332
|
pass: { badge: "badge-pass", dot: "dot-pass", label: "PASS" },
|
|
@@ -238,7 +348,7 @@ export function buildHealthHtmlReplacements(result, opts) {
|
|
|
238
348
|
li("MI yield rate", miYieldRate),
|
|
239
349
|
li("MI written", num(miWritten)),
|
|
240
350
|
li("Consolidation promoted", num(cons.promoted)),
|
|
241
|
-
li("Consolidation
|
|
351
|
+
li("Consolidation judged: no action", `<abbr title="Candidates the consolidator reviewed but intentionally left unchanged (the 'judgedNoAction' field).">${num(cons.judgedNoAction)}</abbr>`),
|
|
242
352
|
li("Chunk failure", chunkFail),
|
|
243
353
|
li("Median wall time", fmtMs(wallTime.medianMs)),
|
|
244
354
|
li("P95 wall time", fmtMs(wallTime.p95Ms)),
|
|
@@ -259,12 +369,12 @@ export function buildHealthHtmlReplacements(result, opts) {
|
|
|
259
369
|
const snapRows = latest
|
|
260
370
|
? [
|
|
261
371
|
li("Run id", `<code>${esc(latest.id.slice(0, 28))}</code>`),
|
|
262
|
-
li("Completed",
|
|
372
|
+
li("Completed", isoTimeTag(latest.completedAt || latest.startedAt)),
|
|
263
373
|
li("Status", latest.ok ? "✅ ok" : "❌ failed"),
|
|
264
374
|
li("Wall time", fmtMs(latest.wallTimeMs)),
|
|
265
375
|
li("Reflect ok/fail", `${latest.reflectOk} / ${latest.reflectFailed}`),
|
|
266
376
|
li("Promoted", String(latest.promoted)),
|
|
267
|
-
li("
|
|
377
|
+
li("Judged: no action", `<abbr title="Candidates reviewed but intentionally left unchanged on this run.">${latest.judgedNoAction}</abbr>`),
|
|
268
378
|
li("MI written", String(latest.miWritten)),
|
|
269
379
|
li("Graph entities/relations", `${latest.geEntities} / ${latest.geRelations}`),
|
|
270
380
|
].join("")
|
|
@@ -273,16 +383,50 @@ export function buildHealthHtmlReplacements(result, opts) {
|
|
|
273
383
|
li("Report window", esc(opts.window)),
|
|
274
384
|
li("Compare window", esc(opts.compare)),
|
|
275
385
|
li("Runs", `${num(totalRuns)} (${failedRuns} failed)`),
|
|
276
|
-
li("Stash derived", num(improve.memorySummary.derived)),
|
|
277
|
-
li("Stash eligible", num(improve.memorySummary.eligible)),
|
|
386
|
+
li("Stash derived", `<abbr title="Whole-stash recount of derived assets at report time — not a per-run sum.">${num(improve.memorySummary.derived)}</abbr>`),
|
|
387
|
+
li("Stash eligible", `<abbr title="Whole-stash recount of eligible assets at report time — not a per-run sum.">${num(improve.memorySummary.eligible)}</abbr>`),
|
|
278
388
|
li("Pending proposals", String(proposals.length)),
|
|
279
389
|
li("Semantic search", sem.blocked ? "BLOCKED" : "OK"),
|
|
280
390
|
].join("");
|
|
281
391
|
const overallEmoji = trend.overall === "improving" ? "📈" : trend.overall === "degrading" ? "📉" : "↔️";
|
|
392
|
+
// ── Synthesized verdict (one sentence + 2-3 drivers) ───────────────────────
|
|
393
|
+
// The verdict WORD is the authoritative health status (hard checks). Concerns
|
|
394
|
+
// (failed improve runs, blocked search, pending proposals) are advisory and do
|
|
395
|
+
// NOT gate that word, so we must not pair a green PASS with red "drivers" as
|
|
396
|
+
// if they were failures. When the status is PASS we frame concerns as "watch:"
|
|
397
|
+
// alongside the trend; only a degraded status (WARN/FAIL) leads with them.
|
|
398
|
+
const concerns = [];
|
|
399
|
+
if (failedRuns > 0)
|
|
400
|
+
concerns.push(`${failedRuns} failed run${failedRuns === 1 ? "" : "s"}`);
|
|
401
|
+
if (sem.blocked)
|
|
402
|
+
concerns.push("semantic search blocked");
|
|
403
|
+
if (proposals.length > 0)
|
|
404
|
+
concerns.push(`${proposals.length} pending proposal${proposals.length === 1 ? "" : "s"}`);
|
|
405
|
+
const trendClause = trend.overall === "improving"
|
|
406
|
+
? "throughput and latency improving"
|
|
407
|
+
: trend.overall === "degrading"
|
|
408
|
+
? "throughput or latency degrading"
|
|
409
|
+
: "throughput and latency steady";
|
|
410
|
+
const verdictWord = badge.label;
|
|
411
|
+
let verdictRest;
|
|
412
|
+
if (result.status === "pass") {
|
|
413
|
+
const watch = concerns.length > 0 ? `; watch: ${concerns.slice(0, 3).join(", ")}` : "";
|
|
414
|
+
verdictRest = `healthy, ${trendClause}${watch}`;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
const lead = concerns.length > 0 ? concerns.slice(0, 3).join("; ") : trendClause;
|
|
418
|
+
verdictRest = trend.overall === "degrading" && concerns.length > 0 ? `${lead}; ${trendClause}` : lead;
|
|
419
|
+
}
|
|
420
|
+
const verdictSentence = `${verdictWord} — ${esc(verdictRest)}.`;
|
|
421
|
+
const verdictHtml = `<div class="verdict ${result.status}"><b>Verdict:</b> ${verdictSentence}</div>`;
|
|
422
|
+
// ── Freshness line ─────────────────────────────────────────────────────────
|
|
423
|
+
const freshnessHtml = `<div class="freshness${isStale ? " stale" : ""}">${isStale ? "⚠️ Stale: " : ""}Latest run ${latestRunHuman} · generated ${isoTimeTag(generatedAt)}${isStale ? " — no recent activity (newest run older than the 6h freshness threshold)." : "."}</div>`;
|
|
282
424
|
const execSummary = `
|
|
283
425
|
<h2>${overallEmoji} Executive Summary
|
|
284
426
|
<span class="badge-pill ${badge.badge}" style="font-size:11px;">
|
|
285
427
|
<span class="dot ${badge.dot}"></span>${badge.label}</span></h2>
|
|
428
|
+
${verdictHtml}
|
|
429
|
+
${freshnessHtml}
|
|
286
430
|
<div class="exec-grid">
|
|
287
431
|
<div>
|
|
288
432
|
<h4>Quick Numbers</h4>
|
|
@@ -306,143 +450,341 @@ export function buildHealthHtmlReplacements(result, opts) {
|
|
|
306
450
|
<div class="overall">Overall trend: <b>${esc(trend.overall)}</b> ${overallEmoji}
|
|
307
451
|
· based on decision quality, output volume, failures, and latency vs the prior window.</div>`.trim();
|
|
308
452
|
// ── KPI cards ──────────────────────────────────────────────────────────────
|
|
453
|
+
// Color is a health SIGNAL, not decoration: green/yellow/red where a card has
|
|
454
|
+
// a meaningful threshold; "neutral" for purely-informational counts. Cards are
|
|
455
|
+
// ordered by operator priority (failures first, informational counts last).
|
|
309
456
|
const semValue = sem.blocked ? "BLOCKED" : "OK";
|
|
310
|
-
const semColor = sem.blocked ? "
|
|
457
|
+
const semColor = sem.blocked ? "red" : "green";
|
|
311
458
|
const semStyle = sem.blocked ? "font-size:18px;" : "";
|
|
459
|
+
const completionPct = invoked ? (100 * completed) / invoked : 100;
|
|
460
|
+
const completionColor = completionPct >= 99 ? "green" : completionPct >= 90 ? "yellow" : "red";
|
|
461
|
+
const llmTokensCompact = compact(llm.totalTokens);
|
|
312
462
|
const kpiCard = (color, label, value, sub, valueStyle = "") => `<div class="kpi-card ${color}">
|
|
313
463
|
<div class="label">${label}</div>
|
|
314
464
|
<div class="value"${valueStyle ? ` style="${valueStyle}"` : ""}>${value}</div>
|
|
315
465
|
<div class="sub">${sub}</div>
|
|
316
466
|
</div>`;
|
|
317
467
|
const kpiCards = [
|
|
318
|
-
kpiCard(failedRuns === 0 ? "green" : "yellow", "Completion Rate", completionRate, `${num(completed)} / ${num(invoked)} invoked`),
|
|
319
468
|
kpiCard(failedRuns === 0 ? "green" : "red", "Failed Runs", String(failedRuns), `of ${num(totalRuns)} runs · ${taskFailRate} task fail`),
|
|
469
|
+
kpiCard(completionColor, "Completion Rate", completionRate, `${num(completed)} / ${num(invoked)} invoked`),
|
|
470
|
+
kpiCard("neutral", "Median Duration", `${medianDurMin}m`, `p95 = ${p95DurMin}m`),
|
|
320
471
|
kpiCard("blue", "Total Promoted", num(cons.promoted), `avg ${avgPromoted} / run`),
|
|
321
472
|
kpiCard("blue", "MI Written", num(miWritten), `${miYieldRate} yield rate`),
|
|
322
473
|
kpiCard("purple", "Graph Entities", num(ge.entities), `+${num(ge.relations)} relations`),
|
|
323
|
-
kpiCard("
|
|
324
|
-
|
|
474
|
+
kpiCard("neutral", "Stash Derived", num(improve.memorySummary.derived), `of ${num(improve.memorySummary.eligible)} eligible (whole-stash)`),
|
|
475
|
+
// #576: real LLM work — duration leads, tokens compact, not a GPU proxy.
|
|
476
|
+
kpiCard(llm.calls > 0 ? "purple" : "neutral", "🧠 LLM Work", `${llmTokensCompact} tok`, `${fmtMs(llm.totalDurationMs)} · ${num(llm.calls)} calls · ${compact(llm.reasoningTokens)} reasoning`),
|
|
325
477
|
kpiCard(semColor, "Semantic Search", semValue, esc(sem.detail), semStyle),
|
|
326
|
-
kpiCard("yellow", "Pending Proposals", String(proposals.length), `from ${esc(opts.window)} batch`),
|
|
478
|
+
kpiCard(proposals.length > 0 ? "yellow" : "neutral", "Pending Proposals", String(proposals.length), `from ${esc(opts.window)} batch`),
|
|
327
479
|
].join("\n");
|
|
328
480
|
// ── Chart payload ──────────────────────────────────────────────────────────
|
|
329
|
-
const distillReasons = [...new Set(runs.flatMap((r) => Object.keys(r.distillByReason)))]
|
|
481
|
+
const distillReasons = [...new Set(runs.flatMap((r) => Object.keys(r.distillByReason)))]
|
|
482
|
+
.filter((reason) => !DISTILL_REASONS_HIDDEN.has(reason))
|
|
483
|
+
.sort();
|
|
330
484
|
const runsJsConst = `const RUNS = ${JSON.stringify(runs)};`;
|
|
331
485
|
// ── Summary table rows ─────────────────────────────────────────────────────
|
|
486
|
+
// Optional 4th element = a glossary tooltip rendered as <abbr title>.
|
|
332
487
|
const summaryRows = [
|
|
333
488
|
["Task fail rate", taskFailRate, "flat"],
|
|
334
489
|
["Agent fail rate", agentFailRate, "flat"],
|
|
335
490
|
["Improve completion", `${num(completed)} / ${num(invoked)}`, "flat"],
|
|
336
|
-
[
|
|
337
|
-
|
|
491
|
+
[
|
|
492
|
+
"MI yield rate",
|
|
493
|
+
miYieldRate,
|
|
494
|
+
trend.decisionQuality,
|
|
495
|
+
"Memory-inference yield: share of considered candidates that produced a written fact.",
|
|
496
|
+
],
|
|
497
|
+
["MI written", num(miWritten), trend.outputVolume, "Memory-inference: facts written this window."],
|
|
338
498
|
["Consolidation promoted", num(cons.promoted), trend.outputVolume],
|
|
339
499
|
["Consolidation merged", num(cons.merged), "flat"],
|
|
340
500
|
["Consolidation deleted", num(cons.deleted), "flat"],
|
|
341
501
|
["Consolidation contradicted", num(cons.contradicted), "flat"],
|
|
342
|
-
[
|
|
502
|
+
[
|
|
503
|
+
"Consolidation judged: no action",
|
|
504
|
+
num(cons.judgedNoAction),
|
|
505
|
+
"flat",
|
|
506
|
+
"Candidates reviewed but intentionally left unchanged (the 'judgedNoAction' field).",
|
|
507
|
+
],
|
|
343
508
|
["Chunk failure", chunkFail, "flat"],
|
|
344
509
|
["Graph entities", num(ge.entities), "up"],
|
|
345
510
|
["Graph relations", num(ge.relations), "up"],
|
|
346
|
-
[
|
|
511
|
+
[
|
|
512
|
+
"Stash derived",
|
|
513
|
+
num(improve.memorySummary.derived),
|
|
514
|
+
"up",
|
|
515
|
+
"Whole-stash recount of derived assets at report time — not a per-run sum.",
|
|
516
|
+
],
|
|
347
517
|
["Median wall time", fmtMs(wallTime.medianMs), trend.latency],
|
|
348
518
|
["P95 wall time", fmtMs(wallTime.p95Ms), trend.latency],
|
|
519
|
+
// #576: real LLM accounting (replaces the GPU-time proxy).
|
|
520
|
+
["LLM calls", num(llm.calls), "flat"],
|
|
521
|
+
["LLM total tokens", num(llm.totalTokens), "flat"],
|
|
522
|
+
["LLM prompt tokens", num(llm.promptTokens), "flat"],
|
|
523
|
+
["LLM completion tokens", num(llm.completionTokens), "flat"],
|
|
524
|
+
[
|
|
525
|
+
"LLM reasoning tokens",
|
|
526
|
+
num(llm.reasoningTokens),
|
|
527
|
+
"flat",
|
|
528
|
+
"Tokens spent on model reasoning/thinking, billed separately from prompt and completion.",
|
|
529
|
+
],
|
|
530
|
+
["LLM wall time", fmtMs(llm.totalDurationMs), trend.latency],
|
|
349
531
|
];
|
|
532
|
+
// #612 — auto-accept gate calibration. Only surface when the gate actually
|
|
533
|
+
// acted on proposals in the window (samples > 0); a default ungated install
|
|
534
|
+
// reports an empty summary and we omit the rows to keep the table parity-clean.
|
|
535
|
+
const calibration = improve.calibration;
|
|
536
|
+
if (calibration && calibration.samples > 0) {
|
|
537
|
+
summaryRows.push([
|
|
538
|
+
"Calibration samples",
|
|
539
|
+
num(calibration.samples),
|
|
540
|
+
"flat",
|
|
541
|
+
"Auto-accept gate decisions (auto-accepted + auto-rejected) the calibration join measured this window.",
|
|
542
|
+
], [
|
|
543
|
+
"Calibration accept rate",
|
|
544
|
+
String(calibration.overallAcceptRate),
|
|
545
|
+
"flat",
|
|
546
|
+
"Realized accept rate of acted-on gate decisions (auto-accepted / total acted-on).",
|
|
547
|
+
], [
|
|
548
|
+
"Calibration gap",
|
|
549
|
+
String(calibration.calibrationGap),
|
|
550
|
+
"flat",
|
|
551
|
+
"Mean predicted confidence minus realized accept rate. Positive = the gate is over-confident.",
|
|
552
|
+
]);
|
|
553
|
+
}
|
|
554
|
+
// WS-5: denominator-fixed coverage rows (only when we have real data).
|
|
555
|
+
if (coverage && !Number.isNaN(coverage.rate)) {
|
|
556
|
+
summaryRows.push([
|
|
557
|
+
"Coverage rate",
|
|
558
|
+
pct(coverage.rate, 1),
|
|
559
|
+
"flat",
|
|
560
|
+
"Accepted proposals / total stash assets (denominator-fixed). Shows what fraction of the corpus has been touched.",
|
|
561
|
+
], [
|
|
562
|
+
"Eligible fraction",
|
|
563
|
+
pct(coverage.eligibleFraction, 1),
|
|
564
|
+
"flat",
|
|
565
|
+
"Eligible assets / total stash assets. Fraction the improve pipeline actively considers.",
|
|
566
|
+
], [
|
|
567
|
+
"Coverage accepted",
|
|
568
|
+
num(coverage.acceptedProposals),
|
|
569
|
+
"up",
|
|
570
|
+
"Total accepted proposals used for the denominator-fixed coverage rate.",
|
|
571
|
+
]);
|
|
572
|
+
}
|
|
573
|
+
// WS-5: perf telemetry rows (only when at least one run reported telemetry).
|
|
574
|
+
if (perf.runsWithTelemetry > 0) {
|
|
575
|
+
const embedCacheTotal = perf.embedCacheHits + perf.embedCacheMisses;
|
|
576
|
+
const embedCacheHitRate = embedCacheTotal > 0 ? pct(perf.embedCacheHits / embedCacheTotal, 1) : "—";
|
|
577
|
+
summaryRows.push([
|
|
578
|
+
"Embed cache hit rate",
|
|
579
|
+
embedCacheHitRate,
|
|
580
|
+
"flat",
|
|
581
|
+
"Fraction of embedding lookups served from cache (>95% is healthy). Aggregated across WS-5 runs.",
|
|
582
|
+
], [
|
|
583
|
+
"Embed wall time",
|
|
584
|
+
fmtMs(perf.embedMs),
|
|
585
|
+
"flat",
|
|
586
|
+
"Cumulative embedding wall-clock time across consolidation runs in the window.",
|
|
587
|
+
], [
|
|
588
|
+
"Judged-cache skipped",
|
|
589
|
+
num(perf.judgedCacheSkipped),
|
|
590
|
+
"flat",
|
|
591
|
+
"Candidates skipped by the judged-cache (not sent to LLM). Higher = more efficient reuse of prior judgments.",
|
|
592
|
+
], [
|
|
593
|
+
"Dedup pool size",
|
|
594
|
+
num(perf.dedupPoolSize),
|
|
595
|
+
"flat",
|
|
596
|
+
"Average memory pool size after deduplication (before judged-cache narrowing). WS-5 perf telemetry.",
|
|
597
|
+
], [
|
|
598
|
+
"Over-budget consolidation runs",
|
|
599
|
+
String(perf.overBudgetRuns),
|
|
600
|
+
perf.overBudgetRuns > 0 ? "down" : "flat",
|
|
601
|
+
"Runs where consolidation alone exceeded the total run budget (estimatedBudgetFractionUsed > 1.0).",
|
|
602
|
+
]);
|
|
603
|
+
}
|
|
604
|
+
// WS-5: degradation metrics rows.
|
|
605
|
+
if (degradation) {
|
|
606
|
+
summaryRows.push([
|
|
607
|
+
"Corpus diversity (Gini)",
|
|
608
|
+
num(degradation.corpusCentroidDistance),
|
|
609
|
+
degradation.entrenchmentFlagged ? "down" : "flat",
|
|
610
|
+
"Gini coefficient of retrieval_salience for top-100 ranked assets. High Gini (>0.35) = entrenchment risk.",
|
|
611
|
+
], [
|
|
612
|
+
"Merge fidelity contradiction rate",
|
|
613
|
+
pct(degradation.mergeFidelityContradictionRate, 1),
|
|
614
|
+
"flat",
|
|
615
|
+
"Fraction of consolidated proposals that involved a contradiction, from consolidation result envelopes.",
|
|
616
|
+
], [
|
|
617
|
+
"High-generation fraction",
|
|
618
|
+
pct(degradation.highGenerationFraction, 1),
|
|
619
|
+
"flat",
|
|
620
|
+
"Fraction of assets with consecutive_no_ops >= 2 (proxy for high-generation assets in the salience table).",
|
|
621
|
+
]);
|
|
622
|
+
}
|
|
350
623
|
const summaryRowsHtml = summaryRows
|
|
351
|
-
.map(([label, value, t]) =>
|
|
352
|
-
`<
|
|
624
|
+
.map(([label, value, t, tip]) => {
|
|
625
|
+
const labelHtml = tip ? `<abbr title="${esc(tip)}">${esc(label)}</abbr>` : esc(label);
|
|
626
|
+
return (` <tr><td>${labelHtml}</td><td>${esc(value)}</td>` +
|
|
627
|
+
`<td class="trend ${trendClass(t)}">${trendLabel(t)}</td></tr>`);
|
|
628
|
+
})
|
|
353
629
|
.join("\n");
|
|
354
|
-
// ──
|
|
355
|
-
|
|
630
|
+
// ── Action Items (merged + de-duplicated advisories + what-to-watch) ────────
|
|
631
|
+
// Advisories and the old "what to watch" cards were built from the same data
|
|
632
|
+
// (result.advisories, sem-blocked, proposals, tail-latency, failed-runs). We
|
|
633
|
+
// collapse them into ONE prioritized, de-duplicated list (P1/P2/P3 + a
|
|
634
|
+
// remediation command per item), keyed so each concern appears exactly once.
|
|
635
|
+
const items = [];
|
|
636
|
+
const seen = new Set();
|
|
637
|
+
const pushItem = (item) => {
|
|
638
|
+
if (seen.has(item.key))
|
|
639
|
+
return;
|
|
640
|
+
seen.add(item.key);
|
|
641
|
+
items.push(item);
|
|
642
|
+
};
|
|
643
|
+
// Hard advisories from the health check engine (own remediation in message).
|
|
356
644
|
for (const a of result.advisories) {
|
|
357
645
|
if (a.status !== "warn" && a.status !== "fail")
|
|
358
646
|
continue;
|
|
359
|
-
|
|
647
|
+
pushItem({
|
|
648
|
+
key: `advisory:${a.name}`,
|
|
649
|
+
prio: a.status === "fail" ? "P1" : "P2",
|
|
650
|
+
cls: a.status === "fail" ? "fail" : "warn",
|
|
651
|
+
title: esc(humanize(a.name)),
|
|
652
|
+
descHtml: esc(a.message),
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
// Failed runs in window.
|
|
656
|
+
if (failedRuns > 0) {
|
|
657
|
+
pushItem({
|
|
658
|
+
key: "failed-runs",
|
|
659
|
+
prio: "P1",
|
|
660
|
+
cls: "fail",
|
|
661
|
+
title: `${failedRuns} failed run${failedRuns === 1 ? "" : "s"} in window`,
|
|
662
|
+
descHtml: `Task fail rate ${esc(taskFailRate)}. Inspect failed runs (ok=false) for early-exit or harness errors.`,
|
|
663
|
+
remedy: `akm health --since=${opts.window} --group-by run`,
|
|
664
|
+
});
|
|
360
665
|
}
|
|
666
|
+
// Semantic search blocked.
|
|
361
667
|
if (sem.blocked) {
|
|
362
|
-
|
|
668
|
+
pushItem({
|
|
669
|
+
key: "semantic-search-blocked",
|
|
670
|
+
prio: "P2",
|
|
671
|
+
cls: "warn",
|
|
672
|
+
title: "Semantic search blocked",
|
|
673
|
+
descHtml: `Embedding provider unreachable. ${esc(sem.detail)}. Curate falls back to keyword search — relevance scoring degraded.`,
|
|
674
|
+
remedy: "akm config show",
|
|
675
|
+
});
|
|
363
676
|
}
|
|
677
|
+
// Pending proposals to drain.
|
|
364
678
|
if (proposals.length > 0) {
|
|
365
|
-
|
|
679
|
+
const bySource = new Map();
|
|
680
|
+
for (const p of proposals)
|
|
681
|
+
bySource.set(p.source, (bySource.get(p.source) ?? 0) + 1);
|
|
682
|
+
const srcSummary = [...bySource.entries()]
|
|
683
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
684
|
+
.map(([source, count]) => `${count} via ${esc(source)}`)
|
|
685
|
+
.join(", ");
|
|
686
|
+
pushItem({
|
|
687
|
+
key: "drain-proposals",
|
|
688
|
+
prio: "P2",
|
|
689
|
+
cls: "warn",
|
|
690
|
+
title: `Drain ${proposals.length} pending proposal${proposals.length === 1 ? "" : "s"}`,
|
|
691
|
+
descHtml: `Proposals generated this batch (${srcSummary}). Review before the queue grows further.`,
|
|
692
|
+
remedy: "akm proposal list",
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
// High tail latency.
|
|
696
|
+
if (wallTime.p95Ms && wallTime.medianMs && wallTime.p95Ms / wallTime.medianMs > 2.5) {
|
|
697
|
+
pushItem({
|
|
698
|
+
key: "tail-latency",
|
|
699
|
+
prio: "P3",
|
|
700
|
+
cls: "warn",
|
|
701
|
+
title: `High tail latency: p95=${fmtMs(wallTime.p95Ms)}, median=${fmtMs(wallTime.medianMs)}`,
|
|
702
|
+
descHtml: "P95 is well above median. Consolidation/LLM phase dominates wall time on slow runs. " +
|
|
703
|
+
"Check for slow chunks or LLM rate limiting.",
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
// Stale freshness.
|
|
707
|
+
if (isStale && totalRuns > 0) {
|
|
708
|
+
pushItem({
|
|
709
|
+
key: "stale",
|
|
710
|
+
prio: "P3",
|
|
711
|
+
cls: "warn",
|
|
712
|
+
title: "No recent improve runs",
|
|
713
|
+
descHtml: `Newest run is ${esc(latestRunHuman)} — older than the 6h freshness threshold. Check the improve scheduler/cron.`,
|
|
714
|
+
});
|
|
366
715
|
}
|
|
716
|
+
// WS-5: corpus entrenchment flag.
|
|
717
|
+
if (degradation?.entrenchmentFlagged) {
|
|
718
|
+
pushItem({
|
|
719
|
+
key: "corpus-entrenchment",
|
|
720
|
+
prio: "P2",
|
|
721
|
+
cls: "warn",
|
|
722
|
+
title: "Corpus entrenchment risk: retrieval_salience Gini > 0.35",
|
|
723
|
+
descHtml: "A small set of assets dominates retrieval — retrieval diversity is low. " +
|
|
724
|
+
"Review top-ranked assets for stale or over-represented content. " +
|
|
725
|
+
`Corpus diversity proxy: ${esc(String(degradation.corpusCentroidDistance))}.`,
|
|
726
|
+
remedy: "akm health --format json | jq '.improve.degradation'",
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
// WS-5: over-budget consolidation advisory.
|
|
730
|
+
if (perf.overBudgetRuns > 0) {
|
|
731
|
+
pushItem({
|
|
732
|
+
key: "over-budget-consolidation",
|
|
733
|
+
prio: "P2",
|
|
734
|
+
cls: "warn",
|
|
735
|
+
title: `${perf.overBudgetRuns} consolidation run${perf.overBudgetRuns === 1 ? "" : "s"} exceeded budget`,
|
|
736
|
+
descHtml: "Consolidation phase wall time exceeded the total run budget on these runs. " +
|
|
737
|
+
"Consider increasing the timeout or reducing the consolidation pool via profile config.",
|
|
738
|
+
remedy: "akm config show",
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
// Session-log notes (informational, lowest priority).
|
|
367
742
|
if (result.sessionLogAdvisories.length > 0) {
|
|
368
743
|
const patterns = result.sessionLogAdvisories
|
|
369
744
|
.slice(0, 6)
|
|
370
745
|
.map((p) => `<li>${esc(p.topic)}</li>`)
|
|
371
746
|
.join("");
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
747
|
+
pushItem({
|
|
748
|
+
key: "session-log-notes",
|
|
749
|
+
prio: "P3",
|
|
750
|
+
cls: "warn",
|
|
751
|
+
title: `${result.sessionLogAdvisories.length} session-log note(s) (informational)`,
|
|
752
|
+
descHtml: `<ul style="margin:4px 0 0 16px;padding:0;">${patterns}</ul>`,
|
|
753
|
+
});
|
|
376
754
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
755
|
+
items.sort((a, b) => PRIO_RANK[a.prio] - PRIO_RANK[b.prio]);
|
|
756
|
+
const actionItemsHtml = items.length > 0
|
|
757
|
+
? items.map(actionItemCard).join("\n")
|
|
758
|
+
: passCard("No action items", "All checks passed and nothing needs attention for this window.");
|
|
380
759
|
// ── Proposal rows ──────────────────────────────────────────────────────────
|
|
381
760
|
const proposalRowsHtml = proposals.length > 0
|
|
382
761
|
? proposals
|
|
383
762
|
.map((p, i) => {
|
|
384
763
|
const tagCls = p.source === "extract" ? "tag-extract" : "tag-consolidate";
|
|
385
|
-
const ts = p.createdAt.slice(0, 16).replace("T", " ");
|
|
386
764
|
return (`<tr><td>${i + 1}</td><td><code>${esc(p.ref)}</code></td>` +
|
|
387
765
|
`<td><span class="tag ${tagCls}">${esc(p.source)}</span></td>` +
|
|
388
|
-
`<td>${
|
|
766
|
+
`<td>${isoTimeTag(p.createdAt)}</td></tr>`);
|
|
389
767
|
})
|
|
390
768
|
.join("\n")
|
|
391
769
|
: '<tr><td colspan="4" style="text-align:center;color:var(--muted);">No pending proposals</td></tr>';
|
|
392
|
-
// ── What to watch ──────────────────────────────────────────────────────────
|
|
393
|
-
const watchParts = [];
|
|
394
|
-
for (const a of result.advisories) {
|
|
395
|
-
if (a.status !== "warn" && a.status !== "fail")
|
|
396
|
-
continue;
|
|
397
|
-
const prio = a.status === "fail" ? "P1" : "P2";
|
|
398
|
-
watchParts.push(advisoryCard(a.status === "fail" ? "fail" : "warn", a.status === "fail" ? "🔴" : "🟡", `${esc(a.name)} (${prio})`, esc(a.message)));
|
|
399
|
-
}
|
|
400
|
-
if (sem.blocked) {
|
|
401
|
-
watchParts.push(advisoryCard("warn", "🟡", "Embedding server unreachable (P2)", "Curate quality and semantic ranking are degraded. Check the embedding endpoint configured in config.json."));
|
|
402
|
-
}
|
|
403
|
-
if (proposals.length > 0) {
|
|
404
|
-
const bySource = new Map();
|
|
405
|
-
for (const p of proposals)
|
|
406
|
-
bySource.set(p.source, (bySource.get(p.source) ?? 0) + 1);
|
|
407
|
-
const srcSummary = [...bySource.entries()]
|
|
408
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
409
|
-
.map(([source, count]) => `${count} via ${esc(source)}`)
|
|
410
|
-
.join(", ");
|
|
411
|
-
watchParts.push(advisoryCard("warn", "🟡", `Drain ${proposals.length} pending proposals (P2)`, `Proposals generated this batch (${srcSummary}). Run <code>akm proposal list</code> before the queue grows further.`));
|
|
412
|
-
}
|
|
413
|
-
if (wallTime.p95Ms && wallTime.medianMs && wallTime.p95Ms / wallTime.medianMs > 2.5) {
|
|
414
|
-
watchParts.push(advisoryCard("warn", "🟡", `High tail latency (P3): p95=${fmtMs(wallTime.p95Ms)}, median=${fmtMs(wallTime.medianMs)}`, "P95 is well above median. Consolidation/LLM phase dominates wall time on slow runs. " +
|
|
415
|
-
"Check for slow chunks or LLM rate limiting."));
|
|
416
|
-
}
|
|
417
|
-
if (failedRuns > 0) {
|
|
418
|
-
watchParts.push(advisoryCard("warn", "🟡", `${failedRuns} failed run(s) in window (P2)`, `Task fail rate ${taskFailRate}. Inspect failed runs (ok=false) for early-exit or harness errors.`));
|
|
419
|
-
}
|
|
420
|
-
const watchItemsHtml = watchParts.length > 0
|
|
421
|
-
? watchParts.join("\n")
|
|
422
|
-
: passCard("Nothing critical to watch", "All indicators are within normal range.");
|
|
423
|
-
// ── Commands used ──────────────────────────────────────────────────────────
|
|
424
|
-
const commandsHtml = [
|
|
425
|
-
` <div><span>akm health --since=${esc(opts.window)} --group-by run --format json</span></div>`,
|
|
426
|
-
` <div><span>akm health --since=${esc(opts.window)} --window-compare=${esc(opts.compare)} --format json</span></div>`,
|
|
427
|
-
" <div><span>akm proposal list</span></div>",
|
|
428
|
-
].join("\n");
|
|
429
770
|
return {
|
|
430
771
|
"%%ECHARTS_TAG%%": buildEchartsTag(opts),
|
|
431
772
|
"%%REPORT_TITLE%%": esc(reportTitle),
|
|
432
773
|
"%%WINDOW%%": esc(opts.window),
|
|
433
|
-
"%%SINCE_HUMAN%%":
|
|
774
|
+
"%%SINCE_HUMAN%%": sinceHuman,
|
|
434
775
|
"%%RUN_COUNT%%": num(totalRuns),
|
|
435
776
|
"%%STATUS_BADGE_HTML%%": `${statusBadge}\n ${failBadge}`,
|
|
436
777
|
"%%EXEC_SUMMARY_HTML%%": execSummary,
|
|
437
778
|
"%%KPI_CARDS_HTML%%": kpiCards,
|
|
438
779
|
"%%RUNS_JS_CONST%%": runsJsConst,
|
|
439
780
|
"%%DISTILL_REASONS_JSON%%": JSON.stringify(distillReasons),
|
|
781
|
+
"%%SLICE_OPTIONS_HTML%%": buildSliceOptions(opts.window),
|
|
782
|
+
"%%LLM_BY_STAGE_JSON%%": JSON.stringify(llm.byStage ?? {}),
|
|
440
783
|
"%%SUMMARY_ROWS_HTML%%": summaryRowsHtml,
|
|
441
|
-
"%%
|
|
784
|
+
"%%ACTION_ITEMS_HTML%%": actionItemsHtml,
|
|
442
785
|
"%%PROPOSAL_ROWS_HTML%%": proposalRowsHtml,
|
|
443
786
|
"%%PROPOSAL_COUNT%%": String(proposals.length),
|
|
444
|
-
"%%WATCH_ITEMS_HTML%%": watchItemsHtml,
|
|
445
|
-
"%%COMMANDS_HTML%%": commandsHtml,
|
|
446
787
|
"%%GENERATED_AT%%": esc(generatedAt),
|
|
788
|
+
"%%AKM_VERSION%%": esc(pkgVersion),
|
|
447
789
|
};
|
|
448
790
|
}
|