akm-cli 0.8.0-rc2 → 0.8.1
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/{.github/CHANGELOG.md → CHANGELOG.md} +238 -3
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/assets/help/help-accept.md +12 -0
- package/dist/assets/help/help-improve.md +81 -0
- package/dist/{commands → assets}/help/help-proposals.md +7 -4
- package/dist/assets/help/help-reject.md +11 -0
- package/dist/{output → assets/hints}/cli-hints-full.md +60 -32
- package/dist/{output → assets/hints}/cli-hints-short.md +10 -7
- package/dist/assets/profiles/default.json +15 -0
- package/dist/assets/profiles/graph-refresh.json +13 -0
- package/dist/assets/profiles/memory-focus.json +12 -0
- package/dist/assets/profiles/quick.json +15 -0
- package/dist/assets/profiles/thorough.json +15 -0
- package/dist/assets/prompts/extract-session.md +80 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +10 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +93 -3
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2141 -1268
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +20 -12
- package/dist/commands/agent-support.js +11 -5
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +129 -517
- package/dist/commands/consolidate.js +1557 -147
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +5 -3
- package/dist/commands/distill.js +906 -100
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +3 -0
- package/dist/commands/events.js +3 -0
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +217 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +260 -5
- package/dist/commands/health.js +1042 -55
- package/dist/commands/history.js +51 -16
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +236 -0
- package/dist/commands/improve-profiles.js +138 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1736 -346
- package/dist/commands/info.js +26 -28
- package/dist/commands/init.js +49 -1
- package/dist/commands/installed-stashes.js +6 -23
- package/dist/commands/knowledge.js +3 -0
- package/dist/commands/lint/agent-linter.js +3 -0
- package/dist/commands/lint/base-linter.js +199 -5
- package/dist/commands/lint/command-linter.js +3 -0
- package/dist/commands/lint/default-linter.js +3 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +92 -3
- package/dist/commands/lint/knowledge-linter.js +3 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +3 -0
- package/dist/commands/lint/registry.js +3 -0
- package/dist/commands/lint/skill-linter.js +3 -0
- package/dist/commands/lint/task-linter.js +15 -12
- package/dist/commands/lint/types.js +3 -0
- package/dist/commands/lint/workflow-linter.js +3 -0
- package/dist/commands/lint.js +3 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal-drain-policies.js +128 -0
- package/dist/commands/proposal-drain.js +477 -0
- package/dist/commands/proposal.js +60 -6
- package/dist/commands/propose.js +24 -19
- package/dist/commands/reflect.js +1004 -94
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +3 -0
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +15 -6
- package/dist/commands/schema-repair.js +88 -15
- package/dist/commands/search.js +99 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +32 -13
- package/dist/commands/source-add.js +7 -35
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +3 -0
- package/dist/commands/tasks.js +161 -95
- package/dist/commands/url-checker.js +3 -0
- package/dist/core/action-contributors.js +3 -0
- package/dist/core/asset-ref.js +13 -2
- package/dist/core/asset-registry.js +9 -2
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +61 -5
- package/dist/core/common.js +93 -5
- package/dist/core/concurrent.js +3 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +558 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +366 -1077
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +31 -25
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -10
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +3 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +142 -14
- package/dist/core/parse.js +3 -0
- package/dist/core/paths.js +218 -50
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +11 -3
- package/dist/core/proposals.js +464 -5
- package/dist/core/state-db.js +349 -56
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +3 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +7 -2
- package/dist/core/write-source.js +12 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +136 -28
- package/dist/indexer/db.js +661 -166
- package/dist/indexer/ensure-index.js +3 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +162 -40
- package/dist/indexer/graph-db.js +241 -51
- package/dist/indexer/graph-dedup.js +3 -7
- package/dist/indexer/graph-extraction.js +242 -149
- package/dist/indexer/index-context.js +3 -9
- package/dist/indexer/indexer.js +86 -16
- package/dist/indexer/llm-cache.js +24 -19
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +184 -11
- package/dist/indexer/memory-inference.js +94 -50
- package/dist/indexer/metadata-contributors.js +3 -0
- package/dist/indexer/metadata.js +110 -50
- package/dist/indexer/path-resolver.js +3 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +134 -7
- package/dist/indexer/ranking.js +8 -1
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +91 -2
- package/dist/indexer/search-source.js +20 -1
- package/dist/indexer/semantic-status.js +4 -1
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +3 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +121 -401
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +6 -14
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +3 -0
- package/dist/integrations/agent/prompts.js +137 -8
- package/dist/integrations/agent/runner.js +208 -0
- package/dist/integrations/agent/sdk-runner.js +8 -2
- package/dist/integrations/agent/spawn.js +54 -14
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +22 -51
- package/dist/integrations/session-logs/index.js +4 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +226 -0
- package/dist/integrations/session-logs/providers/opencode.js +231 -25
- package/dist/integrations/session-logs/types.js +3 -0
- package/dist/llm/call-ai.js +14 -26
- package/dist/llm/client.js +16 -2
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +92 -56
- package/dist/llm/graph-extract.js +402 -31
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +30 -2
- package/dist/llm/metadata-enhance.js +3 -7
- package/dist/output/cli-hints.js +7 -4
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +170 -194
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +105 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -549
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1059 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +12 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1329
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +11 -2
- package/dist/registry/providers/static-index.js +10 -1
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17767 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +306 -67
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +3 -11
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +171 -21
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +3 -0
- package/dist/tasks/backends/cron.js +3 -0
- package/dist/tasks/backends/exec-utils.js +3 -0
- package/dist/tasks/backends/index.js +3 -11
- package/dist/tasks/backends/launchd.js +4 -1
- package/dist/tasks/backends/schtasks.js +4 -1
- package/dist/tasks/parser.js +51 -38
- package/dist/tasks/resolveAkmBin.js +3 -0
- package/dist/tasks/runner.js +35 -9
- package/dist/tasks/schedule.js +20 -1
- package/dist/tasks/schema.js +5 -3
- package/dist/tasks/validator.js +6 -3
- package/dist/version.js +3 -0
- package/dist/wiki/wiki-templates.js +6 -3
- package/dist/wiki/wiki.js +4 -1
- package/dist/workflows/authoring.js +4 -1
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +3 -0
- package/dist/workflows/runs.js +18 -1
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +5 -9
- package/docs/README.md +7 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +57 -5
- package/docs/migration/v0.7-to-v0.8.md +1378 -0
- package/package.json +28 -11
- package/.github/LICENSE +0 -374
- package/dist/commands/help/help-accept.md +0 -9
- package/dist/commands/help/help-improve.md +0 -53
- package/dist/commands/help/help-reject.md +0 -8
- package/dist/commands/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -310
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
- package/dist/llm/prompts/graph-extract-user-prompt.md +0 -12
- /package/dist/{tasks → assets}/backends/launchd-template.xml +0 -0
- /package/dist/{tasks → assets}/backends/schtasks-template.xml +0 -0
- /package/dist/{commands → assets}/help/help-propose.md +0 -0
- /package/dist/{wiki → assets/wiki}/index-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/ingest-workflow-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/log-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/schema-template.md +0 -0
- /package/dist/{workflows → assets/workflows}/workflow-template.md +0 -0
package/dist/commands/health.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import { spawnSync } from "node:child_process";
|
|
2
5
|
import fs from "node:fs";
|
|
3
6
|
import { loadConfig } from "../core/config";
|
|
@@ -48,19 +51,28 @@ function createUnknownImproveMetrics() {
|
|
|
48
51
|
skipped: 0,
|
|
49
52
|
skipReasons: {},
|
|
50
53
|
plannedRefs: 0,
|
|
54
|
+
profileFilteredRefs: 0,
|
|
51
55
|
actions: {
|
|
52
|
-
reflect: 0,
|
|
53
|
-
distill:
|
|
54
|
-
|
|
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
|
+
},
|
|
55
69
|
memoryPrune: 0,
|
|
56
70
|
memoryInference: 0,
|
|
57
71
|
graphExtraction: 0,
|
|
58
72
|
error: 0,
|
|
59
73
|
},
|
|
60
|
-
|
|
61
|
-
feedbackRatioUsed: false,
|
|
74
|
+
reflectsWithErrorContext: 0,
|
|
62
75
|
coverageGapCount: 0,
|
|
63
|
-
executionLogCandidateCount: 0,
|
|
64
76
|
evalCasesWritten: 0,
|
|
65
77
|
deadUrlCount: 0,
|
|
66
78
|
memorySummary: { eligible: 0, derived: 0 },
|
|
@@ -72,9 +84,72 @@ function createUnknownImproveMetrics() {
|
|
|
72
84
|
archived: 0,
|
|
73
85
|
warnings: 0,
|
|
74
86
|
},
|
|
75
|
-
consolidation: {
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
consolidation: {
|
|
88
|
+
ran: false,
|
|
89
|
+
processed: 0,
|
|
90
|
+
promoted: 0,
|
|
91
|
+
merged: 0,
|
|
92
|
+
deleted: 0,
|
|
93
|
+
contradicted: 0,
|
|
94
|
+
judgedNoAction: 0,
|
|
95
|
+
mergedSecondaries: 0,
|
|
96
|
+
failedChunkMemories: 0,
|
|
97
|
+
skipReasons: {},
|
|
98
|
+
failedChunks: 0,
|
|
99
|
+
totalChunks: 0,
|
|
100
|
+
durationMs: 0,
|
|
101
|
+
},
|
|
102
|
+
memoryInference: {
|
|
103
|
+
ran: false,
|
|
104
|
+
considered: 0,
|
|
105
|
+
cacheHits: 0,
|
|
106
|
+
freshAttempts: 0,
|
|
107
|
+
splitParents: 0,
|
|
108
|
+
written: 0,
|
|
109
|
+
skippedNoFacts: 0,
|
|
110
|
+
skippedChildExists: 0,
|
|
111
|
+
skippedAborted: 0,
|
|
112
|
+
unaccounted: 0,
|
|
113
|
+
yieldEligibleRuns: 0,
|
|
114
|
+
yieldEligibleConsidered: 0,
|
|
115
|
+
yieldEligibleWritten: 0,
|
|
116
|
+
yieldRate: 0,
|
|
117
|
+
durationMs: 0,
|
|
118
|
+
writes: 0,
|
|
119
|
+
},
|
|
120
|
+
graphExtraction: {
|
|
121
|
+
ran: false,
|
|
122
|
+
extractedFiles: 0,
|
|
123
|
+
entities: 0,
|
|
124
|
+
relations: 0,
|
|
125
|
+
cacheHits: 0,
|
|
126
|
+
cacheMisses: 0,
|
|
127
|
+
cacheHitRate: 0,
|
|
128
|
+
truncations: 0,
|
|
129
|
+
failures: 0,
|
|
130
|
+
durationMs: 0,
|
|
131
|
+
},
|
|
132
|
+
sessionExtraction: {
|
|
133
|
+
ran: false,
|
|
134
|
+
sessionsScanned: 0,
|
|
135
|
+
sessionsExtracted: 0,
|
|
136
|
+
sessionsSkipped: 0,
|
|
137
|
+
proposalsCreated: 0,
|
|
138
|
+
warnings: 0,
|
|
139
|
+
durationMs: 0,
|
|
140
|
+
},
|
|
141
|
+
wallTime: {
|
|
142
|
+
count: 0,
|
|
143
|
+
medianMs: 0,
|
|
144
|
+
p95Ms: 0,
|
|
145
|
+
minMs: 0,
|
|
146
|
+
maxMs: 0,
|
|
147
|
+
byPhase: {
|
|
148
|
+
consolidation: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
|
|
149
|
+
memoryInference: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
|
|
150
|
+
graphExtraction: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
78
153
|
};
|
|
79
154
|
}
|
|
80
155
|
function toFiniteNumber(value) {
|
|
@@ -87,46 +162,579 @@ function toFiniteNumber(value) {
|
|
|
87
162
|
}
|
|
88
163
|
return 0;
|
|
89
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Event-derived metrics. Only `completed` and skipReasons/invoked are sourced
|
|
167
|
+
* from events in v2 — the richer fields come from {@link summarizeImproveRuns}.
|
|
168
|
+
* The function still receives `improve_completed` events so that the completed
|
|
169
|
+
* count reflects the canonical event stream (it lines up 1:1 with improve_runs
|
|
170
|
+
* rows in practice, but the events table remains the system-of-record for the
|
|
171
|
+
* existence of a run).
|
|
172
|
+
*/
|
|
90
173
|
function summarizeImproveCompleted(events) {
|
|
91
174
|
const metrics = createUnknownImproveMetrics();
|
|
92
175
|
metrics.completed = events.length;
|
|
93
|
-
for (const event of events) {
|
|
94
|
-
const meta = event.metadata ?? {};
|
|
95
|
-
metrics.plannedRefs += toFiniteNumber(meta.plannedRefs);
|
|
96
|
-
metrics.actions.reflect += toFiniteNumber(meta.reflectActions);
|
|
97
|
-
metrics.actions.distill += toFiniteNumber(meta.distillActions);
|
|
98
|
-
metrics.actions.distillSkipped += toFiniteNumber(meta.distillSkippedActions);
|
|
99
|
-
metrics.actions.memoryPrune += toFiniteNumber(meta.memoryPruneActions);
|
|
100
|
-
metrics.actions.memoryInference += toFiniteNumber(meta.memoryInferenceActions);
|
|
101
|
-
metrics.actions.graphExtraction += toFiniteNumber(meta.graphExtractionActions);
|
|
102
|
-
metrics.actions.error += toFiniteNumber(meta.errorActions);
|
|
103
|
-
metrics.crossStepErrorsInjected += toFiniteNumber(meta.crossStepErrorsInjected);
|
|
104
|
-
metrics.coverageGapCount += toFiniteNumber(meta.coverageGapCount);
|
|
105
|
-
metrics.executionLogCandidateCount += toFiniteNumber(meta.executionLogCandidateCount);
|
|
106
|
-
metrics.evalCasesWritten += toFiniteNumber(meta.evalCasesWritten);
|
|
107
|
-
metrics.deadUrlCount += toFiniteNumber(meta.deadUrlCount);
|
|
108
|
-
metrics.memorySummary.eligible += toFiniteNumber(meta.memoryEligible);
|
|
109
|
-
metrics.memorySummary.derived += toFiniteNumber(meta.memoryDerived);
|
|
110
|
-
metrics.memoryCleanup.pruneCandidates += toFiniteNumber(meta.memoryCleanupPruneCandidates);
|
|
111
|
-
metrics.memoryCleanup.contradictionCandidates += toFiniteNumber(meta.memoryCleanupContradictionCandidates);
|
|
112
|
-
metrics.memoryCleanup.beliefStateTransitions += toFiniteNumber(meta.memoryCleanupBeliefStateTransitions);
|
|
113
|
-
metrics.memoryCleanup.consolidationCandidates += toFiniteNumber(meta.memoryCleanupConsolidationCandidates);
|
|
114
|
-
metrics.memoryCleanup.archived += toFiniteNumber(meta.memoryCleanupArchived);
|
|
115
|
-
metrics.memoryCleanup.warnings += toFiniteNumber(meta.memoryCleanupWarnings);
|
|
116
|
-
metrics.consolidation.processed += toFiniteNumber(meta.consolidationProcessed);
|
|
117
|
-
metrics.consolidation.durationMs += toFiniteNumber(meta.consolidationDurationMs);
|
|
118
|
-
metrics.memoryInference.writes += toFiniteNumber(meta.memoryInferenceWrites);
|
|
119
|
-
metrics.memoryInference.durationMs += toFiniteNumber(meta.memoryInferenceDurationMs);
|
|
120
|
-
metrics.graphExtraction.extractedFiles += toFiniteNumber(meta.graphExtractionExtractedFiles);
|
|
121
|
-
metrics.graphExtraction.durationMs += toFiniteNumber(meta.graphExtractionDurationMs);
|
|
122
|
-
if (meta.feedbackRatioUsed === true)
|
|
123
|
-
metrics.feedbackRatioUsed = true;
|
|
124
|
-
}
|
|
125
|
-
metrics.consolidation.ran = metrics.consolidation.processed > 0 || metrics.consolidation.durationMs > 0;
|
|
126
|
-
metrics.memoryInference.ran = metrics.memoryInference.writes > 0 || metrics.memoryInference.durationMs > 0;
|
|
127
|
-
metrics.graphExtraction.ran = metrics.graphExtraction.extractedFiles > 0 || metrics.graphExtraction.durationMs > 0;
|
|
128
176
|
return metrics;
|
|
129
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Project a single `improve_runs.result_json` envelope into an accumulator-shaped
|
|
180
|
+
* ImproveHealthMetrics. The aggregator merges these per-row metrics into one
|
|
181
|
+
* window-level metric.
|
|
182
|
+
*/
|
|
183
|
+
function projectRunMetrics(result) {
|
|
184
|
+
const metrics = createUnknownImproveMetrics();
|
|
185
|
+
// plannedRefs (array of {ref, reason})
|
|
186
|
+
const plannedRefs = result.plannedRefs;
|
|
187
|
+
if (Array.isArray(plannedRefs))
|
|
188
|
+
metrics.plannedRefs += plannedRefs.length;
|
|
189
|
+
// profileFilteredRefs (array of {ref, reason}) — 2026-05-27: pre-filter
|
|
190
|
+
// bucket from `collectEligibleRefs` so the metric reflects work the
|
|
191
|
+
// planner dropped before signal-delta / per-pass dispatch.
|
|
192
|
+
const profileFilteredRefs = result.profileFilteredRefs;
|
|
193
|
+
if (Array.isArray(profileFilteredRefs))
|
|
194
|
+
metrics.profileFilteredRefs += profileFilteredRefs.length;
|
|
195
|
+
// actions: split reflect / distill by outcome, count others.
|
|
196
|
+
const actions = result.actions;
|
|
197
|
+
if (Array.isArray(actions)) {
|
|
198
|
+
for (const action of actions) {
|
|
199
|
+
const mode = typeof action.mode === "string" ? action.mode : "";
|
|
200
|
+
switch (mode) {
|
|
201
|
+
case "reflect":
|
|
202
|
+
metrics.actions.reflect.ok += 1;
|
|
203
|
+
break;
|
|
204
|
+
case "reflect-failed":
|
|
205
|
+
metrics.actions.reflect.failed += 1;
|
|
206
|
+
break;
|
|
207
|
+
case "reflect-cooldown":
|
|
208
|
+
metrics.actions.reflect.cooldown += 1;
|
|
209
|
+
break;
|
|
210
|
+
case "reflect-skipped": {
|
|
211
|
+
metrics.actions.reflect.skipped += 1;
|
|
212
|
+
const r = action.result;
|
|
213
|
+
const reason = typeof r?.reason === "string" && r.reason.trim() ? r.reason : "unknown";
|
|
214
|
+
metrics.actions.reflect.skippedByReason[reason] = (metrics.actions.reflect.skippedByReason[reason] ?? 0) + 1;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case "reflect-guard-rejected":
|
|
218
|
+
metrics.actions.reflect.guardRejected += 1;
|
|
219
|
+
break;
|
|
220
|
+
case "distill": {
|
|
221
|
+
const r = action.result;
|
|
222
|
+
const outcome = typeof r?.outcome === "string" ? r.outcome : "";
|
|
223
|
+
switch (outcome) {
|
|
224
|
+
case "queued":
|
|
225
|
+
metrics.actions.distill.queued += 1;
|
|
226
|
+
break;
|
|
227
|
+
case "llm_failed":
|
|
228
|
+
metrics.actions.distill.llmFailed += 1;
|
|
229
|
+
break;
|
|
230
|
+
case "quality_rejected":
|
|
231
|
+
case "review_needed":
|
|
232
|
+
metrics.actions.distill.qualityRejected += 1;
|
|
233
|
+
metrics.actions.distill.judgeRejected += 1;
|
|
234
|
+
break;
|
|
235
|
+
case "validation_failed":
|
|
236
|
+
metrics.actions.distill.qualityRejected += 1;
|
|
237
|
+
metrics.actions.distill.validatorRejected += 1;
|
|
238
|
+
break;
|
|
239
|
+
case "config_disabled":
|
|
240
|
+
metrics.actions.distill.configDisabled += 1;
|
|
241
|
+
break;
|
|
242
|
+
case "skipped": {
|
|
243
|
+
// Previously dropped on the floor. The four sub-paths that emit
|
|
244
|
+
// `outcome: "skipped"` (see distill.ts:893, 1024, 1120, 1576):
|
|
245
|
+
// - recursive_lesson_input (type guard refused a lesson input)
|
|
246
|
+
// - conflict_noop (LLM resolved destination conflict as NOOP)
|
|
247
|
+
// - proposal-skipped cooldown / dedup at persistence
|
|
248
|
+
// 465 events/7d in the user's live stack. The result message
|
|
249
|
+
// typically encodes the reason; we also accept an explicit
|
|
250
|
+
// `skipReason` field when downstream code sets it.
|
|
251
|
+
metrics.actions.distill.deferred += 1;
|
|
252
|
+
const explicitReason = typeof r?.skipReason === "string" ? r.skipReason : undefined;
|
|
253
|
+
const msg = typeof r?.message === "string" ? r.message : "";
|
|
254
|
+
let reason = explicitReason ?? "unknown";
|
|
255
|
+
if (!explicitReason) {
|
|
256
|
+
if (/lesson inputs/i.test(msg))
|
|
257
|
+
reason = "recursive_lesson_input";
|
|
258
|
+
else if (/NOOP/.test(msg))
|
|
259
|
+
reason = "conflict_noop";
|
|
260
|
+
else if (/cooldown/i.test(msg))
|
|
261
|
+
reason = "proposal_cooldown";
|
|
262
|
+
else if (/content[_ ]?hash/i.test(msg))
|
|
263
|
+
reason = "content_hash_match";
|
|
264
|
+
}
|
|
265
|
+
metrics.actions.distill.deferredByReason[reason] =
|
|
266
|
+
(metrics.actions.distill.deferredByReason[reason] ?? 0) + 1;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
default:
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
case "distill-skipped": {
|
|
275
|
+
metrics.actions.distill.skipped += 1;
|
|
276
|
+
const r = action.result;
|
|
277
|
+
const reason = typeof r?.reason === "string" && r.reason.trim() ? r.reason : "unknown";
|
|
278
|
+
metrics.actions.distill.skippedByReason[reason] = (metrics.actions.distill.skippedByReason[reason] ?? 0) + 1;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "memory-prune":
|
|
282
|
+
metrics.actions.memoryPrune += 1;
|
|
283
|
+
break;
|
|
284
|
+
case "memory-inference":
|
|
285
|
+
metrics.actions.memoryInference += 1;
|
|
286
|
+
break;
|
|
287
|
+
case "graph-extraction":
|
|
288
|
+
metrics.actions.graphExtraction += 1;
|
|
289
|
+
break;
|
|
290
|
+
case "error":
|
|
291
|
+
metrics.actions.error += 1;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
metrics.reflectsWithErrorContext += toFiniteNumber(result.reflectsWithErrorContext);
|
|
297
|
+
if (Array.isArray(result.coverageGaps))
|
|
298
|
+
metrics.coverageGapCount += result.coverageGaps.length;
|
|
299
|
+
metrics.evalCasesWritten += toFiniteNumber(result.evalCasesWritten);
|
|
300
|
+
if (Array.isArray(result.deadUrls))
|
|
301
|
+
metrics.deadUrlCount += result.deadUrls.length;
|
|
302
|
+
const memorySummary = result.memorySummary;
|
|
303
|
+
if (memorySummary) {
|
|
304
|
+
metrics.memorySummary.eligible += toFiniteNumber(memorySummary.eligible);
|
|
305
|
+
metrics.memorySummary.derived += toFiniteNumber(memorySummary.derived);
|
|
306
|
+
}
|
|
307
|
+
const memoryCleanup = result.memoryCleanup;
|
|
308
|
+
if (memoryCleanup) {
|
|
309
|
+
if (Array.isArray(memoryCleanup.pruneCandidates))
|
|
310
|
+
metrics.memoryCleanup.pruneCandidates += memoryCleanup.pruneCandidates.length;
|
|
311
|
+
if (Array.isArray(memoryCleanup.contradictionCandidates))
|
|
312
|
+
metrics.memoryCleanup.contradictionCandidates += memoryCleanup.contradictionCandidates.length;
|
|
313
|
+
if (Array.isArray(memoryCleanup.beliefStateTransitions))
|
|
314
|
+
metrics.memoryCleanup.beliefStateTransitions += memoryCleanup.beliefStateTransitions.length;
|
|
315
|
+
if (Array.isArray(memoryCleanup.consolidationCandidates))
|
|
316
|
+
metrics.memoryCleanup.consolidationCandidates += memoryCleanup.consolidationCandidates.length;
|
|
317
|
+
if (Array.isArray(memoryCleanup.archived))
|
|
318
|
+
metrics.memoryCleanup.archived += memoryCleanup.archived.length;
|
|
319
|
+
if (Array.isArray(memoryCleanup.warnings))
|
|
320
|
+
metrics.memoryCleanup.warnings += memoryCleanup.warnings.length;
|
|
321
|
+
}
|
|
322
|
+
const consolidation = result.consolidation;
|
|
323
|
+
if (consolidation) {
|
|
324
|
+
metrics.consolidation.processed += toFiniteNumber(consolidation.processed);
|
|
325
|
+
metrics.consolidation.merged += toFiniteNumber(consolidation.merged);
|
|
326
|
+
metrics.consolidation.deleted += toFiniteNumber(consolidation.deleted);
|
|
327
|
+
metrics.consolidation.contradicted += toFiniteNumber(consolidation.contradicted);
|
|
328
|
+
if (Array.isArray(consolidation.promoted))
|
|
329
|
+
metrics.consolidation.promoted += consolidation.promoted.length;
|
|
330
|
+
metrics.consolidation.failedChunks += toFiniteNumber(consolidation.failedChunks);
|
|
331
|
+
metrics.consolidation.totalChunks += toFiniteNumber(consolidation.totalChunks);
|
|
332
|
+
metrics.consolidation.durationMs += toFiniteNumber(consolidation.durationMs);
|
|
333
|
+
metrics.consolidation.judgedNoAction += toFiniteNumber(consolidation.judgedNoAction);
|
|
334
|
+
metrics.consolidation.mergedSecondaries += toFiniteNumber(consolidation.mergedSecondaries);
|
|
335
|
+
metrics.consolidation.failedChunkMemories += toFiniteNumber(consolidation.failedChunkMemories);
|
|
336
|
+
// Structured emitter (new on this branch): consolidate.ts now pushes
|
|
337
|
+
// `{op, ref, reason}` entries to `skipReasons` for every deterministic
|
|
338
|
+
// post-LLM rejection. Pre-fix envelopes have neither field, so be
|
|
339
|
+
// defensive.
|
|
340
|
+
const skipReasons = consolidation.skipReasons;
|
|
341
|
+
if (Array.isArray(skipReasons)) {
|
|
342
|
+
for (const entry of skipReasons) {
|
|
343
|
+
if (!entry || typeof entry !== "object")
|
|
344
|
+
continue;
|
|
345
|
+
const reason = entry.reason;
|
|
346
|
+
if (typeof reason !== "string" || !reason.trim())
|
|
347
|
+
continue;
|
|
348
|
+
metrics.consolidation.skipReasons[reason] = (metrics.consolidation.skipReasons[reason] ?? 0) + 1;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const memoryInference = result.memoryInference;
|
|
353
|
+
if (memoryInference) {
|
|
354
|
+
const considered = toFiniteNumber(memoryInference.considered);
|
|
355
|
+
const writtenFacts = toFiniteNumber(memoryInference.writtenFacts);
|
|
356
|
+
metrics.memoryInference.considered += considered;
|
|
357
|
+
metrics.memoryInference.cacheHits += toFiniteNumber(memoryInference.cacheHits);
|
|
358
|
+
metrics.memoryInference.splitParents += toFiniteNumber(memoryInference.splitParents);
|
|
359
|
+
metrics.memoryInference.written += writtenFacts;
|
|
360
|
+
metrics.memoryInference.skippedNoFacts += toFiniteNumber(memoryInference.skippedNoFacts);
|
|
361
|
+
metrics.memoryInference.skippedChildExists += toFiniteNumber(memoryInference.skippedChildExists);
|
|
362
|
+
metrics.memoryInference.skippedAborted += toFiniteNumber(memoryInference.skippedAborted);
|
|
363
|
+
metrics.memoryInference.unaccounted += toFiniteNumber(memoryInference.unaccounted);
|
|
364
|
+
// Yield-rate gating: pre-cache-feature envelopes lack the `cacheHits`
|
|
365
|
+
// field entirely. Treating their `considered` as freshAttempts (since
|
|
366
|
+
// cacheHits=0) is mathematically tempting but operationally wrong —
|
|
367
|
+
// historical runs with the legacy schema have no cache instrumentation
|
|
368
|
+
// and the SUM dragged the reported rate to ~14% in local data. Only
|
|
369
|
+
// contribute to the yield aggregate when the envelope actually carries
|
|
370
|
+
// the field. See investigation 2026-05-26.
|
|
371
|
+
if (Object.hasOwn(memoryInference, "cacheHits")) {
|
|
372
|
+
metrics.memoryInference.yieldEligibleRuns += 1;
|
|
373
|
+
metrics.memoryInference.yieldEligibleConsidered += considered;
|
|
374
|
+
metrics.memoryInference.yieldEligibleWritten += writtenFacts;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
metrics.memoryInference.durationMs += toFiniteNumber(result.memoryInferenceDurationMs);
|
|
378
|
+
const graphExtraction = result.graphExtraction;
|
|
379
|
+
if (graphExtraction) {
|
|
380
|
+
const quality = graphExtraction.quality;
|
|
381
|
+
if (quality)
|
|
382
|
+
metrics.graphExtraction.extractedFiles += toFiniteNumber(quality.extractedFiles);
|
|
383
|
+
metrics.graphExtraction.entities += toFiniteNumber(graphExtraction.totalEntities);
|
|
384
|
+
metrics.graphExtraction.relations += toFiniteNumber(graphExtraction.totalRelations);
|
|
385
|
+
const telemetry = graphExtraction.telemetry;
|
|
386
|
+
if (telemetry) {
|
|
387
|
+
metrics.graphExtraction.cacheHits += toFiniteNumber(telemetry.cacheHits);
|
|
388
|
+
metrics.graphExtraction.cacheMisses += toFiniteNumber(telemetry.cacheMisses);
|
|
389
|
+
metrics.graphExtraction.truncations += toFiniteNumber(telemetry.truncationCount);
|
|
390
|
+
metrics.graphExtraction.failures += toFiniteNumber(telemetry.failureCount);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
metrics.graphExtraction.durationMs += toFiniteNumber(result.graphExtractionDurationMs);
|
|
394
|
+
if (Array.isArray(result.extract)) {
|
|
395
|
+
for (const e of result.extract) {
|
|
396
|
+
metrics.sessionExtraction.sessionsScanned += toFiniteNumber(e.sessionsProcessed);
|
|
397
|
+
metrics.sessionExtraction.sessionsSkipped += toFiniteNumber(e.sessionsSkipped);
|
|
398
|
+
if (Array.isArray(e.sessions)) {
|
|
399
|
+
metrics.sessionExtraction.sessionsExtracted += e.sessions.filter((s) => Array.isArray(s.proposalIds) && s.proposalIds.length > 0).length;
|
|
400
|
+
}
|
|
401
|
+
metrics.sessionExtraction.proposalsCreated += Array.isArray(e.proposals) ? e.proposals.length : 0;
|
|
402
|
+
metrics.sessionExtraction.warnings += Array.isArray(e.warnings) ? e.warnings.length : 0;
|
|
403
|
+
metrics.sessionExtraction.durationMs += toFiniteNumber(e.durationMs);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return metrics;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Finalize derived flags and rates on an accumulator. Used both for the
|
|
410
|
+
* window-level aggregate and for each per-run row in --detail per-run mode
|
|
411
|
+
* so the single-row metrics still expose `ran` / `yieldRate` / `cacheHitRate`.
|
|
412
|
+
*/
|
|
413
|
+
function finalizeImproveMetrics(metrics) {
|
|
414
|
+
metrics.consolidation.ran =
|
|
415
|
+
metrics.consolidation.processed > 0 ||
|
|
416
|
+
metrics.consolidation.durationMs > 0 ||
|
|
417
|
+
metrics.consolidation.promoted > 0 ||
|
|
418
|
+
metrics.consolidation.merged > 0 ||
|
|
419
|
+
metrics.consolidation.deleted > 0 ||
|
|
420
|
+
metrics.consolidation.contradicted > 0 ||
|
|
421
|
+
metrics.consolidation.totalChunks > 0;
|
|
422
|
+
metrics.memoryInference.ran =
|
|
423
|
+
metrics.memoryInference.considered > 0 ||
|
|
424
|
+
metrics.memoryInference.written > 0 ||
|
|
425
|
+
metrics.memoryInference.durationMs > 0;
|
|
426
|
+
metrics.memoryInference.writes = metrics.memoryInference.written;
|
|
427
|
+
// Yield denominator excludes cache hits AND legacy (pre-cacheHits-field)
|
|
428
|
+
// envelopes. Only runs whose envelope carries a `cacheHits` field
|
|
429
|
+
// contribute to freshAttempts/yieldRate; legacy rows remain in
|
|
430
|
+
// `considered`/`written` for totals but are excluded from the rate so
|
|
431
|
+
// they cannot drag it down. See ImproveHealthMetrics.memoryInference
|
|
432
|
+
// jsdoc for the rationale.
|
|
433
|
+
metrics.memoryInference.freshAttempts = Math.max(0, metrics.memoryInference.yieldEligibleConsidered -
|
|
434
|
+
metrics.memoryInference.cacheHits -
|
|
435
|
+
metrics.memoryInference.skippedAborted);
|
|
436
|
+
metrics.memoryInference.yieldRate =
|
|
437
|
+
metrics.memoryInference.freshAttempts > 0
|
|
438
|
+
? roundRate(metrics.memoryInference.yieldEligibleWritten / metrics.memoryInference.freshAttempts)
|
|
439
|
+
: 0;
|
|
440
|
+
metrics.graphExtraction.ran =
|
|
441
|
+
metrics.graphExtraction.extractedFiles > 0 ||
|
|
442
|
+
metrics.graphExtraction.entities > 0 ||
|
|
443
|
+
metrics.graphExtraction.durationMs > 0;
|
|
444
|
+
const cacheTotal = metrics.graphExtraction.cacheHits + metrics.graphExtraction.cacheMisses;
|
|
445
|
+
metrics.graphExtraction.cacheHitRate = cacheTotal > 0 ? roundRate(metrics.graphExtraction.cacheHits / cacheTotal) : 0;
|
|
446
|
+
metrics.sessionExtraction.ran =
|
|
447
|
+
metrics.sessionExtraction.sessionsScanned > 0 ||
|
|
448
|
+
metrics.sessionExtraction.proposalsCreated > 0 ||
|
|
449
|
+
metrics.sessionExtraction.durationMs > 0;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Merge per-row metrics from `src` into accumulator `dst`. All numeric fields
|
|
453
|
+
* are additive; cumulative rates are recomputed by finalizeImproveMetrics.
|
|
454
|
+
*/
|
|
455
|
+
function mergeImproveMetrics(dst, src) {
|
|
456
|
+
dst.plannedRefs += src.plannedRefs;
|
|
457
|
+
dst.profileFilteredRefs += src.profileFilteredRefs;
|
|
458
|
+
dst.actions.reflect.ok += src.actions.reflect.ok;
|
|
459
|
+
dst.actions.reflect.failed += src.actions.reflect.failed;
|
|
460
|
+
dst.actions.reflect.cooldown += src.actions.reflect.cooldown;
|
|
461
|
+
dst.actions.reflect.skipped += src.actions.reflect.skipped;
|
|
462
|
+
dst.actions.reflect.guardRejected += src.actions.reflect.guardRejected;
|
|
463
|
+
for (const [reason, count] of Object.entries(src.actions.reflect.skippedByReason)) {
|
|
464
|
+
dst.actions.reflect.skippedByReason[reason] = (dst.actions.reflect.skippedByReason[reason] ?? 0) + count;
|
|
465
|
+
}
|
|
466
|
+
dst.actions.distill.queued += src.actions.distill.queued;
|
|
467
|
+
dst.actions.distill.llmFailed += src.actions.distill.llmFailed;
|
|
468
|
+
dst.actions.distill.qualityRejected += src.actions.distill.qualityRejected;
|
|
469
|
+
dst.actions.distill.judgeRejected += src.actions.distill.judgeRejected;
|
|
470
|
+
dst.actions.distill.validatorRejected += src.actions.distill.validatorRejected;
|
|
471
|
+
dst.actions.distill.configDisabled += src.actions.distill.configDisabled;
|
|
472
|
+
dst.actions.distill.skipped += src.actions.distill.skipped;
|
|
473
|
+
for (const [reason, count] of Object.entries(src.actions.distill.skippedByReason)) {
|
|
474
|
+
dst.actions.distill.skippedByReason[reason] = (dst.actions.distill.skippedByReason[reason] ?? 0) + count;
|
|
475
|
+
}
|
|
476
|
+
dst.actions.distill.deferred += src.actions.distill.deferred;
|
|
477
|
+
for (const [reason, count] of Object.entries(src.actions.distill.deferredByReason)) {
|
|
478
|
+
dst.actions.distill.deferredByReason[reason] = (dst.actions.distill.deferredByReason[reason] ?? 0) + count;
|
|
479
|
+
}
|
|
480
|
+
dst.actions.memoryPrune += src.actions.memoryPrune;
|
|
481
|
+
dst.actions.memoryInference += src.actions.memoryInference;
|
|
482
|
+
dst.actions.graphExtraction += src.actions.graphExtraction;
|
|
483
|
+
dst.actions.error += src.actions.error;
|
|
484
|
+
dst.reflectsWithErrorContext += src.reflectsWithErrorContext;
|
|
485
|
+
dst.coverageGapCount += src.coverageGapCount;
|
|
486
|
+
dst.evalCasesWritten += src.evalCasesWritten;
|
|
487
|
+
dst.deadUrlCount += src.deadUrlCount;
|
|
488
|
+
dst.memorySummary.eligible += src.memorySummary.eligible;
|
|
489
|
+
dst.memorySummary.derived += src.memorySummary.derived;
|
|
490
|
+
dst.memoryCleanup.pruneCandidates += src.memoryCleanup.pruneCandidates;
|
|
491
|
+
dst.memoryCleanup.contradictionCandidates += src.memoryCleanup.contradictionCandidates;
|
|
492
|
+
dst.memoryCleanup.beliefStateTransitions += src.memoryCleanup.beliefStateTransitions;
|
|
493
|
+
dst.memoryCleanup.consolidationCandidates += src.memoryCleanup.consolidationCandidates;
|
|
494
|
+
dst.memoryCleanup.archived += src.memoryCleanup.archived;
|
|
495
|
+
dst.memoryCleanup.warnings += src.memoryCleanup.warnings;
|
|
496
|
+
dst.consolidation.processed += src.consolidation.processed;
|
|
497
|
+
dst.consolidation.promoted += src.consolidation.promoted;
|
|
498
|
+
dst.consolidation.merged += src.consolidation.merged;
|
|
499
|
+
dst.consolidation.deleted += src.consolidation.deleted;
|
|
500
|
+
dst.consolidation.contradicted += src.consolidation.contradicted;
|
|
501
|
+
dst.consolidation.failedChunks += src.consolidation.failedChunks;
|
|
502
|
+
dst.consolidation.totalChunks += src.consolidation.totalChunks;
|
|
503
|
+
dst.consolidation.durationMs += src.consolidation.durationMs;
|
|
504
|
+
dst.consolidation.judgedNoAction += src.consolidation.judgedNoAction;
|
|
505
|
+
dst.consolidation.mergedSecondaries += src.consolidation.mergedSecondaries;
|
|
506
|
+
dst.consolidation.failedChunkMemories += src.consolidation.failedChunkMemories;
|
|
507
|
+
for (const [reason, count] of Object.entries(src.consolidation.skipReasons)) {
|
|
508
|
+
dst.consolidation.skipReasons[reason] = (dst.consolidation.skipReasons[reason] ?? 0) + count;
|
|
509
|
+
}
|
|
510
|
+
dst.memoryInference.considered += src.memoryInference.considered;
|
|
511
|
+
dst.memoryInference.cacheHits += src.memoryInference.cacheHits;
|
|
512
|
+
dst.memoryInference.splitParents += src.memoryInference.splitParents;
|
|
513
|
+
dst.memoryInference.written += src.memoryInference.written;
|
|
514
|
+
dst.memoryInference.skippedNoFacts += src.memoryInference.skippedNoFacts;
|
|
515
|
+
dst.memoryInference.skippedChildExists += src.memoryInference.skippedChildExists;
|
|
516
|
+
dst.memoryInference.skippedAborted += src.memoryInference.skippedAborted;
|
|
517
|
+
dst.memoryInference.unaccounted += src.memoryInference.unaccounted;
|
|
518
|
+
dst.memoryInference.yieldEligibleRuns += src.memoryInference.yieldEligibleRuns;
|
|
519
|
+
dst.memoryInference.yieldEligibleConsidered += src.memoryInference.yieldEligibleConsidered;
|
|
520
|
+
dst.memoryInference.yieldEligibleWritten += src.memoryInference.yieldEligibleWritten;
|
|
521
|
+
dst.memoryInference.durationMs += src.memoryInference.durationMs;
|
|
522
|
+
dst.graphExtraction.extractedFiles += src.graphExtraction.extractedFiles;
|
|
523
|
+
dst.graphExtraction.entities += src.graphExtraction.entities;
|
|
524
|
+
dst.graphExtraction.relations += src.graphExtraction.relations;
|
|
525
|
+
dst.graphExtraction.cacheHits += src.graphExtraction.cacheHits;
|
|
526
|
+
dst.graphExtraction.cacheMisses += src.graphExtraction.cacheMisses;
|
|
527
|
+
dst.graphExtraction.truncations += src.graphExtraction.truncations;
|
|
528
|
+
dst.graphExtraction.failures += src.graphExtraction.failures;
|
|
529
|
+
dst.graphExtraction.durationMs += src.graphExtraction.durationMs;
|
|
530
|
+
dst.sessionExtraction.sessionsScanned += src.sessionExtraction.sessionsScanned;
|
|
531
|
+
dst.sessionExtraction.sessionsExtracted += src.sessionExtraction.sessionsExtracted;
|
|
532
|
+
dst.sessionExtraction.sessionsSkipped += src.sessionExtraction.sessionsSkipped;
|
|
533
|
+
dst.sessionExtraction.proposalsCreated += src.sessionExtraction.proposalsCreated;
|
|
534
|
+
dst.sessionExtraction.warnings += src.sessionExtraction.warnings;
|
|
535
|
+
dst.sessionExtraction.durationMs += src.sessionExtraction.durationMs;
|
|
536
|
+
}
|
|
537
|
+
function loadImproveRunRows(db, since, until) {
|
|
538
|
+
const sql = until
|
|
539
|
+
? "SELECT id, started_at, completed_at, ok, scope_mode, scope_value, result_json FROM improve_runs WHERE started_at >= ? AND started_at < ? AND dry_run = 0 ORDER BY started_at DESC"
|
|
540
|
+
: "SELECT id, started_at, completed_at, ok, scope_mode, scope_value, result_json FROM improve_runs WHERE started_at >= ? AND dry_run = 0 ORDER BY started_at DESC";
|
|
541
|
+
return (until ? db.prepare(sql).all(since, until) : db.prepare(sql).all(since));
|
|
542
|
+
}
|
|
543
|
+
function summarizeImproveRuns(db, since, until) {
|
|
544
|
+
const accum = createUnknownImproveMetrics();
|
|
545
|
+
const rows = loadImproveRunRows(db, since, until);
|
|
546
|
+
// Per-phase wall-time samples. Each entry is one envelope's durationMs for
|
|
547
|
+
// that phase. Phases that did not run on a given envelope are simply
|
|
548
|
+
// omitted (NOT counted as 0) so the median/p95 reflect actual phase work.
|
|
549
|
+
const phaseDurations = {
|
|
550
|
+
consolidation: [],
|
|
551
|
+
memoryInference: [],
|
|
552
|
+
graphExtraction: [],
|
|
553
|
+
};
|
|
554
|
+
for (const row of rows) {
|
|
555
|
+
let result;
|
|
556
|
+
try {
|
|
557
|
+
result = JSON.parse(row.result_json);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const perRow = projectRunMetrics(result);
|
|
563
|
+
mergeImproveMetrics(accum, perRow);
|
|
564
|
+
// Collect per-phase durations directly off the envelope. consolidation's
|
|
565
|
+
// duration lives inside the sub-object; memoryInference and graphExtraction
|
|
566
|
+
// expose top-level *DurationMs keys (`memoryInferenceDurationMs`,
|
|
567
|
+
// `graphExtractionDurationMs`) when they actually ran on that envelope.
|
|
568
|
+
const consol = result.consolidation;
|
|
569
|
+
const consolMs = toFiniteNumber(consol?.durationMs);
|
|
570
|
+
if (consolMs > 0)
|
|
571
|
+
phaseDurations.consolidation.push(consolMs);
|
|
572
|
+
const memMs = toFiniteNumber(result.memoryInferenceDurationMs);
|
|
573
|
+
if (memMs > 0)
|
|
574
|
+
phaseDurations.memoryInference.push(memMs);
|
|
575
|
+
const graphMs = toFiniteNumber(result.graphExtractionDurationMs);
|
|
576
|
+
if (graphMs > 0)
|
|
577
|
+
phaseDurations.graphExtraction.push(graphMs);
|
|
578
|
+
}
|
|
579
|
+
finalizeImproveMetrics(accum);
|
|
580
|
+
accum.wallTime.byPhase = {
|
|
581
|
+
consolidation: summarizePhaseDurations(phaseDurations.consolidation),
|
|
582
|
+
memoryInference: summarizePhaseDurations(phaseDurations.memoryInference),
|
|
583
|
+
graphExtraction: summarizePhaseDurations(phaseDurations.graphExtraction),
|
|
584
|
+
};
|
|
585
|
+
return { metrics: accum, runCount: rows.length };
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Aggregate a list of per-envelope phase durations into the
|
|
589
|
+
* `wallTime.byPhase.*` shape: count, total, median, p95. Median/p95 use the
|
|
590
|
+
* same nearest-rank picker as the top-level wallTime stats so the two are
|
|
591
|
+
* comparable.
|
|
592
|
+
*/
|
|
593
|
+
function summarizePhaseDurations(samples) {
|
|
594
|
+
if (samples.length === 0)
|
|
595
|
+
return { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 };
|
|
596
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
597
|
+
const pick = (q) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))] ?? 0;
|
|
598
|
+
const totalMs = sorted.reduce((acc, n) => acc + n, 0);
|
|
599
|
+
return {
|
|
600
|
+
count: sorted.length,
|
|
601
|
+
totalMs,
|
|
602
|
+
medianMs: pick(0.5),
|
|
603
|
+
p95Ms: pick(0.95),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Project an improve_runs row + wall-time lookup into a single ImproveRunSummary.
|
|
608
|
+
* Used by `akm health --detail per-run`.
|
|
609
|
+
*/
|
|
610
|
+
function projectImproveRunSummary(row, wallTimeMs) {
|
|
611
|
+
let result = {};
|
|
612
|
+
try {
|
|
613
|
+
result = JSON.parse(row.result_json);
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
// fall through with empty result so per-stage rollups are zeros
|
|
617
|
+
}
|
|
618
|
+
const perRow = projectRunMetrics(result);
|
|
619
|
+
finalizeImproveMetrics(perRow);
|
|
620
|
+
const orphansPurged = toFiniteNumber(result.orphansPurged);
|
|
621
|
+
const lintSummary = result.lintSummary;
|
|
622
|
+
const lintFixed = lintSummary ? toFiniteNumber(lintSummary.fixed) : 0;
|
|
623
|
+
const lintFlagged = lintSummary ? toFiniteNumber(lintSummary.flagged) : 0;
|
|
624
|
+
return {
|
|
625
|
+
id: row.id,
|
|
626
|
+
startedAt: row.started_at,
|
|
627
|
+
completedAt: row.completed_at,
|
|
628
|
+
wallTimeMs,
|
|
629
|
+
ok: row.ok === 1,
|
|
630
|
+
scope: {
|
|
631
|
+
mode: row.scope_mode,
|
|
632
|
+
...(row.scope_value ? { value: row.scope_value } : {}),
|
|
633
|
+
},
|
|
634
|
+
actions: perRow.actions,
|
|
635
|
+
memorySummary: perRow.memorySummary,
|
|
636
|
+
memoryCleanup: perRow.memoryCleanup,
|
|
637
|
+
consolidation: perRow.consolidation,
|
|
638
|
+
memoryInference: perRow.memoryInference,
|
|
639
|
+
graphExtraction: perRow.graphExtraction,
|
|
640
|
+
reflectsWithErrorContext: perRow.reflectsWithErrorContext,
|
|
641
|
+
evalCasesWritten: perRow.evalCasesWritten,
|
|
642
|
+
orphansPurged,
|
|
643
|
+
lintFixed,
|
|
644
|
+
lintFlagged,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Load task_history intervals for `task_id='akm-improve'` in the window.
|
|
649
|
+
* Returned sorted by startMs ascending so containment lookups can use a
|
|
650
|
+
* linear scan (typical N is ~24/day; not worth a tree).
|
|
651
|
+
*
|
|
652
|
+
* The window filter is widened by 5 minutes on each side because the cron
|
|
653
|
+
* task wraps `akm improve` — the task `started_at` fires at e.g. :07:01
|
|
654
|
+
* while `recordImproveRun` writes the matching `improve_runs.started_at`
|
|
655
|
+
* later (after config load, planning, etc.), so the improve_runs row can
|
|
656
|
+
* be inside the window even when its enclosing task_history row started
|
|
657
|
+
* just before the window opened.
|
|
658
|
+
*/
|
|
659
|
+
function loadTaskIntervals(db, since, until) {
|
|
660
|
+
const sinceMs = new Date(since).getTime();
|
|
661
|
+
const untilMs = until ? new Date(until).getTime() : Number.POSITIVE_INFINITY;
|
|
662
|
+
const widenedSince = new Date(sinceMs - 5 * 60 * 1000).toISOString();
|
|
663
|
+
const widenedUntil = Number.isFinite(untilMs) ? new Date(untilMs + 5 * 60 * 1000).toISOString() : undefined;
|
|
664
|
+
const sql = widenedUntil
|
|
665
|
+
? "SELECT started_at, completed_at FROM task_history WHERE task_id = 'akm-improve' AND started_at >= ? AND started_at < ? AND completed_at IS NOT NULL ORDER BY started_at"
|
|
666
|
+
: "SELECT started_at, completed_at FROM task_history WHERE task_id = 'akm-improve' AND started_at >= ? AND completed_at IS NOT NULL ORDER BY started_at";
|
|
667
|
+
const rows = (widenedUntil ? db.prepare(sql).all(widenedSince, widenedUntil) : db.prepare(sql).all(widenedSince));
|
|
668
|
+
const intervals = [];
|
|
669
|
+
for (const row of rows) {
|
|
670
|
+
const startMs = new Date(row.started_at).getTime();
|
|
671
|
+
const endMs = new Date(row.completed_at).getTime();
|
|
672
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs)
|
|
673
|
+
continue;
|
|
674
|
+
intervals.push({ startMs, endMs, durationMs: endMs - startMs });
|
|
675
|
+
}
|
|
676
|
+
return intervals;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Find the task_history interval that contains the given timestamp. The
|
|
680
|
+
* task wraps `akm improve`, so `improve_runs.started_at` (when
|
|
681
|
+
* `recordImproveRun` writes) always falls inside the enclosing task's
|
|
682
|
+
* [started_at, completed_at]. Returns undefined when no interval
|
|
683
|
+
* contains the timestamp (which happens for manually-invoked improve
|
|
684
|
+
* runs not driven by the `akm-improve` task).
|
|
685
|
+
*
|
|
686
|
+
* Linear scan because N is small. We tolerate a 1s slop on the upper
|
|
687
|
+
* bound to handle clock skew between the wrapper's `completed_at` write
|
|
688
|
+
* and recordImproveRun's `started_at` write.
|
|
689
|
+
*/
|
|
690
|
+
function findContainingTaskInterval(timestampMs, intervals) {
|
|
691
|
+
const SLOP_MS = 1000;
|
|
692
|
+
for (const interval of intervals) {
|
|
693
|
+
if (timestampMs >= interval.startMs && timestampMs <= interval.endMs + SLOP_MS) {
|
|
694
|
+
return interval;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return undefined;
|
|
698
|
+
}
|
|
699
|
+
function buildPerRunSummaries(db, since, until) {
|
|
700
|
+
const rows = loadImproveRunRows(db, since, until);
|
|
701
|
+
const taskIntervals = loadTaskIntervals(db, since, until);
|
|
702
|
+
const summaries = [];
|
|
703
|
+
for (const row of rows) {
|
|
704
|
+
const startMs = new Date(row.started_at).getTime();
|
|
705
|
+
const endMs = new Date(row.completed_at).getTime();
|
|
706
|
+
// Prefer the task_history interval (which has distinct start/end timestamps).
|
|
707
|
+
// Fall back to the improve_runs row's own delta (usually 0 because
|
|
708
|
+
// recordImproveRun writes started_at == completed_at == end-of-run timestamp).
|
|
709
|
+
const fallbackWallMs = Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs ? endMs - startMs : 0;
|
|
710
|
+
const interval = Number.isFinite(startMs) ? findContainingTaskInterval(startMs, taskIntervals) : undefined;
|
|
711
|
+
const wallTimeMs = interval?.durationMs ?? fallbackWallMs;
|
|
712
|
+
summaries.push(projectImproveRunSummary(row, wallTimeMs));
|
|
713
|
+
}
|
|
714
|
+
return summaries;
|
|
715
|
+
}
|
|
716
|
+
function emptyPhaseStats() {
|
|
717
|
+
return {
|
|
718
|
+
consolidation: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
|
|
719
|
+
memoryInference: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
|
|
720
|
+
graphExtraction: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
function computeWallTimeStats(durationsMs, byPhase) {
|
|
724
|
+
const phase = byPhase ?? emptyPhaseStats();
|
|
725
|
+
if (durationsMs.length === 0)
|
|
726
|
+
return { count: 0, medianMs: 0, p95Ms: 0, minMs: 0, maxMs: 0, byPhase: phase };
|
|
727
|
+
const sorted = [...durationsMs].sort((a, b) => a - b);
|
|
728
|
+
const pick = (q) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))] ?? 0;
|
|
729
|
+
return {
|
|
730
|
+
count: sorted.length,
|
|
731
|
+
medianMs: pick(0.5),
|
|
732
|
+
p95Ms: pick(0.95),
|
|
733
|
+
minMs: sorted[0] ?? 0,
|
|
734
|
+
maxMs: sorted[sorted.length - 1] ?? 0,
|
|
735
|
+
byPhase: phase,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
130
738
|
function buildImproveSkipSummary(events) {
|
|
131
739
|
const skipReasons = {};
|
|
132
740
|
for (const event of events) {
|
|
@@ -148,7 +756,31 @@ function probeStateDbRoundTrip(stateDbPath) {
|
|
|
148
756
|
}
|
|
149
757
|
function runAgentProbe() {
|
|
150
758
|
const config = loadConfig();
|
|
151
|
-
|
|
759
|
+
// v2: check profiles.agent first
|
|
760
|
+
if (config.profiles?.agent) {
|
|
761
|
+
const defaultName = config.defaults?.agent;
|
|
762
|
+
const profileCount = Object.keys(config.profiles.agent).length;
|
|
763
|
+
if (profileCount === 0) {
|
|
764
|
+
return {
|
|
765
|
+
name: "agent-profile",
|
|
766
|
+
kind: "deterministic",
|
|
767
|
+
status: "unknown",
|
|
768
|
+
confidence: "high",
|
|
769
|
+
message: "No agent profiles configured in profiles.agent.",
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
const profileName = defaultName ?? Object.keys(config.profiles.agent)[0];
|
|
773
|
+
const profile = config.profiles.agent[profileName];
|
|
774
|
+
return {
|
|
775
|
+
name: "agent-profile",
|
|
776
|
+
kind: "deterministic",
|
|
777
|
+
status: "pass",
|
|
778
|
+
confidence: "high",
|
|
779
|
+
message: `v2 agent profile "${profileName}" configured (platform: ${profile?.platform ?? "unknown"}).`,
|
|
780
|
+
evidence: { profile: profileName, platform: profile?.platform, profileCount },
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
if (!config.profiles?.agent && !config.defaults?.agent) {
|
|
152
784
|
return {
|
|
153
785
|
name: "agent-profile",
|
|
154
786
|
kind: "deterministic",
|
|
@@ -159,7 +791,7 @@ function runAgentProbe() {
|
|
|
159
791
|
}
|
|
160
792
|
let profile;
|
|
161
793
|
try {
|
|
162
|
-
profile = requireAgentProfile(config
|
|
794
|
+
profile = requireAgentProfile(config);
|
|
163
795
|
}
|
|
164
796
|
catch (error) {
|
|
165
797
|
return {
|
|
@@ -182,7 +814,7 @@ function runAgentProbe() {
|
|
|
182
814
|
evidence: { profile: profile.name, sdkMode: true, model: profile.model ?? null },
|
|
183
815
|
};
|
|
184
816
|
}
|
|
185
|
-
const detections = detectAgentCliProfiles(config
|
|
817
|
+
const detections = detectAgentCliProfiles(config);
|
|
186
818
|
const detection = detections.find((entry) => entry.name === profile.name);
|
|
187
819
|
if (!detection?.available) {
|
|
188
820
|
return {
|
|
@@ -219,7 +851,175 @@ function runAgentProbe() {
|
|
|
219
851
|
evidence: { profile: profile.name, bin: profile.bin, version: (version.stdout ?? "").trim() },
|
|
220
852
|
};
|
|
221
853
|
}
|
|
854
|
+
/**
|
|
855
|
+
* Parse a `--window-compare <duration>` shorthand into two adjacent windows
|
|
856
|
+
* (current, prior). Duration syntax matches {@link parseHealthSince}.
|
|
857
|
+
*/
|
|
858
|
+
function resolveWindowCompare(duration) {
|
|
859
|
+
const trimmed = duration.trim();
|
|
860
|
+
const durationMatch = trimmed.match(/^(\d+)([dhm])$/i);
|
|
861
|
+
if (!durationMatch) {
|
|
862
|
+
throw new UsageError("--window-compare must be a duration like '24h', '7d', or '30m'.", "INVALID_FLAG_VALUE");
|
|
863
|
+
}
|
|
864
|
+
const amount = Number.parseInt(durationMatch[1] ?? "0", 10);
|
|
865
|
+
const unit = (durationMatch[2] ?? "h").toLowerCase();
|
|
866
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
867
|
+
throw new UsageError("--window-compare must be a positive duration.", "INVALID_FLAG_VALUE");
|
|
868
|
+
}
|
|
869
|
+
const multiplier = unit === "h" ? 60 * 60 * 1000 : unit === "m" ? 60 * 1000 : 24 * 60 * 60 * 1000;
|
|
870
|
+
const ms = amount * multiplier;
|
|
871
|
+
const now = Date.now();
|
|
872
|
+
const currentSince = new Date(now - ms).toISOString();
|
|
873
|
+
const currentUntil = new Date(now).toISOString();
|
|
874
|
+
const priorSince = new Date(now - 2 * ms).toISOString();
|
|
875
|
+
const priorUntil = currentSince;
|
|
876
|
+
return [
|
|
877
|
+
{ name: "current", since: currentSince, until: currentUntil },
|
|
878
|
+
{ name: "prior", since: priorSince, until: priorUntil },
|
|
879
|
+
];
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Parse a single repeatable `--windows` value of the form
|
|
883
|
+
* `name=...,since=...,until=...`. All keys are optional EXCEPT name and since.
|
|
884
|
+
*/
|
|
885
|
+
export function parseWindowSpec(raw) {
|
|
886
|
+
const fields = {};
|
|
887
|
+
for (const part of raw.split(",")) {
|
|
888
|
+
const trimmed = part.trim();
|
|
889
|
+
if (!trimmed)
|
|
890
|
+
continue;
|
|
891
|
+
const eq = trimmed.indexOf("=");
|
|
892
|
+
if (eq < 0) {
|
|
893
|
+
throw new UsageError(`--windows entry must be a comma-separated list of key=value pairs: ${raw}`, "INVALID_FLAG_VALUE");
|
|
894
|
+
}
|
|
895
|
+
const key = trimmed.slice(0, eq).trim();
|
|
896
|
+
const value = trimmed.slice(eq + 1).trim();
|
|
897
|
+
fields[key] = value;
|
|
898
|
+
}
|
|
899
|
+
if (!fields.name) {
|
|
900
|
+
throw new UsageError(`--windows entry is missing required 'name': ${raw}`, "INVALID_FLAG_VALUE");
|
|
901
|
+
}
|
|
902
|
+
if (!fields.since) {
|
|
903
|
+
throw new UsageError(`--windows entry is missing required 'since': ${raw}`, "INVALID_FLAG_VALUE");
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
name: fields.name,
|
|
907
|
+
since: fields.since,
|
|
908
|
+
...(fields.until ? { until: fields.until } : {}),
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
/** Hard-coded list of "interesting" metric paths for window-compare deltas. */
|
|
912
|
+
const INTERESTING_DELTA_PATHS = [
|
|
913
|
+
"improve.actions.reflect.failed",
|
|
914
|
+
"improve.actions.reflect.guardRejected",
|
|
915
|
+
"improve.actions.distill.llmFailed",
|
|
916
|
+
"improve.actions.distill.queued",
|
|
917
|
+
"improve.actions.distill.deferred",
|
|
918
|
+
"improve.consolidation.promoted",
|
|
919
|
+
"improve.memoryInference.written",
|
|
920
|
+
"improve.memoryInference.yieldRate",
|
|
921
|
+
"improve.memoryInference.skippedNoFacts",
|
|
922
|
+
"improve.graphExtraction.cacheHitRate",
|
|
923
|
+
"improve.graphExtraction.failures",
|
|
924
|
+
"improve.sessionExtraction.sessionsScanned",
|
|
925
|
+
"improve.sessionExtraction.proposalsCreated",
|
|
926
|
+
"improve.wallTime.medianMs",
|
|
927
|
+
"improve.wallTime.p95Ms",
|
|
928
|
+
];
|
|
929
|
+
function readNumericPath(obj, path) {
|
|
930
|
+
const parts = path.split(".");
|
|
931
|
+
let cursor = obj;
|
|
932
|
+
for (const part of parts) {
|
|
933
|
+
if (typeof cursor !== "object" || cursor === null)
|
|
934
|
+
return 0;
|
|
935
|
+
cursor = cursor[part];
|
|
936
|
+
}
|
|
937
|
+
return typeof cursor === "number" && Number.isFinite(cursor) ? cursor : 0;
|
|
938
|
+
}
|
|
939
|
+
function computeDeltas(first, last) {
|
|
940
|
+
const out = {};
|
|
941
|
+
for (const path of INTERESTING_DELTA_PATHS) {
|
|
942
|
+
const from = readNumericPath(first, path);
|
|
943
|
+
const to = readNumericPath(last, path);
|
|
944
|
+
if (from === 0 && to === 0)
|
|
945
|
+
continue;
|
|
946
|
+
let pctChange;
|
|
947
|
+
if (from === 0) {
|
|
948
|
+
pctChange = to === 0 ? 0 : "+inf";
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
pctChange = Number((((to - from) / from) * 100).toFixed(2));
|
|
952
|
+
}
|
|
953
|
+
out[path] = { from, to, pctChange };
|
|
954
|
+
}
|
|
955
|
+
return out;
|
|
956
|
+
}
|
|
957
|
+
function buildWindowMetrics(db, stateDbPath, since, until) {
|
|
958
|
+
const taskRows = queryTaskHistory(db, { since }).filter((row) => {
|
|
959
|
+
const startMs = new Date(row.started_at).getTime();
|
|
960
|
+
const untilMs = new Date(until).getTime();
|
|
961
|
+
return !Number.isFinite(untilMs) || startMs < untilMs;
|
|
962
|
+
});
|
|
963
|
+
const taskRowsWithLogs = taskRows.filter((row) => row.log_path !== null);
|
|
964
|
+
const existingLogRows = taskRowsWithLogs.filter((row) => row.log_path && fs.existsSync(row.log_path));
|
|
965
|
+
const failedTaskRows = taskRows.filter((row) => row.status === "failed");
|
|
966
|
+
const activeRows = taskRows.filter((row) => row.status === "active");
|
|
967
|
+
const stuckActiveRuns = activeRows.filter((row) => Date.now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS).length;
|
|
968
|
+
const promptRows = taskRows.filter((row) => row.target_kind === "prompt");
|
|
969
|
+
const promptFailures = promptRows.filter((row) => {
|
|
970
|
+
const detail = parseTaskMetadata(row).detail;
|
|
971
|
+
return typeof detail?.reason === "string" && detail.reason.length > 0;
|
|
972
|
+
});
|
|
973
|
+
const logBackingRate = taskRowsWithLogs.length === 0 ? 1 : existingLogRows.length / taskRowsWithLogs.length;
|
|
974
|
+
const taskFailRate = taskRows.length === 0 ? 0 : failedTaskRows.length / taskRows.length;
|
|
975
|
+
const agentFailureRate = promptRows.length === 0 ? 0 : promptFailures.length / promptRows.length;
|
|
976
|
+
const improveInvoked = readEvents({ since, type: "improve_invoked" }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime()).length;
|
|
977
|
+
const improveCompletedEvents = readEvents({ since, type: IMPROVE_COMPLETED_EVENT }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime());
|
|
978
|
+
const improveSkippedEvents = readEvents({ since, type: "improve_skipped" }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime());
|
|
979
|
+
const eventsMetrics = summarizeImproveCompleted(improveCompletedEvents);
|
|
980
|
+
const { metrics: improveSummary, runCount } = summarizeImproveRuns(db, since, until);
|
|
981
|
+
improveSummary.invoked = improveInvoked;
|
|
982
|
+
improveSummary.completed = eventsMetrics.completed;
|
|
983
|
+
const skipSummary = buildImproveSkipSummary(improveSkippedEvents);
|
|
984
|
+
improveSummary.skipped = skipSummary.skipped;
|
|
985
|
+
improveSummary.skipReasons = skipSummary.skipReasons;
|
|
986
|
+
// Preserve the per-phase aggregation computed by summarizeImproveRuns and
|
|
987
|
+
// derive top-level wall times from the same improve-runs window so counts
|
|
988
|
+
// and percentiles stay aligned with per-run reporting.
|
|
989
|
+
const perRunSummaries = buildPerRunSummaries(db, since, until);
|
|
990
|
+
const wallTimes = perRunSummaries.map((run) => run.wallTimeMs).filter((ms) => Number.isFinite(ms) && ms > 0);
|
|
991
|
+
improveSummary.wallTime = computeWallTimeStats(wallTimes, improveSummary.wallTime.byPhase);
|
|
992
|
+
const metrics = {
|
|
993
|
+
taskFailRate: roundRate(taskFailRate),
|
|
994
|
+
agentFailureRate: roundRate(agentFailureRate),
|
|
995
|
+
stuckActiveRuns,
|
|
996
|
+
logBackingRate: roundRate(logBackingRate),
|
|
997
|
+
probeRoundTripMs: null,
|
|
998
|
+
};
|
|
999
|
+
return { improve: improveSummary, metrics, runs: runCount };
|
|
1000
|
+
}
|
|
1001
|
+
function validateAkmHealthOptions(options) {
|
|
1002
|
+
if (options.groupBy !== undefined && options.groupBy !== "run") {
|
|
1003
|
+
throw new UsageError(`Invalid value for --group-by: ${options.groupBy}. Expected: run`, "INVALID_FLAG_VALUE");
|
|
1004
|
+
}
|
|
1005
|
+
if (options.windowCompare !== undefined && options.windows !== undefined && options.windows.length > 0) {
|
|
1006
|
+
throw new UsageError("--window-compare and --windows are mutually exclusive.", "INVALID_FLAG_VALUE");
|
|
1007
|
+
}
|
|
1008
|
+
if (options.windows) {
|
|
1009
|
+
if (options.windows.length > 4) {
|
|
1010
|
+
throw new UsageError("--windows accepts at most 4 entries.", "INVALID_FLAG_VALUE");
|
|
1011
|
+
}
|
|
1012
|
+
const seen = new Set();
|
|
1013
|
+
for (const spec of options.windows) {
|
|
1014
|
+
if (seen.has(spec.name)) {
|
|
1015
|
+
throw new UsageError(`--windows has duplicate name: ${spec.name}`, "INVALID_FLAG_VALUE");
|
|
1016
|
+
}
|
|
1017
|
+
seen.add(spec.name);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
222
1021
|
export function akmHealth(options = {}) {
|
|
1022
|
+
validateAkmHealthOptions(options);
|
|
223
1023
|
const since = parseHealthSince(options.since);
|
|
224
1024
|
const stateDbPath = getStateDbPathInDataDir();
|
|
225
1025
|
const hardChecks = [];
|
|
@@ -320,11 +1120,16 @@ export function akmHealth(options = {}) {
|
|
|
320
1120
|
const improveInvoked = readEvents({ since, type: "improve_invoked" }, { dbPath: stateDbPath }).events.length;
|
|
321
1121
|
const improveCompletedEvents = readEvents({ since, type: IMPROVE_COMPLETED_EVENT }, { dbPath: stateDbPath }).events;
|
|
322
1122
|
const improveSkippedEvents = readEvents({ since, type: "improve_skipped" }, { dbPath: stateDbPath }).events;
|
|
323
|
-
const
|
|
1123
|
+
const eventsMetrics = summarizeImproveCompleted(improveCompletedEvents);
|
|
1124
|
+
const { metrics: improveSummary } = summarizeImproveRuns(db, since);
|
|
324
1125
|
improveSummary.invoked = improveInvoked;
|
|
1126
|
+
improveSummary.completed = eventsMetrics.completed;
|
|
325
1127
|
const skipSummary = buildImproveSkipSummary(improveSkippedEvents);
|
|
326
1128
|
improveSummary.skipped = skipSummary.skipped;
|
|
327
1129
|
improveSummary.skipReasons = skipSummary.skipReasons;
|
|
1130
|
+
const perRunSummaries = buildPerRunSummaries(db, since);
|
|
1131
|
+
const wallTimes = perRunSummaries.map((run) => run.wallTimeMs).filter((ms) => Number.isFinite(ms) && ms > 0);
|
|
1132
|
+
improveSummary.wallTime = computeWallTimeStats(wallTimes, improveSummary.wallTime.byPhase);
|
|
328
1133
|
let sessionLogEntries = [];
|
|
329
1134
|
try {
|
|
330
1135
|
const sinceDays = Math.max(0, Math.ceil((Date.now() - new Date(since).getTime()) / (24 * 60 * 60 * 1000)));
|
|
@@ -338,16 +1143,46 @@ export function akmHealth(options = {}) {
|
|
|
338
1143
|
catch {
|
|
339
1144
|
sessionLogEntries = [];
|
|
340
1145
|
}
|
|
1146
|
+
// session-log-failures: demoted to informational — the ERROR_PATTERNS regex
|
|
1147
|
+
// scans pre-LLM session text and produces false positives on diagnostic
|
|
1148
|
+
// conversation. It does not gate the real extraction pipeline (akmExtract).
|
|
1149
|
+
// Never triggers warn; kept for backward-compat visibility only.
|
|
341
1150
|
advisories.push({
|
|
342
1151
|
name: "session-log-failures",
|
|
343
1152
|
kind: "heuristic",
|
|
344
|
-
status:
|
|
345
|
-
confidence:
|
|
1153
|
+
status: "pass",
|
|
1154
|
+
confidence: "low",
|
|
346
1155
|
message: sessionLogEntries.length === 0
|
|
347
1156
|
? "No repeated external session-log failure patterns were detected."
|
|
348
|
-
: `${sessionLogEntries.length}
|
|
1157
|
+
: `${sessionLogEntries.length} raw session-log keyword match(es) detected (pre-LLM, informational only).`,
|
|
349
1158
|
evidence: { candidates: sessionLogEntries.slice(0, 5) },
|
|
350
1159
|
});
|
|
1160
|
+
const sx = improveSummary.sessionExtraction;
|
|
1161
|
+
const sxWarnReasons = [];
|
|
1162
|
+
if (sx.warnings > 0)
|
|
1163
|
+
sxWarnReasons.push(`${sx.warnings} harness error(s)`);
|
|
1164
|
+
if (sx.ran && sx.sessionsScanned >= 5 && sx.proposalsCreated === 0)
|
|
1165
|
+
sxWarnReasons.push("no proposals generated across scanned sessions");
|
|
1166
|
+
advisories.push({
|
|
1167
|
+
name: "session-extraction",
|
|
1168
|
+
kind: "heuristic",
|
|
1169
|
+
status: sxWarnReasons.length > 0 ? "warn" : "pass",
|
|
1170
|
+
confidence: sx.ran ? "medium" : "low",
|
|
1171
|
+
message: sx.ran
|
|
1172
|
+
? sxWarnReasons.length > 0
|
|
1173
|
+
? `Session extraction degraded: ${sxWarnReasons.join("; ")}.`
|
|
1174
|
+
: `Session extraction healthy: ${sx.sessionsScanned} scanned, ${sx.sessionsExtracted} extracted, ${sx.proposalsCreated} proposal(s) created.`
|
|
1175
|
+
: "Session extraction not active (feature disabled or no harness available).",
|
|
1176
|
+
evidence: {
|
|
1177
|
+
ran: sx.ran,
|
|
1178
|
+
sessionsScanned: sx.sessionsScanned,
|
|
1179
|
+
sessionsExtracted: sx.sessionsExtracted,
|
|
1180
|
+
sessionsSkipped: sx.sessionsSkipped,
|
|
1181
|
+
proposalsCreated: sx.proposalsCreated,
|
|
1182
|
+
warnings: sx.warnings,
|
|
1183
|
+
durationMs: sx.durationMs,
|
|
1184
|
+
},
|
|
1185
|
+
});
|
|
351
1186
|
const metrics = {
|
|
352
1187
|
taskFailRate: roundRate(taskFailRate),
|
|
353
1188
|
agentFailureRate: roundRate(agentFailureRate),
|
|
@@ -358,19 +1193,171 @@ export function akmHealth(options = {}) {
|
|
|
358
1193
|
const hardFailure = hardChecks.some((check) => check.status === "fail");
|
|
359
1194
|
const deterministicWarnings = [...hardChecks, ...advisories].some((check) => check.status === "warn" && check.kind === "deterministic");
|
|
360
1195
|
const status = hardFailure ? "fail" : deterministicWarnings ? "warn" : "pass";
|
|
1196
|
+
// ── Window-compare mode (Phase 3) ─────────────────────────────────────
|
|
1197
|
+
let windowSpecs;
|
|
1198
|
+
if (options.windowCompare) {
|
|
1199
|
+
windowSpecs = resolveWindowCompare(options.windowCompare);
|
|
1200
|
+
}
|
|
1201
|
+
else if (options.windows && options.windows.length > 0) {
|
|
1202
|
+
windowSpecs = options.windows;
|
|
1203
|
+
}
|
|
1204
|
+
let windowResults;
|
|
1205
|
+
let deltas;
|
|
1206
|
+
let topLevelImprove = improveSummary;
|
|
1207
|
+
let topLevelMetrics = metrics;
|
|
1208
|
+
let topLevelSince = since;
|
|
1209
|
+
if (windowSpecs && db) {
|
|
1210
|
+
windowResults = windowSpecs.map((spec) => {
|
|
1211
|
+
const winSince = parseHealthSince(spec.since);
|
|
1212
|
+
const winUntil = spec.until ? parseHealthSince(spec.until) : new Date().toISOString();
|
|
1213
|
+
const bundle = buildWindowMetrics(db, stateDbPath, winSince, winUntil);
|
|
1214
|
+
return {
|
|
1215
|
+
name: spec.name,
|
|
1216
|
+
since: winSince,
|
|
1217
|
+
until: winUntil,
|
|
1218
|
+
runs: bundle.runs,
|
|
1219
|
+
improve: bundle.improve,
|
|
1220
|
+
metrics: bundle.metrics,
|
|
1221
|
+
};
|
|
1222
|
+
});
|
|
1223
|
+
// Preserve backward compat: top-level improve/metrics reflect window 0.
|
|
1224
|
+
if (windowResults.length > 0) {
|
|
1225
|
+
topLevelImprove = windowResults[0].improve;
|
|
1226
|
+
topLevelMetrics = { ...windowResults[0].metrics, probeRoundTripMs: probe.durationMs };
|
|
1227
|
+
topLevelSince = windowResults[0].since;
|
|
1228
|
+
}
|
|
1229
|
+
if (windowResults.length >= 2) {
|
|
1230
|
+
// Deltas always read chronologically: `from` = earliest window,
|
|
1231
|
+
// `to` = latest. Positive pctChange on a failure metric (e.g.
|
|
1232
|
+
// distill.llmFailed) means things got WORSE going forward in
|
|
1233
|
+
// time; negative means improvement. Window 0 in the output
|
|
1234
|
+
// array is whatever the user specified first (typically
|
|
1235
|
+
// `current` for --window-compare), but the delta direction is
|
|
1236
|
+
// independent of that array order.
|
|
1237
|
+
const sorted = [...windowResults].sort((a, b) => new Date(a.since).getTime() - new Date(b.since).getTime());
|
|
1238
|
+
deltas = computeDeltas(sorted[0], sorted[sorted.length - 1]);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
// ── Per-run mode (Phase 2) ────────────────────────────────────────────
|
|
1242
|
+
let runs;
|
|
1243
|
+
if (options.groupBy === "run") {
|
|
1244
|
+
runs = buildPerRunSummaries(db, since);
|
|
1245
|
+
}
|
|
361
1246
|
return {
|
|
362
|
-
schemaVersion:
|
|
1247
|
+
schemaVersion: 2,
|
|
363
1248
|
ok: !hardFailure,
|
|
364
1249
|
status,
|
|
365
|
-
since,
|
|
1250
|
+
since: topLevelSince,
|
|
366
1251
|
hardChecks,
|
|
367
1252
|
advisories,
|
|
368
|
-
metrics,
|
|
369
|
-
improve:
|
|
1253
|
+
metrics: topLevelMetrics,
|
|
1254
|
+
improve: topLevelImprove,
|
|
370
1255
|
sessionLogAdvisories: sessionLogEntries,
|
|
1256
|
+
...(runs ? { runs } : {}),
|
|
1257
|
+
...(windowResults ? { windows: windowResults } : {}),
|
|
1258
|
+
...(deltas ? { deltas } : {}),
|
|
371
1259
|
};
|
|
372
1260
|
}
|
|
373
1261
|
finally {
|
|
374
1262
|
db.close();
|
|
375
1263
|
}
|
|
376
1264
|
}
|
|
1265
|
+
// ── Markdown renderers ───────────────────────────────────────────────────────
|
|
1266
|
+
function padRight(s, width) {
|
|
1267
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
1268
|
+
}
|
|
1269
|
+
function renderTable(headers, rows) {
|
|
1270
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
1271
|
+
const lines = [];
|
|
1272
|
+
lines.push(headers.map((h, i) => padRight(h, widths[i] ?? 0)).join(" "));
|
|
1273
|
+
for (const row of rows) {
|
|
1274
|
+
lines.push(row.map((cell, i) => padRight(cell ?? "", widths[i] ?? 0)).join(" "));
|
|
1275
|
+
}
|
|
1276
|
+
return lines.join("\n");
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Render `--detail per-run` rows as a TSV-ish aligned table. The column
|
|
1280
|
+
* shape was originally inherited from the retired
|
|
1281
|
+
* `scripts/improve-stats/runs-detail` bash helper; keep the same shape
|
|
1282
|
+
* so operator muscle memory carries over.
|
|
1283
|
+
*
|
|
1284
|
+
* Columns: ts | ok | actions | refl_ok/fail/cd/skip |
|
|
1285
|
+
* distill_q/llm-fail/qrej/cfg/skip | cons_proc/promo/merge/del |
|
|
1286
|
+
* mem_cons/written/skip | graph_f/e/r | orphans | lint_f/fl
|
|
1287
|
+
*/
|
|
1288
|
+
export function renderRunsDetailMd(runs) {
|
|
1289
|
+
const headers = [
|
|
1290
|
+
"ts",
|
|
1291
|
+
"ok",
|
|
1292
|
+
"actions",
|
|
1293
|
+
"refl_ok/fail/cd/skip",
|
|
1294
|
+
"distill_q/llm-fail/qrej/cfg/skip",
|
|
1295
|
+
"cons_proc/promo/merge/del",
|
|
1296
|
+
"mem_cons/written/skip",
|
|
1297
|
+
"graph_f/e/r",
|
|
1298
|
+
"orphans",
|
|
1299
|
+
"lint_f/fl",
|
|
1300
|
+
];
|
|
1301
|
+
const rows = runs.map((r) => {
|
|
1302
|
+
const totalActions = r.actions.reflect.ok +
|
|
1303
|
+
r.actions.reflect.failed +
|
|
1304
|
+
r.actions.reflect.cooldown +
|
|
1305
|
+
r.actions.reflect.skipped +
|
|
1306
|
+
r.actions.distill.queued +
|
|
1307
|
+
r.actions.distill.llmFailed +
|
|
1308
|
+
r.actions.distill.qualityRejected +
|
|
1309
|
+
r.actions.distill.configDisabled +
|
|
1310
|
+
r.actions.distill.skipped +
|
|
1311
|
+
r.actions.memoryPrune +
|
|
1312
|
+
r.actions.memoryInference +
|
|
1313
|
+
r.actions.graphExtraction +
|
|
1314
|
+
r.actions.error;
|
|
1315
|
+
return [
|
|
1316
|
+
r.startedAt,
|
|
1317
|
+
String(r.ok),
|
|
1318
|
+
String(totalActions),
|
|
1319
|
+
`${r.actions.reflect.ok}/${r.actions.reflect.failed}/${r.actions.reflect.cooldown}/${r.actions.reflect.skipped}`,
|
|
1320
|
+
`${r.actions.distill.queued}/${r.actions.distill.llmFailed}/${r.actions.distill.qualityRejected}/${r.actions.distill.configDisabled}/${r.actions.distill.skipped}`,
|
|
1321
|
+
`${r.consolidation.processed}/${r.consolidation.promoted}/${r.consolidation.merged}/${r.consolidation.deleted}`,
|
|
1322
|
+
`${r.memoryInference.considered}/${r.memoryInference.written}/${r.memoryInference.skippedNoFacts}`,
|
|
1323
|
+
`${r.graphExtraction.extractedFiles}/${r.graphExtraction.entities}/${r.graphExtraction.relations}`,
|
|
1324
|
+
String(r.orphansPurged),
|
|
1325
|
+
`${r.lintFixed}/${r.lintFlagged}`,
|
|
1326
|
+
];
|
|
1327
|
+
});
|
|
1328
|
+
return renderTable(headers, rows);
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Render a window-compare comparison as a side-by-side metric table with a
|
|
1332
|
+
* delta column. Bad-direction deltas (e.g. +pct on failed counts) get a `!`
|
|
1333
|
+
* marker prefix.
|
|
1334
|
+
*/
|
|
1335
|
+
export function renderWindowCompareMd(windows, deltas) {
|
|
1336
|
+
if (windows.length === 0)
|
|
1337
|
+
return "";
|
|
1338
|
+
const headers = ["metric", ...windows.map((w) => w.name), "delta"];
|
|
1339
|
+
const badIfPositive = new Set([
|
|
1340
|
+
"improve.actions.reflect.failed",
|
|
1341
|
+
"improve.actions.distill.llmFailed",
|
|
1342
|
+
"improve.graphExtraction.failures",
|
|
1343
|
+
"improve.wallTime.medianMs",
|
|
1344
|
+
"improve.wallTime.p95Ms",
|
|
1345
|
+
"improve.memoryInference.skippedNoFacts",
|
|
1346
|
+
]);
|
|
1347
|
+
const rows = [];
|
|
1348
|
+
for (const path of INTERESTING_DELTA_PATHS) {
|
|
1349
|
+
const values = windows.map((w) => String(readNumericPath(w, path)));
|
|
1350
|
+
const delta = deltas?.[path];
|
|
1351
|
+
let deltaStr = "—";
|
|
1352
|
+
if (delta) {
|
|
1353
|
+
const pct = delta.pctChange;
|
|
1354
|
+
const num = typeof pct === "number" ? pct : pct;
|
|
1355
|
+
const sign = typeof num === "number" && num > 0 ? "+" : "";
|
|
1356
|
+
const formatted = typeof num === "number" ? `${sign}${num}%` : String(num);
|
|
1357
|
+
const marker = badIfPositive.has(path) && typeof num === "number" && num > 0 ? "!" : "";
|
|
1358
|
+
deltaStr = marker + formatted;
|
|
1359
|
+
}
|
|
1360
|
+
rows.push([path, ...values, deltaStr]);
|
|
1361
|
+
}
|
|
1362
|
+
return renderTable(headers, rows);
|
|
1363
|
+
}
|