akm-cli 0.9.0-beta.9 → 0.9.0-rc.0
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 +592 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/help/help-improve.md +9 -6
- package/dist/assets/hints/cli-hints-full.md +6 -5
- 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/proactive-maintenance.json +25 -0
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/recombine-only.json +21 -0
- package/dist/assets/profiles/reflect-distill.json +30 -0
- 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 +11 -3
- 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 +45 -16
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/clack.js +56 -0
- package/dist/cli/config-migrate.js +7 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +25 -21
- package/dist/commands/agent/agent-dispatch.js +3 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +26 -7
- package/dist/commands/config-cli.js +26 -13
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +220 -227
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/env/secret-cli.js +140 -138
- package/dist/commands/feedback-cli.js +156 -155
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +3 -3
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/checks.js +103 -16
- package/dist/commands/health/html-report.js +447 -81
- package/dist/commands/health/improve-metrics.js +771 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/surfaces.js +216 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +26 -0
- package/dist/commands/health/windows.js +195 -0
- package/dist/commands/health.js +91 -1091
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/collapse-detector.js +421 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -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 +1295 -1277
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +229 -0
- package/dist/commands/improve/distill/quality-gate.js +236 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +228 -605
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +179 -59
- package/dist/commands/improve/extract-prompt.js +54 -3
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +409 -43
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +157 -10
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +28 -8
- package/dist/commands/improve/improve-result-file.js +15 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2764
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1100 -0
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/outcome-loop.js +270 -0
- package/dist/commands/improve/preparation.js +2002 -0
- package/dist/commands/improve/proactive-maintenance.js +37 -35
- package/dist/commands/improve/procedural.js +398 -0
- package/dist/commands/improve/recombine.js +818 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +206 -45
- package/dist/commands/improve/salience.js +455 -0
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +51 -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/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +13 -4
- package/dist/commands/proposal/drain.js +45 -51
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +24 -34
- package/dist/commands/proposal/proposal.js +2 -1
- package/dist/commands/proposal/propose.js +8 -3
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +93 -895
- package/dist/commands/read/curate.js +410 -111
- package/dist/commands/read/knowledge.js +10 -3
- package/dist/commands/read/remember-cli.js +133 -138
- package/dist/commands/read/search-cli.js +15 -8
- package/dist/commands/read/search.js +22 -11
- package/dist/commands/read/show.js +106 -14
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +66 -18
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +44 -46
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +19 -39
- package/dist/commands/sources/stash-skeleton.js +57 -8
- package/dist/commands/tasks/default-tasks.js +15 -2
- package/dist/commands/tasks/tasks-cli.js +20 -29
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +23 -38
- package/dist/commands/workflow-cli.js +15 -1
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +21 -4
- package/dist/core/asset/frontmatter.js +188 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +4 -23
- package/dist/core/concurrent.js +10 -1
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +18 -40
- package/dist/core/config/config-schema.js +382 -62
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +67 -22
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/errors.js +1 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/events.js +4 -7
- package/dist/core/improve-types.js +47 -8
- package/dist/core/logs-db.js +14 -75
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +18 -18
- 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 +770 -0
- package/dist/core/state-db.js +132 -1126
- package/dist/core/structured.js +69 -0
- package/dist/core/time.js +53 -0
- package/dist/core/warn.js +21 -0
- package/dist/core/write-source.js +37 -0
- package/dist/indexer/db/db.js +259 -769
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +36 -92
- package/dist/indexer/feedback/utility-policy.js +75 -0
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +207 -4
- package/dist/indexer/index-writer-lock.js +18 -11
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +182 -204
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +89 -13
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/search/ranking-contributors.js +95 -9
- package/dist/indexer/search/ranking.js +79 -3
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/search/search-source.js +32 -21
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +39 -13
- package/dist/integrations/agent/config.js +20 -59
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +3 -19
- package/dist/integrations/agent/model-aliases.js +7 -2
- package/dist/integrations/agent/profiles.js +7 -1
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +69 -67
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
- package/dist/integrations/harnesses/claude/index.js +2 -0
- package/dist/integrations/harnesses/claude/session-log.js +10 -0
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +2 -0
- 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 +98 -17
- package/dist/integrations/harnesses/types.js +1 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/call-ai.js +2 -2
- package/dist/llm/client.js +34 -11
- package/dist/llm/embedder.js +67 -4
- package/dist/llm/embedders/cache.js +3 -1
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +73 -3
- package/dist/llm/feature-gate.js +16 -15
- 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 +85 -14
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/text/helpers.js +29 -1
- package/dist/output/text/workflow.js +1 -0
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +27 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2661 -2369
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +52 -1614
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- 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/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/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +146 -0
- package/dist/storage/repositories/index-db.js +14 -8
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/registry-cache.js +93 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/index.js +9 -0
- 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 +15 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/cli.js +1 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +43 -118
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +5 -4
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +10 -10
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
- package/dist/indexer/passes/staleness-detect.js +0 -488
package/dist/commands/health.js
CHANGED
|
@@ -2,1011 +2,44 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { resolveStashDir } from "../core/common.js";
|
|
7
|
+
import { loadConfig } from "../core/config/config.js";
|
|
5
8
|
import { ConfigError, UsageError } from "../core/errors.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { getStateDbPathInDataDir } from "../core/paths.js";
|
|
9
|
-
import { listExistingTableNames, openStateDatabase
|
|
10
|
-
import { parseSinceToIso } from "../core/time.js";
|
|
9
|
+
import { readEvents } from "../core/events.js";
|
|
10
|
+
import { openLogsDatabase } from "../core/logs-db.js";
|
|
11
|
+
import { getCacheDir, getConfigDir, getConfigPath, getDataDir, getStateDbPathInDataDir } from "../core/paths.js";
|
|
12
|
+
import { listExistingTableNames, openStateDatabase } from "../core/state-db.js";
|
|
13
|
+
import { DURATION_UNITS, parseDuration, parseSinceToIso } from "../core/time.js";
|
|
11
14
|
import { readSemanticStatus } from "../indexer/search/semantic-status.js";
|
|
12
15
|
import { getExecutionLogCandidates } from "../integrations/session-logs/index.js";
|
|
13
|
-
import {
|
|
16
|
+
import { queryTaskHistory } from "../storage/repositories/task-history-repository.js";
|
|
17
|
+
import { collectImproveAdvisories } from "./health/advisories.js";
|
|
14
18
|
import { HEALTH_CHECKS } from "./health/checks.js";
|
|
19
|
+
import { buildImproveSkipSummary, computeWallTimeStats, parseTaskMetadata, roundRate, summarizeImproveCompleted, summarizeImproveRuns, } from "./health/improve-metrics.js";
|
|
20
|
+
import { readLlmUsageAggregate } from "./health/llm-usage.js";
|
|
21
|
+
import { computeDegradationMetrics, computeDenominatorFixedCoverage, computeEnrichmentMintingRollup, probeStateDbRoundTrip, readCalibration, } from "./health/metrics.js";
|
|
22
|
+
import { collectStashExposureAdvisory } from "./health/stash-exposure.js";
|
|
23
|
+
import { collectSurfacesAdvisories } from "./health/surfaces.js";
|
|
24
|
+
import { buildPerRunSummaries } from "./health/task-runs.js";
|
|
25
|
+
import { ACTIVE_RUN_WARN_MS, IMPROVE_COMPLETED_EVENT, } from "./health/types.js";
|
|
26
|
+
import { buildWindowMetrics, computeDeltas, partitionLogBackedRows, resolveWindowCompare } from "./health/windows.js";
|
|
15
27
|
const DEFAULT_SINCE_MS = 24 * 60 * 60 * 1000;
|
|
16
|
-
const IMPROVE_COMPLETED_EVENT = "improve_completed";
|
|
17
|
-
const HEALTH_PROBE_EVENT = "health_probe";
|
|
18
|
-
const ACTIVE_RUN_WARN_MS = 15 * 60 * 1000;
|
|
19
28
|
export function parseHealthSince(since) {
|
|
20
29
|
if (since === undefined || since.trim() === "") {
|
|
21
30
|
return new Date(Date.now() - DEFAULT_SINCE_MS).toISOString();
|
|
22
31
|
}
|
|
23
32
|
const trimmed = since.trim();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const multiplier = unit === "h" ? 60 * 60 * 1000 : unit === "m" ? 30 * 24 * 60 * 60 * 1000 : 24 * 60 * 60 * 1000;
|
|
32
|
-
return new Date(Date.now() - amount * multiplier).toISOString();
|
|
33
|
+
// Unit grammar is the CLI-wide canonical map: `m` = minutes, `M` = months.
|
|
34
|
+
// (Historically `--since 5m` meant 5 months here; it now means 5 minutes,
|
|
35
|
+
// with `5M` for months — unified with consolidate / `--window-compare`.)
|
|
36
|
+
// Not lower-cased: case distinguishes `m` (minutes) from `M` (months).
|
|
37
|
+
const durationMs = parseDuration(trimmed, DURATION_UNITS);
|
|
38
|
+
if (durationMs !== null) {
|
|
39
|
+
return new Date(Date.now() - durationMs).toISOString();
|
|
33
40
|
}
|
|
34
41
|
return parseSinceToIso(trimmed);
|
|
35
42
|
}
|
|
36
|
-
function roundRate(value) {
|
|
37
|
-
return Number(value.toFixed(4));
|
|
38
|
-
}
|
|
39
|
-
function parseTaskMetadata(row) {
|
|
40
|
-
try {
|
|
41
|
-
return JSON.parse(row.metadata_json);
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return {};
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
function createUnknownImproveMetrics() {
|
|
48
|
-
return {
|
|
49
|
-
invoked: 0,
|
|
50
|
-
completed: 0,
|
|
51
|
-
skipped: 0,
|
|
52
|
-
skipReasons: {},
|
|
53
|
-
plannedRefs: 0,
|
|
54
|
-
profileFilteredRefs: 0,
|
|
55
|
-
actions: {
|
|
56
|
-
reflect: { ok: 0, failed: 0, cooldown: 0, skipped: 0, guardRejected: 0, skippedByReason: {} },
|
|
57
|
-
distill: {
|
|
58
|
-
queued: 0,
|
|
59
|
-
llmFailed: 0,
|
|
60
|
-
qualityRejected: 0,
|
|
61
|
-
judgeRejected: 0,
|
|
62
|
-
validatorRejected: 0,
|
|
63
|
-
configDisabled: 0,
|
|
64
|
-
skipped: 0,
|
|
65
|
-
skippedByReason: {},
|
|
66
|
-
deferred: 0,
|
|
67
|
-
deferredByReason: {},
|
|
68
|
-
},
|
|
69
|
-
memoryPrune: 0,
|
|
70
|
-
memoryInference: 0,
|
|
71
|
-
graphExtraction: 0,
|
|
72
|
-
error: 0,
|
|
73
|
-
},
|
|
74
|
-
autoAccept: { promoted: 0, validationFailed: 0 },
|
|
75
|
-
reflectsWithErrorContext: 0,
|
|
76
|
-
coverageGapCount: 0,
|
|
77
|
-
evalCasesWritten: 0,
|
|
78
|
-
deadUrlCount: 0,
|
|
79
|
-
memorySummary: { eligible: 0, derived: 0 },
|
|
80
|
-
memoryCleanup: {
|
|
81
|
-
pruneCandidates: 0,
|
|
82
|
-
contradictionCandidates: 0,
|
|
83
|
-
beliefStateTransitions: 0,
|
|
84
|
-
consolidationCandidates: 0,
|
|
85
|
-
archived: 0,
|
|
86
|
-
warnings: 0,
|
|
87
|
-
},
|
|
88
|
-
consolidation: {
|
|
89
|
-
ran: false,
|
|
90
|
-
processed: 0,
|
|
91
|
-
promoted: 0,
|
|
92
|
-
merged: 0,
|
|
93
|
-
deleted: 0,
|
|
94
|
-
contradicted: 0,
|
|
95
|
-
judgedNoAction: 0,
|
|
96
|
-
mergedSecondaries: 0,
|
|
97
|
-
failedChunkMemories: 0,
|
|
98
|
-
skipReasons: {},
|
|
99
|
-
failedChunks: 0,
|
|
100
|
-
totalChunks: 0,
|
|
101
|
-
durationMs: 0,
|
|
102
|
-
},
|
|
103
|
-
memoryInference: {
|
|
104
|
-
ran: false,
|
|
105
|
-
considered: 0,
|
|
106
|
-
cacheHits: 0,
|
|
107
|
-
retryAttempts: 0,
|
|
108
|
-
freshAttempts: 0,
|
|
109
|
-
splitParents: 0,
|
|
110
|
-
written: 0,
|
|
111
|
-
skippedNoFacts: 0,
|
|
112
|
-
skippedChildExists: 0,
|
|
113
|
-
skippedAborted: 0,
|
|
114
|
-
unaccounted: 0,
|
|
115
|
-
htmlErrorCount: 0,
|
|
116
|
-
yieldEligibleRuns: 0,
|
|
117
|
-
yieldEligibleConsidered: 0,
|
|
118
|
-
yieldEligibleWritten: 0,
|
|
119
|
-
yieldRate: 0,
|
|
120
|
-
durationMs: 0,
|
|
121
|
-
writes: 0,
|
|
122
|
-
},
|
|
123
|
-
graphExtraction: {
|
|
124
|
-
ran: false,
|
|
125
|
-
extractedFiles: 0,
|
|
126
|
-
entities: 0,
|
|
127
|
-
relations: 0,
|
|
128
|
-
cacheHits: 0,
|
|
129
|
-
cacheMisses: 0,
|
|
130
|
-
cacheHitRate: 0,
|
|
131
|
-
truncations: 0,
|
|
132
|
-
failures: 0,
|
|
133
|
-
htmlErrors: 0,
|
|
134
|
-
retryAttempts: 0,
|
|
135
|
-
durationMs: 0,
|
|
136
|
-
},
|
|
137
|
-
sessionExtraction: {
|
|
138
|
-
ran: false,
|
|
139
|
-
sessionsScanned: 0,
|
|
140
|
-
sessionsExtracted: 0,
|
|
141
|
-
sessionsSkipped: 0,
|
|
142
|
-
proposalsCreated: 0,
|
|
143
|
-
warnings: 0,
|
|
144
|
-
durationMs: 0,
|
|
145
|
-
},
|
|
146
|
-
wallTime: {
|
|
147
|
-
count: 0,
|
|
148
|
-
medianMs: 0,
|
|
149
|
-
p95Ms: 0,
|
|
150
|
-
minMs: 0,
|
|
151
|
-
maxMs: 0,
|
|
152
|
-
byPhase: {
|
|
153
|
-
consolidation: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
|
|
154
|
-
memoryInference: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
|
|
155
|
-
graphExtraction: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
function toFiniteNumber(value) {
|
|
161
|
-
if (typeof value === "number" && Number.isFinite(value))
|
|
162
|
-
return value;
|
|
163
|
-
if (typeof value === "string" && value.trim()) {
|
|
164
|
-
const parsed = Number(value);
|
|
165
|
-
if (Number.isFinite(parsed))
|
|
166
|
-
return parsed;
|
|
167
|
-
}
|
|
168
|
-
return 0;
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Event-derived metrics. Only `completed` and skipReasons/invoked are sourced
|
|
172
|
-
* from events in v2 — the richer fields come from {@link summarizeImproveRuns}.
|
|
173
|
-
* The function still receives `improve_completed` events so that the completed
|
|
174
|
-
* count reflects the canonical event stream (it lines up 1:1 with improve_runs
|
|
175
|
-
* rows in practice, but the events table remains the system-of-record for the
|
|
176
|
-
* existence of a run).
|
|
177
|
-
*/
|
|
178
|
-
function summarizeImproveCompleted(events) {
|
|
179
|
-
const metrics = createUnknownImproveMetrics();
|
|
180
|
-
metrics.completed = events.length;
|
|
181
|
-
return metrics;
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Project a single `improve_runs.result_json` envelope into an accumulator-shaped
|
|
185
|
-
* ImproveHealthMetrics. The aggregator merges these per-row metrics into one
|
|
186
|
-
* window-level metric.
|
|
187
|
-
*/
|
|
188
|
-
function projectRunMetrics(result) {
|
|
189
|
-
const metrics = createUnknownImproveMetrics();
|
|
190
|
-
// plannedRefs (array of {ref, reason})
|
|
191
|
-
const plannedRefs = result.plannedRefs;
|
|
192
|
-
if (Array.isArray(plannedRefs))
|
|
193
|
-
metrics.plannedRefs += plannedRefs.length;
|
|
194
|
-
// profileFilteredRefs (array of {ref, reason}) — 2026-05-27: pre-filter
|
|
195
|
-
// bucket from `collectEligibleRefs` so the metric reflects work the
|
|
196
|
-
// planner dropped before signal-delta / per-pass dispatch.
|
|
197
|
-
const profileFilteredRefs = result.profileFilteredRefs;
|
|
198
|
-
if (Array.isArray(profileFilteredRefs))
|
|
199
|
-
metrics.profileFilteredRefs += profileFilteredRefs.length;
|
|
200
|
-
// actions: split reflect / distill by outcome, count others.
|
|
201
|
-
const actions = result.actions;
|
|
202
|
-
if (Array.isArray(actions)) {
|
|
203
|
-
for (const action of actions) {
|
|
204
|
-
const mode = typeof action.mode === "string" ? action.mode : "";
|
|
205
|
-
switch (mode) {
|
|
206
|
-
case "reflect":
|
|
207
|
-
metrics.actions.reflect.ok += 1;
|
|
208
|
-
break;
|
|
209
|
-
case "reflect-failed":
|
|
210
|
-
metrics.actions.reflect.failed += 1;
|
|
211
|
-
break;
|
|
212
|
-
case "reflect-cooldown":
|
|
213
|
-
metrics.actions.reflect.cooldown += 1;
|
|
214
|
-
break;
|
|
215
|
-
case "reflect-skipped": {
|
|
216
|
-
metrics.actions.reflect.skipped += 1;
|
|
217
|
-
const r = action.result;
|
|
218
|
-
const reason = typeof r?.reason === "string" && r.reason.trim() ? r.reason : "unknown";
|
|
219
|
-
metrics.actions.reflect.skippedByReason[reason] = (metrics.actions.reflect.skippedByReason[reason] ?? 0) + 1;
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
case "reflect-guard-rejected":
|
|
223
|
-
metrics.actions.reflect.guardRejected += 1;
|
|
224
|
-
break;
|
|
225
|
-
case "distill": {
|
|
226
|
-
const r = action.result;
|
|
227
|
-
const outcome = typeof r?.outcome === "string" ? r.outcome : "";
|
|
228
|
-
switch (outcome) {
|
|
229
|
-
case "queued":
|
|
230
|
-
metrics.actions.distill.queued += 1;
|
|
231
|
-
break;
|
|
232
|
-
case "llm_failed":
|
|
233
|
-
metrics.actions.distill.llmFailed += 1;
|
|
234
|
-
break;
|
|
235
|
-
case "quality_rejected":
|
|
236
|
-
case "review_needed":
|
|
237
|
-
metrics.actions.distill.qualityRejected += 1;
|
|
238
|
-
metrics.actions.distill.judgeRejected += 1;
|
|
239
|
-
break;
|
|
240
|
-
case "validation_failed":
|
|
241
|
-
metrics.actions.distill.qualityRejected += 1;
|
|
242
|
-
metrics.actions.distill.validatorRejected += 1;
|
|
243
|
-
break;
|
|
244
|
-
case "config_disabled":
|
|
245
|
-
metrics.actions.distill.configDisabled += 1;
|
|
246
|
-
break;
|
|
247
|
-
case "skipped": {
|
|
248
|
-
// Previously dropped on the floor. The four sub-paths that emit
|
|
249
|
-
// `outcome: "skipped"` (see distill.ts:893, 1024, 1120, 1576):
|
|
250
|
-
// - recursive_lesson_input (type guard refused a lesson input)
|
|
251
|
-
// - conflict_noop (LLM resolved destination conflict as NOOP)
|
|
252
|
-
// - proposal-skipped cooldown / dedup at persistence
|
|
253
|
-
// 465 events/7d in the user's live stack. The result message
|
|
254
|
-
// typically encodes the reason; we also accept an explicit
|
|
255
|
-
// `skipReason` field when downstream code sets it.
|
|
256
|
-
metrics.actions.distill.deferred += 1;
|
|
257
|
-
const explicitReason = typeof r?.skipReason === "string" ? r.skipReason : undefined;
|
|
258
|
-
const msg = typeof r?.message === "string" ? r.message : "";
|
|
259
|
-
let reason = explicitReason ?? "unknown";
|
|
260
|
-
if (!explicitReason) {
|
|
261
|
-
if (/lesson inputs/i.test(msg))
|
|
262
|
-
reason = "recursive_lesson_input";
|
|
263
|
-
else if (/NOOP/.test(msg))
|
|
264
|
-
reason = "conflict_noop";
|
|
265
|
-
else if (/cooldown/i.test(msg))
|
|
266
|
-
reason = "proposal_cooldown";
|
|
267
|
-
else if (/content[_ ]?hash/i.test(msg))
|
|
268
|
-
reason = "content_hash_match";
|
|
269
|
-
}
|
|
270
|
-
metrics.actions.distill.deferredByReason[reason] =
|
|
271
|
-
(metrics.actions.distill.deferredByReason[reason] ?? 0) + 1;
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
default:
|
|
275
|
-
break;
|
|
276
|
-
}
|
|
277
|
-
break;
|
|
278
|
-
}
|
|
279
|
-
case "distill-skipped": {
|
|
280
|
-
metrics.actions.distill.skipped += 1;
|
|
281
|
-
const r = action.result;
|
|
282
|
-
const reason = typeof r?.reason === "string" && r.reason.trim() ? r.reason : "unknown";
|
|
283
|
-
metrics.actions.distill.skippedByReason[reason] = (metrics.actions.distill.skippedByReason[reason] ?? 0) + 1;
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
286
|
-
case "memory-prune":
|
|
287
|
-
metrics.actions.memoryPrune += 1;
|
|
288
|
-
break;
|
|
289
|
-
case "memory-inference":
|
|
290
|
-
metrics.actions.memoryInference += 1;
|
|
291
|
-
break;
|
|
292
|
-
case "graph-extraction":
|
|
293
|
-
metrics.actions.graphExtraction += 1;
|
|
294
|
-
break;
|
|
295
|
-
case "error":
|
|
296
|
-
metrics.actions.error += 1;
|
|
297
|
-
break;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
metrics.autoAccept.promoted += toFiniteNumber(result.gateAutoAcceptedCount);
|
|
302
|
-
metrics.autoAccept.validationFailed += toFiniteNumber(result.gateAutoAcceptFailedCount);
|
|
303
|
-
metrics.reflectsWithErrorContext += toFiniteNumber(result.reflectsWithErrorContext);
|
|
304
|
-
if (Array.isArray(result.coverageGaps))
|
|
305
|
-
metrics.coverageGapCount += result.coverageGaps.length;
|
|
306
|
-
metrics.evalCasesWritten += toFiniteNumber(result.evalCasesWritten);
|
|
307
|
-
if (Array.isArray(result.deadUrls))
|
|
308
|
-
metrics.deadUrlCount += result.deadUrls.length;
|
|
309
|
-
const memorySummary = result.memorySummary;
|
|
310
|
-
if (memorySummary) {
|
|
311
|
-
metrics.memorySummary.eligible += toFiniteNumber(memorySummary.eligible);
|
|
312
|
-
metrics.memorySummary.derived += toFiniteNumber(memorySummary.derived);
|
|
313
|
-
}
|
|
314
|
-
const memoryCleanup = result.memoryCleanup;
|
|
315
|
-
if (memoryCleanup) {
|
|
316
|
-
if (Array.isArray(memoryCleanup.pruneCandidates))
|
|
317
|
-
metrics.memoryCleanup.pruneCandidates += memoryCleanup.pruneCandidates.length;
|
|
318
|
-
if (Array.isArray(memoryCleanup.contradictionCandidates))
|
|
319
|
-
metrics.memoryCleanup.contradictionCandidates += memoryCleanup.contradictionCandidates.length;
|
|
320
|
-
if (Array.isArray(memoryCleanup.beliefStateTransitions))
|
|
321
|
-
metrics.memoryCleanup.beliefStateTransitions += memoryCleanup.beliefStateTransitions.length;
|
|
322
|
-
if (Array.isArray(memoryCleanup.consolidationCandidates))
|
|
323
|
-
metrics.memoryCleanup.consolidationCandidates += memoryCleanup.consolidationCandidates.length;
|
|
324
|
-
if (Array.isArray(memoryCleanup.archived))
|
|
325
|
-
metrics.memoryCleanup.archived += memoryCleanup.archived.length;
|
|
326
|
-
if (Array.isArray(memoryCleanup.warnings))
|
|
327
|
-
metrics.memoryCleanup.warnings += memoryCleanup.warnings.length;
|
|
328
|
-
}
|
|
329
|
-
const consolidation = result.consolidation;
|
|
330
|
-
if (consolidation) {
|
|
331
|
-
metrics.consolidation.processed += toFiniteNumber(consolidation.processed);
|
|
332
|
-
metrics.consolidation.merged += toFiniteNumber(consolidation.merged);
|
|
333
|
-
metrics.consolidation.deleted += toFiniteNumber(consolidation.deleted);
|
|
334
|
-
metrics.consolidation.contradicted += toFiniteNumber(consolidation.contradicted);
|
|
335
|
-
if (Array.isArray(consolidation.promoted))
|
|
336
|
-
metrics.consolidation.promoted += consolidation.promoted.length;
|
|
337
|
-
metrics.consolidation.failedChunks += toFiniteNumber(consolidation.failedChunks);
|
|
338
|
-
metrics.consolidation.totalChunks += toFiniteNumber(consolidation.totalChunks);
|
|
339
|
-
metrics.consolidation.durationMs += toFiniteNumber(consolidation.durationMs);
|
|
340
|
-
metrics.consolidation.judgedNoAction += toFiniteNumber(consolidation.judgedNoAction);
|
|
341
|
-
metrics.consolidation.mergedSecondaries += toFiniteNumber(consolidation.mergedSecondaries);
|
|
342
|
-
metrics.consolidation.failedChunkMemories += toFiniteNumber(consolidation.failedChunkMemories);
|
|
343
|
-
// Structured emitter (new on this branch): consolidate.ts now pushes
|
|
344
|
-
// per-ref grouped `{ref, skips: [{op, reason}]}` entries to `skipReasons`
|
|
345
|
-
// for every deterministic post-LLM rejection. Each ref appears once but
|
|
346
|
-
// may carry multiple skips; aggregate every reason. Pre-fix envelopes have
|
|
347
|
-
// neither field, so be defensive.
|
|
348
|
-
const skipReasons = consolidation.skipReasons;
|
|
349
|
-
if (Array.isArray(skipReasons)) {
|
|
350
|
-
for (const entry of skipReasons) {
|
|
351
|
-
if (!entry || typeof entry !== "object")
|
|
352
|
-
continue;
|
|
353
|
-
const skips = entry.skips;
|
|
354
|
-
if (!Array.isArray(skips))
|
|
355
|
-
continue;
|
|
356
|
-
for (const skip of skips) {
|
|
357
|
-
if (!skip || typeof skip !== "object")
|
|
358
|
-
continue;
|
|
359
|
-
const reason = skip.reason;
|
|
360
|
-
if (typeof reason !== "string" || !reason.trim())
|
|
361
|
-
continue;
|
|
362
|
-
metrics.consolidation.skipReasons[reason] = (metrics.consolidation.skipReasons[reason] ?? 0) + 1;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
const memoryInference = result.memoryInference;
|
|
368
|
-
if (memoryInference) {
|
|
369
|
-
const considered = toFiniteNumber(memoryInference.considered);
|
|
370
|
-
const writtenFacts = toFiniteNumber(memoryInference.writtenFacts);
|
|
371
|
-
metrics.memoryInference.considered += considered;
|
|
372
|
-
metrics.memoryInference.cacheHits += toFiniteNumber(memoryInference.cacheHits);
|
|
373
|
-
metrics.memoryInference.retryAttempts += toFiniteNumber(memoryInference.retryAttempts);
|
|
374
|
-
metrics.memoryInference.splitParents += toFiniteNumber(memoryInference.splitParents);
|
|
375
|
-
metrics.memoryInference.written += writtenFacts;
|
|
376
|
-
metrics.memoryInference.skippedNoFacts += toFiniteNumber(memoryInference.skippedNoFacts);
|
|
377
|
-
metrics.memoryInference.skippedChildExists += toFiniteNumber(memoryInference.skippedChildExists);
|
|
378
|
-
metrics.memoryInference.skippedAborted += toFiniteNumber(memoryInference.skippedAborted);
|
|
379
|
-
metrics.memoryInference.unaccounted += toFiniteNumber(memoryInference.unaccounted);
|
|
380
|
-
metrics.memoryInference.htmlErrorCount += toFiniteNumber(memoryInference.htmlErrorCount);
|
|
381
|
-
// Yield-rate gating: pre-cache-feature envelopes lack the `cacheHits`
|
|
382
|
-
// field entirely. Treating their `considered` as freshAttempts (since
|
|
383
|
-
// cacheHits=0) is mathematically tempting but operationally wrong —
|
|
384
|
-
// historical runs with the legacy schema have no cache instrumentation
|
|
385
|
-
// and the SUM dragged the reported rate to ~14% in local data. Only
|
|
386
|
-
// contribute to the yield aggregate when the envelope actually carries
|
|
387
|
-
// the field. See investigation 2026-05-26.
|
|
388
|
-
if (Object.hasOwn(memoryInference, "cacheHits")) {
|
|
389
|
-
metrics.memoryInference.yieldEligibleRuns += 1;
|
|
390
|
-
metrics.memoryInference.yieldEligibleConsidered += considered;
|
|
391
|
-
metrics.memoryInference.yieldEligibleWritten += writtenFacts;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
metrics.memoryInference.durationMs += toFiniteNumber(result.memoryInferenceDurationMs);
|
|
395
|
-
const graphExtraction = result.graphExtraction;
|
|
396
|
-
if (graphExtraction) {
|
|
397
|
-
const quality = graphExtraction.quality;
|
|
398
|
-
if (quality)
|
|
399
|
-
metrics.graphExtraction.extractedFiles += toFiniteNumber(quality.extractedFiles);
|
|
400
|
-
metrics.graphExtraction.entities += toFiniteNumber(graphExtraction.totalEntities);
|
|
401
|
-
metrics.graphExtraction.relations += toFiniteNumber(graphExtraction.totalRelations);
|
|
402
|
-
const telemetry = graphExtraction.telemetry;
|
|
403
|
-
if (telemetry) {
|
|
404
|
-
metrics.graphExtraction.cacheHits += toFiniteNumber(telemetry.cacheHits);
|
|
405
|
-
metrics.graphExtraction.cacheMisses += toFiniteNumber(telemetry.cacheMisses);
|
|
406
|
-
metrics.graphExtraction.truncations += toFiniteNumber(telemetry.truncationCount);
|
|
407
|
-
metrics.graphExtraction.failures += toFiniteNumber(telemetry.failureCount);
|
|
408
|
-
metrics.graphExtraction.htmlErrors += toFiniteNumber(telemetry.htmlErrorCount);
|
|
409
|
-
metrics.graphExtraction.retryAttempts += toFiniteNumber(telemetry.retryAttempts);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
metrics.graphExtraction.durationMs += toFiniteNumber(result.graphExtractionDurationMs);
|
|
413
|
-
if (Array.isArray(result.extract)) {
|
|
414
|
-
for (const e of result.extract) {
|
|
415
|
-
metrics.sessionExtraction.sessionsScanned += toFiniteNumber(e.sessionsProcessed);
|
|
416
|
-
metrics.sessionExtraction.sessionsSkipped += toFiniteNumber(e.sessionsSkipped);
|
|
417
|
-
if (Array.isArray(e.sessions)) {
|
|
418
|
-
metrics.sessionExtraction.sessionsExtracted += e.sessions.filter((s) => Array.isArray(s.proposalIds) && s.proposalIds.length > 0).length;
|
|
419
|
-
}
|
|
420
|
-
metrics.sessionExtraction.proposalsCreated += Array.isArray(e.proposals) ? e.proposals.length : 0;
|
|
421
|
-
metrics.sessionExtraction.warnings += Array.isArray(e.warnings) ? e.warnings.length : 0;
|
|
422
|
-
metrics.sessionExtraction.durationMs += toFiniteNumber(e.durationMs);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
return metrics;
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
|
-
* Finalize derived flags and rates on an accumulator. Used both for the
|
|
429
|
-
* window-level aggregate and for each per-run row in --detail per-run mode
|
|
430
|
-
* so the single-row metrics still expose `ran` / `yieldRate` / `cacheHitRate`.
|
|
431
|
-
*/
|
|
432
|
-
function finalizeImproveMetrics(metrics) {
|
|
433
|
-
metrics.consolidation.ran =
|
|
434
|
-
metrics.consolidation.processed > 0 ||
|
|
435
|
-
metrics.consolidation.durationMs > 0 ||
|
|
436
|
-
metrics.consolidation.promoted > 0 ||
|
|
437
|
-
metrics.consolidation.merged > 0 ||
|
|
438
|
-
metrics.consolidation.deleted > 0 ||
|
|
439
|
-
metrics.consolidation.contradicted > 0 ||
|
|
440
|
-
metrics.consolidation.totalChunks > 0;
|
|
441
|
-
metrics.memoryInference.ran =
|
|
442
|
-
metrics.memoryInference.considered > 0 ||
|
|
443
|
-
metrics.memoryInference.written > 0 ||
|
|
444
|
-
metrics.memoryInference.durationMs > 0;
|
|
445
|
-
metrics.memoryInference.writes = metrics.memoryInference.written;
|
|
446
|
-
// Yield denominator excludes cache hits AND legacy (pre-cacheHits-field)
|
|
447
|
-
// envelopes. Only runs whose envelope carries a `cacheHits` field
|
|
448
|
-
// contribute to freshAttempts/yieldRate; legacy rows remain in
|
|
449
|
-
// `considered`/`written` for totals but are excluded from the rate so
|
|
450
|
-
// they cannot drag it down. See ImproveHealthMetrics.memoryInference
|
|
451
|
-
// jsdoc for the rationale.
|
|
452
|
-
metrics.memoryInference.freshAttempts = Math.max(0, metrics.memoryInference.yieldEligibleConsidered -
|
|
453
|
-
metrics.memoryInference.cacheHits -
|
|
454
|
-
metrics.memoryInference.skippedAborted);
|
|
455
|
-
metrics.memoryInference.yieldRate =
|
|
456
|
-
metrics.memoryInference.freshAttempts > 0
|
|
457
|
-
? roundRate(metrics.memoryInference.yieldEligibleWritten / metrics.memoryInference.freshAttempts)
|
|
458
|
-
: 0;
|
|
459
|
-
metrics.graphExtraction.ran =
|
|
460
|
-
metrics.graphExtraction.extractedFiles > 0 ||
|
|
461
|
-
metrics.graphExtraction.entities > 0 ||
|
|
462
|
-
metrics.graphExtraction.durationMs > 0;
|
|
463
|
-
const cacheTotal = metrics.graphExtraction.cacheHits + metrics.graphExtraction.cacheMisses;
|
|
464
|
-
metrics.graphExtraction.cacheHitRate = cacheTotal > 0 ? roundRate(metrics.graphExtraction.cacheHits / cacheTotal) : 0;
|
|
465
|
-
metrics.sessionExtraction.ran =
|
|
466
|
-
metrics.sessionExtraction.sessionsScanned > 0 ||
|
|
467
|
-
metrics.sessionExtraction.proposalsCreated > 0 ||
|
|
468
|
-
metrics.sessionExtraction.durationMs > 0;
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Merge per-row metrics from `src` into accumulator `dst`. All numeric fields
|
|
472
|
-
* are additive; cumulative rates are recomputed by finalizeImproveMetrics.
|
|
473
|
-
*/
|
|
474
|
-
function mergeImproveMetrics(dst, src) {
|
|
475
|
-
dst.plannedRefs += src.plannedRefs;
|
|
476
|
-
dst.profileFilteredRefs += src.profileFilteredRefs;
|
|
477
|
-
dst.actions.reflect.ok += src.actions.reflect.ok;
|
|
478
|
-
dst.actions.reflect.failed += src.actions.reflect.failed;
|
|
479
|
-
dst.actions.reflect.cooldown += src.actions.reflect.cooldown;
|
|
480
|
-
dst.actions.reflect.skipped += src.actions.reflect.skipped;
|
|
481
|
-
dst.actions.reflect.guardRejected += src.actions.reflect.guardRejected;
|
|
482
|
-
for (const [reason, count] of Object.entries(src.actions.reflect.skippedByReason)) {
|
|
483
|
-
dst.actions.reflect.skippedByReason[reason] = (dst.actions.reflect.skippedByReason[reason] ?? 0) + count;
|
|
484
|
-
}
|
|
485
|
-
dst.actions.distill.queued += src.actions.distill.queued;
|
|
486
|
-
dst.actions.distill.llmFailed += src.actions.distill.llmFailed;
|
|
487
|
-
dst.actions.distill.qualityRejected += src.actions.distill.qualityRejected;
|
|
488
|
-
dst.actions.distill.judgeRejected += src.actions.distill.judgeRejected;
|
|
489
|
-
dst.actions.distill.validatorRejected += src.actions.distill.validatorRejected;
|
|
490
|
-
dst.actions.distill.configDisabled += src.actions.distill.configDisabled;
|
|
491
|
-
dst.actions.distill.skipped += src.actions.distill.skipped;
|
|
492
|
-
for (const [reason, count] of Object.entries(src.actions.distill.skippedByReason)) {
|
|
493
|
-
dst.actions.distill.skippedByReason[reason] = (dst.actions.distill.skippedByReason[reason] ?? 0) + count;
|
|
494
|
-
}
|
|
495
|
-
dst.actions.distill.deferred += src.actions.distill.deferred;
|
|
496
|
-
for (const [reason, count] of Object.entries(src.actions.distill.deferredByReason)) {
|
|
497
|
-
dst.actions.distill.deferredByReason[reason] = (dst.actions.distill.deferredByReason[reason] ?? 0) + count;
|
|
498
|
-
}
|
|
499
|
-
dst.actions.memoryPrune += src.actions.memoryPrune;
|
|
500
|
-
dst.actions.memoryInference += src.actions.memoryInference;
|
|
501
|
-
dst.actions.graphExtraction += src.actions.graphExtraction;
|
|
502
|
-
dst.actions.error += src.actions.error;
|
|
503
|
-
dst.autoAccept.promoted += src.autoAccept.promoted;
|
|
504
|
-
dst.autoAccept.validationFailed += src.autoAccept.validationFailed;
|
|
505
|
-
dst.reflectsWithErrorContext += src.reflectsWithErrorContext;
|
|
506
|
-
dst.coverageGapCount += src.coverageGapCount;
|
|
507
|
-
dst.evalCasesWritten += src.evalCasesWritten;
|
|
508
|
-
dst.deadUrlCount += src.deadUrlCount;
|
|
509
|
-
dst.memorySummary.eligible += src.memorySummary.eligible;
|
|
510
|
-
dst.memorySummary.derived += src.memorySummary.derived;
|
|
511
|
-
dst.memoryCleanup.pruneCandidates += src.memoryCleanup.pruneCandidates;
|
|
512
|
-
dst.memoryCleanup.contradictionCandidates += src.memoryCleanup.contradictionCandidates;
|
|
513
|
-
dst.memoryCleanup.beliefStateTransitions += src.memoryCleanup.beliefStateTransitions;
|
|
514
|
-
dst.memoryCleanup.consolidationCandidates += src.memoryCleanup.consolidationCandidates;
|
|
515
|
-
dst.memoryCleanup.archived += src.memoryCleanup.archived;
|
|
516
|
-
dst.memoryCleanup.warnings += src.memoryCleanup.warnings;
|
|
517
|
-
dst.consolidation.processed += src.consolidation.processed;
|
|
518
|
-
dst.consolidation.promoted += src.consolidation.promoted;
|
|
519
|
-
dst.consolidation.merged += src.consolidation.merged;
|
|
520
|
-
dst.consolidation.deleted += src.consolidation.deleted;
|
|
521
|
-
dst.consolidation.contradicted += src.consolidation.contradicted;
|
|
522
|
-
dst.consolidation.failedChunks += src.consolidation.failedChunks;
|
|
523
|
-
dst.consolidation.totalChunks += src.consolidation.totalChunks;
|
|
524
|
-
dst.consolidation.durationMs += src.consolidation.durationMs;
|
|
525
|
-
dst.consolidation.judgedNoAction += src.consolidation.judgedNoAction;
|
|
526
|
-
dst.consolidation.mergedSecondaries += src.consolidation.mergedSecondaries;
|
|
527
|
-
dst.consolidation.failedChunkMemories += src.consolidation.failedChunkMemories;
|
|
528
|
-
for (const [reason, count] of Object.entries(src.consolidation.skipReasons)) {
|
|
529
|
-
dst.consolidation.skipReasons[reason] = (dst.consolidation.skipReasons[reason] ?? 0) + count;
|
|
530
|
-
}
|
|
531
|
-
dst.memoryInference.considered += src.memoryInference.considered;
|
|
532
|
-
dst.memoryInference.cacheHits += src.memoryInference.cacheHits;
|
|
533
|
-
dst.memoryInference.splitParents += src.memoryInference.splitParents;
|
|
534
|
-
dst.memoryInference.written += src.memoryInference.written;
|
|
535
|
-
dst.memoryInference.skippedNoFacts += src.memoryInference.skippedNoFacts;
|
|
536
|
-
dst.memoryInference.skippedChildExists += src.memoryInference.skippedChildExists;
|
|
537
|
-
dst.memoryInference.skippedAborted += src.memoryInference.skippedAborted;
|
|
538
|
-
dst.memoryInference.unaccounted += src.memoryInference.unaccounted;
|
|
539
|
-
dst.memoryInference.htmlErrorCount += src.memoryInference.htmlErrorCount;
|
|
540
|
-
dst.memoryInference.yieldEligibleRuns += src.memoryInference.yieldEligibleRuns;
|
|
541
|
-
dst.memoryInference.yieldEligibleConsidered += src.memoryInference.yieldEligibleConsidered;
|
|
542
|
-
dst.memoryInference.yieldEligibleWritten += src.memoryInference.yieldEligibleWritten;
|
|
543
|
-
dst.memoryInference.durationMs += src.memoryInference.durationMs;
|
|
544
|
-
dst.graphExtraction.extractedFiles += src.graphExtraction.extractedFiles;
|
|
545
|
-
dst.graphExtraction.entities += src.graphExtraction.entities;
|
|
546
|
-
dst.graphExtraction.relations += src.graphExtraction.relations;
|
|
547
|
-
dst.graphExtraction.cacheHits += src.graphExtraction.cacheHits;
|
|
548
|
-
dst.graphExtraction.cacheMisses += src.graphExtraction.cacheMisses;
|
|
549
|
-
dst.graphExtraction.truncations += src.graphExtraction.truncations;
|
|
550
|
-
dst.graphExtraction.failures += src.graphExtraction.failures;
|
|
551
|
-
dst.graphExtraction.htmlErrors += src.graphExtraction.htmlErrors;
|
|
552
|
-
dst.graphExtraction.durationMs += src.graphExtraction.durationMs;
|
|
553
|
-
dst.sessionExtraction.sessionsScanned += src.sessionExtraction.sessionsScanned;
|
|
554
|
-
dst.sessionExtraction.sessionsExtracted += src.sessionExtraction.sessionsExtracted;
|
|
555
|
-
dst.sessionExtraction.sessionsSkipped += src.sessionExtraction.sessionsSkipped;
|
|
556
|
-
dst.sessionExtraction.proposalsCreated += src.sessionExtraction.proposalsCreated;
|
|
557
|
-
dst.sessionExtraction.warnings += src.sessionExtraction.warnings;
|
|
558
|
-
dst.sessionExtraction.durationMs += src.sessionExtraction.durationMs;
|
|
559
|
-
}
|
|
560
|
-
function summarizeImproveRuns(db, since, until) {
|
|
561
|
-
const accum = createUnknownImproveMetrics();
|
|
562
|
-
const rows = queryImproveRuns(db, since, until);
|
|
563
|
-
// Per-phase wall-time samples. Each entry is one envelope's durationMs for
|
|
564
|
-
// that phase. Phases that did not run on a given envelope are simply
|
|
565
|
-
// omitted (NOT counted as 0) so the median/p95 reflect actual phase work.
|
|
566
|
-
const phaseDurations = {
|
|
567
|
-
consolidation: [],
|
|
568
|
-
memoryInference: [],
|
|
569
|
-
graphExtraction: [],
|
|
570
|
-
};
|
|
571
|
-
for (const row of rows) {
|
|
572
|
-
let result;
|
|
573
|
-
try {
|
|
574
|
-
result = JSON.parse(row.result_json);
|
|
575
|
-
}
|
|
576
|
-
catch {
|
|
577
|
-
continue;
|
|
578
|
-
}
|
|
579
|
-
const perRow = projectRunMetrics(result);
|
|
580
|
-
mergeImproveMetrics(accum, perRow);
|
|
581
|
-
// Collect per-phase durations directly off the envelope. consolidation's
|
|
582
|
-
// duration lives inside the sub-object; memoryInference and graphExtraction
|
|
583
|
-
// expose top-level *DurationMs keys (`memoryInferenceDurationMs`,
|
|
584
|
-
// `graphExtractionDurationMs`) when they actually ran on that envelope.
|
|
585
|
-
const consol = result.consolidation;
|
|
586
|
-
const consolMs = toFiniteNumber(consol?.durationMs);
|
|
587
|
-
if (consolMs > 0)
|
|
588
|
-
phaseDurations.consolidation.push(consolMs);
|
|
589
|
-
const memMs = toFiniteNumber(result.memoryInferenceDurationMs);
|
|
590
|
-
if (memMs > 0)
|
|
591
|
-
phaseDurations.memoryInference.push(memMs);
|
|
592
|
-
const graphMs = toFiniteNumber(result.graphExtractionDurationMs);
|
|
593
|
-
if (graphMs > 0)
|
|
594
|
-
phaseDurations.graphExtraction.push(graphMs);
|
|
595
|
-
}
|
|
596
|
-
finalizeImproveMetrics(accum);
|
|
597
|
-
accum.wallTime.byPhase = {
|
|
598
|
-
consolidation: summarizePhaseDurations(phaseDurations.consolidation),
|
|
599
|
-
memoryInference: summarizePhaseDurations(phaseDurations.memoryInference),
|
|
600
|
-
graphExtraction: summarizePhaseDurations(phaseDurations.graphExtraction),
|
|
601
|
-
};
|
|
602
|
-
return { metrics: accum, runCount: rows.length };
|
|
603
|
-
}
|
|
604
|
-
/**
|
|
605
|
-
* Aggregate a list of per-envelope phase durations into the
|
|
606
|
-
* `wallTime.byPhase.*` shape: count, total, median, p95. Median/p95 use the
|
|
607
|
-
* same nearest-rank picker as the top-level wallTime stats so the two are
|
|
608
|
-
* comparable.
|
|
609
|
-
*/
|
|
610
|
-
function summarizePhaseDurations(samples) {
|
|
611
|
-
if (samples.length === 0)
|
|
612
|
-
return { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 };
|
|
613
|
-
const sorted = [...samples].sort((a, b) => a - b);
|
|
614
|
-
const pick = (q) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))] ?? 0;
|
|
615
|
-
const totalMs = sorted.reduce((acc, n) => acc + n, 0);
|
|
616
|
-
return {
|
|
617
|
-
count: sorted.length,
|
|
618
|
-
totalMs,
|
|
619
|
-
medianMs: pick(0.5),
|
|
620
|
-
p95Ms: pick(0.95),
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* Project an improve_runs row + wall-time lookup into a single ImproveRunSummary.
|
|
625
|
-
* Used by `akm health --detail per-run`.
|
|
626
|
-
*/
|
|
627
|
-
function projectImproveRunSummary(row, wallTimeMs) {
|
|
628
|
-
let result = {};
|
|
629
|
-
try {
|
|
630
|
-
result = JSON.parse(row.result_json);
|
|
631
|
-
}
|
|
632
|
-
catch {
|
|
633
|
-
// fall through with empty result so per-stage rollups are zeros
|
|
634
|
-
}
|
|
635
|
-
const perRow = projectRunMetrics(result);
|
|
636
|
-
finalizeImproveMetrics(perRow);
|
|
637
|
-
const orphansPurged = toFiniteNumber(result.orphansPurged);
|
|
638
|
-
const lintSummary = result.lintSummary;
|
|
639
|
-
const lintFixed = lintSummary ? toFiniteNumber(lintSummary.fixed) : 0;
|
|
640
|
-
const lintFlagged = lintSummary ? toFiniteNumber(lintSummary.flagged) : 0;
|
|
641
|
-
return {
|
|
642
|
-
id: row.id,
|
|
643
|
-
startedAt: row.started_at,
|
|
644
|
-
completedAt: row.completed_at,
|
|
645
|
-
wallTimeMs,
|
|
646
|
-
ok: row.ok === 1,
|
|
647
|
-
scope: {
|
|
648
|
-
mode: row.scope_mode,
|
|
649
|
-
...(row.scope_value ? { value: row.scope_value } : {}),
|
|
650
|
-
},
|
|
651
|
-
actions: perRow.actions,
|
|
652
|
-
memorySummary: perRow.memorySummary,
|
|
653
|
-
memoryCleanup: perRow.memoryCleanup,
|
|
654
|
-
consolidation: perRow.consolidation,
|
|
655
|
-
memoryInference: perRow.memoryInference,
|
|
656
|
-
graphExtraction: perRow.graphExtraction,
|
|
657
|
-
reflectsWithErrorContext: perRow.reflectsWithErrorContext,
|
|
658
|
-
evalCasesWritten: perRow.evalCasesWritten,
|
|
659
|
-
orphansPurged,
|
|
660
|
-
lintFixed,
|
|
661
|
-
lintFlagged,
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
/**
|
|
665
|
-
* Load task_history intervals for `task_id='akm-improve'` in the window.
|
|
666
|
-
* Returned sorted by startMs ascending so containment lookups can use a
|
|
667
|
-
* linear scan (typical N is ~24/day; not worth a tree).
|
|
668
|
-
*
|
|
669
|
-
* The window filter is widened by 5 minutes on each side because the cron
|
|
670
|
-
* task wraps `akm improve` — the task `started_at` fires at e.g. :07:01
|
|
671
|
-
* while `recordImproveRun` writes the matching `improve_runs.started_at`
|
|
672
|
-
* later (after config load, planning, etc.), so the improve_runs row can
|
|
673
|
-
* be inside the window even when its enclosing task_history row started
|
|
674
|
-
* just before the window opened.
|
|
675
|
-
*/
|
|
676
|
-
function loadTaskIntervals(db, since, until) {
|
|
677
|
-
const sinceMs = new Date(since).getTime();
|
|
678
|
-
const untilMs = until ? new Date(until).getTime() : Number.POSITIVE_INFINITY;
|
|
679
|
-
const widenedSince = new Date(sinceMs - 5 * 60 * 1000).toISOString();
|
|
680
|
-
const widenedUntil = Number.isFinite(untilMs) ? new Date(untilMs + 5 * 60 * 1000).toISOString() : undefined;
|
|
681
|
-
const rows = queryCompletedTaskIntervals(db, widenedSince, widenedUntil);
|
|
682
|
-
const intervals = [];
|
|
683
|
-
for (const row of rows) {
|
|
684
|
-
const startMs = new Date(row.started_at).getTime();
|
|
685
|
-
const endMs = new Date(row.completed_at).getTime();
|
|
686
|
-
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs)
|
|
687
|
-
continue;
|
|
688
|
-
intervals.push({ startMs, endMs, durationMs: endMs - startMs });
|
|
689
|
-
}
|
|
690
|
-
return intervals;
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* Find the task_history interval that contains the given timestamp. The
|
|
694
|
-
* task wraps `akm improve`, so `improve_runs.started_at` (when
|
|
695
|
-
* `recordImproveRun` writes) always falls inside the enclosing task's
|
|
696
|
-
* [started_at, completed_at]. Returns undefined when no interval
|
|
697
|
-
* contains the timestamp (which happens for manually-invoked improve
|
|
698
|
-
* runs not driven by the `akm-improve` task).
|
|
699
|
-
*
|
|
700
|
-
* Linear scan because N is small. We tolerate a 1s slop on the upper
|
|
701
|
-
* bound to handle clock skew between the wrapper's `completed_at` write
|
|
702
|
-
* and recordImproveRun's `started_at` write.
|
|
703
|
-
*/
|
|
704
|
-
function findContainingTaskInterval(timestampMs, intervals) {
|
|
705
|
-
const SLOP_MS = 1000;
|
|
706
|
-
for (const interval of intervals) {
|
|
707
|
-
if (timestampMs >= interval.startMs && timestampMs <= interval.endMs + SLOP_MS) {
|
|
708
|
-
return interval;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
return undefined;
|
|
712
|
-
}
|
|
713
|
-
function buildPerRunSummaries(db, since, until) {
|
|
714
|
-
const rows = queryImproveRuns(db, since, until);
|
|
715
|
-
const taskIntervals = loadTaskIntervals(db, since, until);
|
|
716
|
-
const summaries = [];
|
|
717
|
-
for (const row of rows) {
|
|
718
|
-
const startMs = new Date(row.started_at).getTime();
|
|
719
|
-
const endMs = new Date(row.completed_at).getTime();
|
|
720
|
-
// Prefer the improve_runs row's own (completed_at - started_at) delta:
|
|
721
|
-
// recordImproveRun now persists distinct start/end timestamps, so the
|
|
722
|
-
// row's own delta is the authoritative per-run wall time even for
|
|
723
|
-
// manually-invoked `akm improve` runs with no enclosing task_history.
|
|
724
|
-
// Only fall back to the task_history containing-interval join for legacy/
|
|
725
|
-
// backfill rows where started_at == completed_at (row delta is 0).
|
|
726
|
-
const hasRowDelta = Number.isFinite(startMs) && Number.isFinite(endMs) && endMs > startMs;
|
|
727
|
-
let wallTimeMs;
|
|
728
|
-
if (hasRowDelta) {
|
|
729
|
-
wallTimeMs = endMs - startMs;
|
|
730
|
-
}
|
|
731
|
-
else {
|
|
732
|
-
const interval = Number.isFinite(startMs) ? findContainingTaskInterval(startMs, taskIntervals) : undefined;
|
|
733
|
-
wallTimeMs = interval?.durationMs ?? 0;
|
|
734
|
-
}
|
|
735
|
-
summaries.push(projectImproveRunSummary(row, wallTimeMs));
|
|
736
|
-
}
|
|
737
|
-
return summaries;
|
|
738
|
-
}
|
|
739
|
-
function emptyPhaseStats() {
|
|
740
|
-
return {
|
|
741
|
-
consolidation: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
|
|
742
|
-
memoryInference: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
|
|
743
|
-
graphExtraction: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
function computeWallTimeStats(durationsMs, byPhase) {
|
|
747
|
-
const phase = byPhase ?? emptyPhaseStats();
|
|
748
|
-
if (durationsMs.length === 0)
|
|
749
|
-
return { count: 0, medianMs: 0, p95Ms: 0, minMs: 0, maxMs: 0, byPhase: phase };
|
|
750
|
-
const sorted = [...durationsMs].sort((a, b) => a - b);
|
|
751
|
-
const pick = (q) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))] ?? 0;
|
|
752
|
-
return {
|
|
753
|
-
count: sorted.length,
|
|
754
|
-
medianMs: pick(0.5),
|
|
755
|
-
p95Ms: pick(0.95),
|
|
756
|
-
minMs: sorted[0] ?? 0,
|
|
757
|
-
maxMs: sorted[sorted.length - 1] ?? 0,
|
|
758
|
-
byPhase: phase,
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
function buildImproveSkipSummary(events) {
|
|
762
|
-
const skipReasons = {};
|
|
763
|
-
let skipped = 0;
|
|
764
|
-
for (const event of events) {
|
|
765
|
-
const reason = typeof event.metadata?.reason === "string" && event.metadata.reason.trim() ? event.metadata.reason : "unknown";
|
|
766
|
-
// Aggregated skip events (e.g. `no_new_signal`, `profile_filtered_all_passes`)
|
|
767
|
-
// carry a `count` of the refs they represent in a single row instead of one
|
|
768
|
-
// event per ref. Honor that count so the skip histogram reflects the true
|
|
769
|
-
// number of skipped refs; per-ref events without a count contribute 1.
|
|
770
|
-
const rawCount = event.metadata?.count;
|
|
771
|
-
const count = typeof rawCount === "number" && Number.isFinite(rawCount) && rawCount > 0 ? rawCount : 1;
|
|
772
|
-
skipReasons[reason] = (skipReasons[reason] ?? 0) + count;
|
|
773
|
-
skipped += count;
|
|
774
|
-
}
|
|
775
|
-
return { skipped, skipReasons };
|
|
776
|
-
}
|
|
777
|
-
function probeStateDbRoundTrip(stateDbPath) {
|
|
778
|
-
const before = readEvents({}, { dbPath: stateDbPath }).nextOffset;
|
|
779
|
-
const started = Date.now();
|
|
780
|
-
appendEvent({ eventType: HEALTH_PROBE_EVENT, ref: "health:probe", metadata: { source: "akm health" } }, { dbPath: stateDbPath });
|
|
781
|
-
const after = readEvents({ sinceOffset: before, type: HEALTH_PROBE_EVENT, ref: "health:probe" }, { dbPath: stateDbPath });
|
|
782
|
-
const durationMs = Date.now() - started;
|
|
783
|
-
if (after.events.length === 0 || after.nextOffset <= before) {
|
|
784
|
-
return { ok: false, durationMs, error: "probe event was not readable after append" };
|
|
785
|
-
}
|
|
786
|
-
return { ok: true, durationMs };
|
|
787
|
-
}
|
|
788
|
-
/**
|
|
789
|
-
* Parse a `--window-compare <duration>` shorthand into two adjacent windows
|
|
790
|
-
* (current, prior). Duration syntax matches {@link parseHealthSince}.
|
|
791
|
-
*/
|
|
792
|
-
function resolveWindowCompare(duration, now = () => Date.now()) {
|
|
793
|
-
const trimmed = duration.trim();
|
|
794
|
-
const durationMatch = trimmed.match(/^(\d+)([dhm])$/i);
|
|
795
|
-
if (!durationMatch) {
|
|
796
|
-
throw new UsageError("--window-compare must be a duration like '24h', '7d', or '30m'.", "INVALID_FLAG_VALUE");
|
|
797
|
-
}
|
|
798
|
-
const amount = Number.parseInt(durationMatch[1] ?? "0", 10);
|
|
799
|
-
const unit = (durationMatch[2] ?? "h").toLowerCase();
|
|
800
|
-
if (!Number.isFinite(amount) || amount <= 0) {
|
|
801
|
-
throw new UsageError("--window-compare must be a positive duration.", "INVALID_FLAG_VALUE");
|
|
802
|
-
}
|
|
803
|
-
const multiplier = unit === "h" ? 60 * 60 * 1000 : unit === "m" ? 60 * 1000 : 24 * 60 * 60 * 1000;
|
|
804
|
-
const ms = amount * multiplier;
|
|
805
|
-
const nowMs = now();
|
|
806
|
-
const currentSince = new Date(nowMs - ms).toISOString();
|
|
807
|
-
const currentUntil = new Date(nowMs).toISOString();
|
|
808
|
-
const priorSince = new Date(nowMs - 2 * ms).toISOString();
|
|
809
|
-
const priorUntil = currentSince;
|
|
810
|
-
return [
|
|
811
|
-
{ name: "current", since: currentSince, until: currentUntil },
|
|
812
|
-
{ name: "prior", since: priorSince, until: priorUntil },
|
|
813
|
-
];
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* Parse a single repeatable `--windows` value of the form
|
|
817
|
-
* `name=...,since=...,until=...`. All keys are optional EXCEPT name and since.
|
|
818
|
-
*/
|
|
819
|
-
export function parseWindowSpec(raw) {
|
|
820
|
-
const fields = {};
|
|
821
|
-
for (const part of raw.split(",")) {
|
|
822
|
-
const trimmed = part.trim();
|
|
823
|
-
if (!trimmed)
|
|
824
|
-
continue;
|
|
825
|
-
const eq = trimmed.indexOf("=");
|
|
826
|
-
if (eq < 0) {
|
|
827
|
-
throw new UsageError(`--windows entry must be a comma-separated list of key=value pairs: ${raw}`, "INVALID_FLAG_VALUE");
|
|
828
|
-
}
|
|
829
|
-
const key = trimmed.slice(0, eq).trim();
|
|
830
|
-
const value = trimmed.slice(eq + 1).trim();
|
|
831
|
-
fields[key] = value;
|
|
832
|
-
}
|
|
833
|
-
if (!fields.name) {
|
|
834
|
-
throw new UsageError(`--windows entry is missing required 'name': ${raw}`, "INVALID_FLAG_VALUE");
|
|
835
|
-
}
|
|
836
|
-
if (!fields.since) {
|
|
837
|
-
throw new UsageError(`--windows entry is missing required 'since': ${raw}`, "INVALID_FLAG_VALUE");
|
|
838
|
-
}
|
|
839
|
-
return {
|
|
840
|
-
name: fields.name,
|
|
841
|
-
since: fields.since,
|
|
842
|
-
...(fields.until ? { until: fields.until } : {}),
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
/** Hard-coded list of "interesting" metric paths for window-compare deltas. */
|
|
846
|
-
const INTERESTING_DELTA_PATHS = [
|
|
847
|
-
"improve.actions.reflect.failed",
|
|
848
|
-
"improve.actions.reflect.guardRejected",
|
|
849
|
-
"improve.actions.distill.llmFailed",
|
|
850
|
-
"improve.actions.distill.queued",
|
|
851
|
-
"improve.actions.distill.deferred",
|
|
852
|
-
"improve.consolidation.promoted",
|
|
853
|
-
"improve.memoryInference.written",
|
|
854
|
-
"improve.memoryInference.yieldRate",
|
|
855
|
-
"improve.memoryInference.skippedNoFacts",
|
|
856
|
-
"improve.memoryInference.htmlErrorCount",
|
|
857
|
-
"improve.graphExtraction.cacheHitRate",
|
|
858
|
-
"improve.graphExtraction.failures",
|
|
859
|
-
"improve.graphExtraction.htmlErrors",
|
|
860
|
-
"improve.sessionExtraction.sessionsScanned",
|
|
861
|
-
"improve.sessionExtraction.proposalsCreated",
|
|
862
|
-
"improve.autoAccept.promoted",
|
|
863
|
-
"improve.autoAccept.validationFailed",
|
|
864
|
-
"improve.wallTime.medianMs",
|
|
865
|
-
"improve.wallTime.p95Ms",
|
|
866
|
-
];
|
|
867
|
-
function readNumericPath(obj, path) {
|
|
868
|
-
const parts = path.split(".");
|
|
869
|
-
let cursor = obj;
|
|
870
|
-
for (const part of parts) {
|
|
871
|
-
if (typeof cursor !== "object" || cursor === null)
|
|
872
|
-
return 0;
|
|
873
|
-
cursor = cursor[part];
|
|
874
|
-
}
|
|
875
|
-
return typeof cursor === "number" && Number.isFinite(cursor) ? cursor : 0;
|
|
876
|
-
}
|
|
877
|
-
function computeDeltas(first, last) {
|
|
878
|
-
const out = {};
|
|
879
|
-
for (const path of INTERESTING_DELTA_PATHS) {
|
|
880
|
-
const from = readNumericPath(first, path);
|
|
881
|
-
const to = readNumericPath(last, path);
|
|
882
|
-
if (from === 0 && to === 0)
|
|
883
|
-
continue;
|
|
884
|
-
let pctChange;
|
|
885
|
-
if (from === 0) {
|
|
886
|
-
pctChange = to === 0 ? 0 : "+inf";
|
|
887
|
-
}
|
|
888
|
-
else {
|
|
889
|
-
pctChange = Number((((to - from) / from) * 100).toFixed(2));
|
|
890
|
-
}
|
|
891
|
-
out[path] = { from, to, pctChange };
|
|
892
|
-
}
|
|
893
|
-
return out;
|
|
894
|
-
}
|
|
895
|
-
/**
|
|
896
|
-
* Partition task_history rows into "should have a log" (non-null log_path) and
|
|
897
|
-
* "log is actually backed". A run counts as backed when logs.db holds rows for
|
|
898
|
-
* its run_id (#579 — the DB is the primary record); rows written before logs.db
|
|
899
|
-
* existed fall back to the transitional on-disk file check. `logsDb` may be
|
|
900
|
-
* undefined when logs.db could not be opened — then only the file check runs.
|
|
901
|
-
*/
|
|
902
|
-
function partitionLogBackedRows(taskRows, logsDb) {
|
|
903
|
-
const withLogs = taskRows.filter((row) => row.log_path !== null);
|
|
904
|
-
const loggedRunIds = logsDb
|
|
905
|
-
? getLoggedRunIds(logsDb, withLogs.map((row) => buildTaskRunId(row.task_id, row.started_at)))
|
|
906
|
-
: new Set();
|
|
907
|
-
const backed = withLogs.filter((row) => loggedRunIds.has(buildTaskRunId(row.task_id, row.started_at)) ||
|
|
908
|
-
(row.log_path !== null && fs.existsSync(row.log_path)));
|
|
909
|
-
return { withLogs, backed };
|
|
910
|
-
}
|
|
911
|
-
/** Stage key used for `llm_usage` events recorded outside any stage scope. */
|
|
912
|
-
const UNATTRIBUTED_STAGE = "unattributed";
|
|
913
|
-
function emptyLlmUsageStageAggregate() {
|
|
914
|
-
return {
|
|
915
|
-
calls: 0,
|
|
916
|
-
totalDurationMs: 0,
|
|
917
|
-
promptTokens: 0,
|
|
918
|
-
completionTokens: 0,
|
|
919
|
-
totalTokens: 0,
|
|
920
|
-
reasoningTokens: 0,
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
function emptyLlmUsageAggregate() {
|
|
924
|
-
return { ...emptyLlmUsageStageAggregate(), byStage: {} };
|
|
925
|
-
}
|
|
926
|
-
/**
|
|
927
|
-
* Aggregate `llm_usage` events (#576) into a window total plus a per-stage
|
|
928
|
-
* breakdown of call count, wall-time, and token usage. Token fields absent from
|
|
929
|
-
* a best-effort record contribute 0. Calls with no `stage` land under
|
|
930
|
-
* {@link UNATTRIBUTED_STAGE}.
|
|
931
|
-
*/
|
|
932
|
-
function summarizeLlmUsage(events) {
|
|
933
|
-
const aggregate = emptyLlmUsageAggregate();
|
|
934
|
-
for (const event of events) {
|
|
935
|
-
const meta = event.metadata ?? {};
|
|
936
|
-
const stageKey = typeof meta.stage === "string" && meta.stage ? meta.stage : UNATTRIBUTED_STAGE;
|
|
937
|
-
let stage = aggregate.byStage[stageKey];
|
|
938
|
-
if (!stage) {
|
|
939
|
-
stage = emptyLlmUsageStageAggregate();
|
|
940
|
-
aggregate.byStage[stageKey] = stage;
|
|
941
|
-
}
|
|
942
|
-
const durationMs = toFiniteNumber(meta.durationMs);
|
|
943
|
-
const promptTokens = toFiniteNumber(meta.promptTokens);
|
|
944
|
-
const completionTokens = toFiniteNumber(meta.completionTokens);
|
|
945
|
-
const totalTokens = toFiniteNumber(meta.totalTokens);
|
|
946
|
-
const reasoningTokens = toFiniteNumber(meta.reasoningTokens);
|
|
947
|
-
for (const target of [aggregate, stage]) {
|
|
948
|
-
target.calls += 1;
|
|
949
|
-
target.totalDurationMs += durationMs;
|
|
950
|
-
target.promptTokens += promptTokens;
|
|
951
|
-
target.completionTokens += completionTokens;
|
|
952
|
-
target.totalTokens += totalTokens;
|
|
953
|
-
target.reasoningTokens += reasoningTokens;
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
return aggregate;
|
|
957
|
-
}
|
|
958
|
-
function readLlmUsageAggregate(stateDbPath, since, until) {
|
|
959
|
-
const events = readEvents({ since, type: LLM_USAGE_EVENT }, { dbPath: stateDbPath }).events.filter((event) => {
|
|
960
|
-
if (until === undefined)
|
|
961
|
-
return true;
|
|
962
|
-
return new Date(event.ts ?? since).getTime() < new Date(until).getTime();
|
|
963
|
-
});
|
|
964
|
-
return summarizeLlmUsage(events);
|
|
965
|
-
}
|
|
966
|
-
function buildWindowMetrics(db, stateDbPath, since, until, now = () => Date.now(), logsDb) {
|
|
967
|
-
const taskRows = queryTaskHistory(db, { since }).filter((row) => {
|
|
968
|
-
const startMs = new Date(row.started_at).getTime();
|
|
969
|
-
const untilMs = new Date(until).getTime();
|
|
970
|
-
return !Number.isFinite(untilMs) || startMs < untilMs;
|
|
971
|
-
});
|
|
972
|
-
const { withLogs: taskRowsWithLogs, backed: existingLogRows } = partitionLogBackedRows(taskRows, logsDb);
|
|
973
|
-
const failedTaskRows = taskRows.filter((row) => row.status === "failed");
|
|
974
|
-
const activeRows = taskRows.filter((row) => row.status === "active");
|
|
975
|
-
const stuckActiveRuns = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS).length;
|
|
976
|
-
const promptRows = taskRows.filter((row) => row.target_kind === "prompt");
|
|
977
|
-
const promptFailures = promptRows.filter((row) => {
|
|
978
|
-
const detail = parseTaskMetadata(row).detail;
|
|
979
|
-
return typeof detail?.reason === "string" && detail.reason.length > 0;
|
|
980
|
-
});
|
|
981
|
-
const logBackingRate = taskRowsWithLogs.length === 0 ? 1 : existingLogRows.length / taskRowsWithLogs.length;
|
|
982
|
-
const taskFailRate = taskRows.length === 0 ? 0 : failedTaskRows.length / taskRows.length;
|
|
983
|
-
const agentFailureRate = promptRows.length === 0 ? 0 : promptFailures.length / promptRows.length;
|
|
984
|
-
const improveInvoked = readEvents({ since, type: "improve_invoked" }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime()).length;
|
|
985
|
-
const improveCompletedEvents = readEvents({ since, type: IMPROVE_COMPLETED_EVENT }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime());
|
|
986
|
-
const improveSkippedEvents = readEvents({ since, type: "improve_skipped" }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime());
|
|
987
|
-
const eventsMetrics = summarizeImproveCompleted(improveCompletedEvents);
|
|
988
|
-
const { metrics: improveSummary, runCount } = summarizeImproveRuns(db, since, until);
|
|
989
|
-
improveSummary.invoked = improveInvoked;
|
|
990
|
-
improveSummary.completed = eventsMetrics.completed;
|
|
991
|
-
const skipSummary = buildImproveSkipSummary(improveSkippedEvents);
|
|
992
|
-
improveSummary.skipped = skipSummary.skipped;
|
|
993
|
-
improveSummary.skipReasons = skipSummary.skipReasons;
|
|
994
|
-
// Preserve the per-phase aggregation computed by summarizeImproveRuns and
|
|
995
|
-
// derive top-level wall times from the same improve-runs window so counts
|
|
996
|
-
// and percentiles stay aligned with per-run reporting.
|
|
997
|
-
const perRunSummaries = buildPerRunSummaries(db, since, until);
|
|
998
|
-
const wallTimes = perRunSummaries.map((run) => run.wallTimeMs).filter((ms) => Number.isFinite(ms) && ms > 0);
|
|
999
|
-
improveSummary.wallTime = computeWallTimeStats(wallTimes, improveSummary.wallTime.byPhase);
|
|
1000
|
-
const metrics = {
|
|
1001
|
-
taskFailRate: roundRate(taskFailRate),
|
|
1002
|
-
agentFailureRate: roundRate(agentFailureRate),
|
|
1003
|
-
stuckActiveRuns,
|
|
1004
|
-
logBackingRate: roundRate(logBackingRate),
|
|
1005
|
-
probeRoundTripMs: null,
|
|
1006
|
-
llmUsage: readLlmUsageAggregate(stateDbPath, since, until),
|
|
1007
|
-
};
|
|
1008
|
-
return { improve: improveSummary, metrics, runs: runCount };
|
|
1009
|
-
}
|
|
1010
43
|
function validateAkmHealthOptions(options) {
|
|
1011
44
|
if (options.groupBy !== undefined && options.groupBy !== "run") {
|
|
1012
45
|
throw new UsageError(`Invalid value for --group-by: ${options.groupBy}. Expected: run`, "INVALID_FLAG_VALUE");
|
|
@@ -1072,6 +105,20 @@ export function akmHealth(options = {}) {
|
|
|
1072
105
|
const taskFailRate = taskRows.length === 0 ? 0 : failedTaskRows.length / taskRows.length;
|
|
1073
106
|
const agentFailureRate = promptRows.length === 0 ? 0 : promptFailures.length / promptRows.length;
|
|
1074
107
|
const semanticStatus = readSemanticStatus();
|
|
108
|
+
// For the embedding-endpoint advisory. Best-effort: an unloadable config
|
|
109
|
+
// leaves both undefined and the check falls back to its generic message.
|
|
110
|
+
let semanticSearchMode;
|
|
111
|
+
let embeddingEndpoint;
|
|
112
|
+
let egressConfigView;
|
|
113
|
+
try {
|
|
114
|
+
const config = loadConfig();
|
|
115
|
+
semanticSearchMode = config.semanticSearchMode;
|
|
116
|
+
embeddingEndpoint = config.embedding?.endpoint;
|
|
117
|
+
egressConfigView = config;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// fall through with undefined
|
|
121
|
+
}
|
|
1075
122
|
const improveInvoked = readEvents({ since, type: "improve_invoked" }, { dbPath: stateDbPath }).events.length;
|
|
1076
123
|
const improveCompletedEvents = readEvents({ since, type: IMPROVE_COMPLETED_EVENT }, { dbPath: stateDbPath }).events;
|
|
1077
124
|
const improveSkippedEvents = readEvents({ since, type: "improve_skipped" }, { dbPath: stateDbPath }).events;
|
|
@@ -1085,6 +132,53 @@ export function akmHealth(options = {}) {
|
|
|
1085
132
|
const perRunSummaries = buildPerRunSummaries(db, since);
|
|
1086
133
|
const wallTimes = perRunSummaries.map((run) => run.wallTimeMs).filter((ms) => Number.isFinite(ms) && ms > 0);
|
|
1087
134
|
improveSummary.wallTime = computeWallTimeStats(wallTimes, improveSummary.wallTime.byPhase);
|
|
135
|
+
improveSummary.calibration = readCalibration(db, since);
|
|
136
|
+
// WS-5: Compute denominator-fixed coverage and per-run degradation metrics
|
|
137
|
+
// for the main health path (not just window-compare mode).
|
|
138
|
+
const until = new Date(now()).toISOString();
|
|
139
|
+
const totalAssetsMain = improveSummary.memorySummary.eligible + improveSummary.memorySummary.derived;
|
|
140
|
+
improveSummary.coverage = computeDenominatorFixedCoverage(db, totalAssetsMain, improveSummary.memorySummary.eligible, since, until);
|
|
141
|
+
const degradationMain = computeDegradationMetrics(db, since, until);
|
|
142
|
+
if (degradationMain) {
|
|
143
|
+
improveSummary.degradation = degradationMain;
|
|
144
|
+
}
|
|
145
|
+
improveSummary.enrichmentMinting = computeEnrichmentMintingRollup(db, since, until);
|
|
146
|
+
advisories.push(...collectImproveAdvisories(db, stateDbPath, since, improveSummary));
|
|
147
|
+
// 08-F1: surface a `stash-git-exposure` advisory when env/secret assets are
|
|
148
|
+
// git-tracked AND a remote is configured (the leak moment). Best-effort.
|
|
149
|
+
// Cheap guard: only shell out to git when the stash has its OWN `.git` (or a
|
|
150
|
+
// test injected a fake seam), so the hot path never spawns for a non-git
|
|
151
|
+
// stash — the common unit-test case. Trade-off: a stash manually pointed at a
|
|
152
|
+
// bare subdirectory of a parent git repo (no `.git` of its own) is not
|
|
153
|
+
// checked. akm-init always creates `.git` at the stash root, so any
|
|
154
|
+
// akm-initialised stash is covered; this only skips hand-pointed nested ones.
|
|
155
|
+
try {
|
|
156
|
+
const exposureStashDir = options.stashDir ?? resolveStashDir();
|
|
157
|
+
if (options.stashExposureGit || fs.existsSync(path.join(exposureStashDir, ".git"))) {
|
|
158
|
+
const stashExposure = collectStashExposureAdvisory(exposureStashDir, options.stashExposureGit);
|
|
159
|
+
if (stashExposure)
|
|
160
|
+
advisories.push(stashExposure);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Non-fatal — a git/probe failure must not abort the health report.
|
|
165
|
+
}
|
|
166
|
+
// 08 surfaces: the remaining read-only advisory group (secret-file-perms,
|
|
167
|
+
// binary-config-skew, orphan-stores, egress-endpoints). Best-effort — a
|
|
168
|
+
// filesystem probe failure must not abort the health report.
|
|
169
|
+
try {
|
|
170
|
+
advisories.push(...collectSurfacesAdvisories({
|
|
171
|
+
stashDir: options.stashDir ?? resolveStashDir(),
|
|
172
|
+
cacheDir: getCacheDir(),
|
|
173
|
+
dataDir: getDataDir(),
|
|
174
|
+
configDir: getConfigDir(),
|
|
175
|
+
configPath: getConfigPath(),
|
|
176
|
+
config: egressConfigView,
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Non-fatal.
|
|
181
|
+
}
|
|
1088
182
|
let sessionLogEntries = [];
|
|
1089
183
|
try {
|
|
1090
184
|
const sinceDays = Math.max(0, Math.ceil((now() - new Date(since).getTime()) / (24 * 60 * 60 * 1000)));
|
|
@@ -1109,11 +203,14 @@ export function akmHealth(options = {}) {
|
|
|
1109
203
|
missingTables,
|
|
1110
204
|
probe,
|
|
1111
205
|
taskRowCount: taskRows.length,
|
|
206
|
+
taskFailRate,
|
|
1112
207
|
taskRowsWithLogsCount: taskRowsWithLogs.length,
|
|
1113
208
|
existingLogRowsCount: existingLogRows.length,
|
|
1114
209
|
logBackingRate,
|
|
1115
210
|
stuckActiveRuns,
|
|
1116
211
|
semanticStatus,
|
|
212
|
+
semanticSearchMode,
|
|
213
|
+
embeddingEndpoint,
|
|
1117
214
|
sessionLogEntries,
|
|
1118
215
|
sessionExtraction: improveSummary.sessionExtraction,
|
|
1119
216
|
autoAccept: improveSummary.autoAccept,
|
|
@@ -1213,102 +310,5 @@ export function akmHealth(options = {}) {
|
|
|
1213
310
|
}
|
|
1214
311
|
}
|
|
1215
312
|
}
|
|
1216
|
-
//
|
|
1217
|
-
|
|
1218
|
-
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
1219
|
-
}
|
|
1220
|
-
function renderTable(headers, rows) {
|
|
1221
|
-
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
1222
|
-
const lines = [];
|
|
1223
|
-
lines.push(headers.map((h, i) => padRight(h, widths[i] ?? 0)).join(" "));
|
|
1224
|
-
for (const row of rows) {
|
|
1225
|
-
lines.push(row.map((cell, i) => padRight(cell ?? "", widths[i] ?? 0)).join(" "));
|
|
1226
|
-
}
|
|
1227
|
-
return lines.join("\n");
|
|
1228
|
-
}
|
|
1229
|
-
/**
|
|
1230
|
-
* Render `--detail per-run` rows as a TSV-ish aligned table. The column
|
|
1231
|
-
* shape was originally inherited from the retired
|
|
1232
|
-
* `scripts/improve-stats/runs-detail` bash helper; keep the same shape
|
|
1233
|
-
* so operator muscle memory carries over.
|
|
1234
|
-
*
|
|
1235
|
-
* Columns: ts | ok | actions | refl_ok/fail/cd/skip |
|
|
1236
|
-
* distill_q/llm-fail/qrej/cfg/skip | cons_proc/promo/merge/del |
|
|
1237
|
-
* mem_cons/written/skip | graph_f/e/r | orphans | lint_f/fl
|
|
1238
|
-
*/
|
|
1239
|
-
export function renderRunsDetailMd(runs) {
|
|
1240
|
-
const headers = [
|
|
1241
|
-
"ts",
|
|
1242
|
-
"ok",
|
|
1243
|
-
"actions",
|
|
1244
|
-
"refl_ok/fail/cd/skip",
|
|
1245
|
-
"distill_q/llm-fail/qrej/cfg/skip",
|
|
1246
|
-
"cons_proc/promo/merge/del",
|
|
1247
|
-
"mem_cons/written/skip",
|
|
1248
|
-
"graph_f/e/r",
|
|
1249
|
-
"orphans",
|
|
1250
|
-
"lint_f/fl",
|
|
1251
|
-
];
|
|
1252
|
-
const rows = runs.map((r) => {
|
|
1253
|
-
const totalActions = r.actions.reflect.ok +
|
|
1254
|
-
r.actions.reflect.failed +
|
|
1255
|
-
r.actions.reflect.cooldown +
|
|
1256
|
-
r.actions.reflect.skipped +
|
|
1257
|
-
r.actions.distill.queued +
|
|
1258
|
-
r.actions.distill.llmFailed +
|
|
1259
|
-
r.actions.distill.qualityRejected +
|
|
1260
|
-
r.actions.distill.configDisabled +
|
|
1261
|
-
r.actions.distill.skipped +
|
|
1262
|
-
r.actions.memoryPrune +
|
|
1263
|
-
r.actions.memoryInference +
|
|
1264
|
-
r.actions.graphExtraction +
|
|
1265
|
-
r.actions.error;
|
|
1266
|
-
return [
|
|
1267
|
-
r.startedAt,
|
|
1268
|
-
String(r.ok),
|
|
1269
|
-
String(totalActions),
|
|
1270
|
-
`${r.actions.reflect.ok}/${r.actions.reflect.failed}/${r.actions.reflect.cooldown}/${r.actions.reflect.skipped}`,
|
|
1271
|
-
`${r.actions.distill.queued}/${r.actions.distill.llmFailed}/${r.actions.distill.qualityRejected}/${r.actions.distill.configDisabled}/${r.actions.distill.skipped}`,
|
|
1272
|
-
`${r.consolidation.processed}/${r.consolidation.promoted}/${r.consolidation.merged}/${r.consolidation.deleted}`,
|
|
1273
|
-
`${r.memoryInference.considered}/${r.memoryInference.written}/${r.memoryInference.skippedNoFacts}`,
|
|
1274
|
-
`${r.graphExtraction.extractedFiles}/${r.graphExtraction.entities}/${r.graphExtraction.relations}`,
|
|
1275
|
-
String(r.orphansPurged),
|
|
1276
|
-
`${r.lintFixed}/${r.lintFlagged}`,
|
|
1277
|
-
];
|
|
1278
|
-
});
|
|
1279
|
-
return renderTable(headers, rows);
|
|
1280
|
-
}
|
|
1281
|
-
/**
|
|
1282
|
-
* Render a window-compare comparison as a side-by-side metric table with a
|
|
1283
|
-
* delta column. Bad-direction deltas (e.g. +pct on failed counts) get a `!`
|
|
1284
|
-
* marker prefix.
|
|
1285
|
-
*/
|
|
1286
|
-
export function renderWindowCompareMd(windows, deltas) {
|
|
1287
|
-
if (windows.length === 0)
|
|
1288
|
-
return "";
|
|
1289
|
-
const headers = ["metric", ...windows.map((w) => w.name), "delta"];
|
|
1290
|
-
const badIfPositive = new Set([
|
|
1291
|
-
"improve.actions.reflect.failed",
|
|
1292
|
-
"improve.actions.distill.llmFailed",
|
|
1293
|
-
"improve.graphExtraction.failures",
|
|
1294
|
-
"improve.wallTime.medianMs",
|
|
1295
|
-
"improve.wallTime.p95Ms",
|
|
1296
|
-
"improve.memoryInference.skippedNoFacts",
|
|
1297
|
-
]);
|
|
1298
|
-
const rows = [];
|
|
1299
|
-
for (const path of INTERESTING_DELTA_PATHS) {
|
|
1300
|
-
const values = windows.map((w) => String(readNumericPath(w, path)));
|
|
1301
|
-
const delta = deltas?.[path];
|
|
1302
|
-
let deltaStr = "—";
|
|
1303
|
-
if (delta) {
|
|
1304
|
-
const pct = delta.pctChange;
|
|
1305
|
-
const num = typeof pct === "number" ? pct : pct;
|
|
1306
|
-
const sign = typeof num === "number" && num > 0 ? "+" : "";
|
|
1307
|
-
const formatted = typeof num === "number" ? `${sign}${num}%` : String(num);
|
|
1308
|
-
const marker = badIfPositive.has(path) && typeof num === "number" && num > 0 ? "!" : "";
|
|
1309
|
-
deltaStr = marker + formatted;
|
|
1310
|
-
}
|
|
1311
|
-
rows.push([path, ...values, deltaStr]);
|
|
1312
|
-
}
|
|
1313
|
-
return renderTable(headers, rows);
|
|
1314
|
-
}
|
|
313
|
+
// Markdown renderers (renderRunsDetailMd / renderWindowCompareMd) live in
|
|
314
|
+
// health/md-report.ts, mirroring the HTML extraction in health/html-report.ts.
|