akm-cli 0.9.0-beta.52 → 0.9.0-beta.54
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/dist/assets/hints/cli-hints-full.md +6 -5
- package/dist/cli/clack.js +56 -0
- package/dist/cli/confirm.js +1 -1
- package/dist/cli.js +0 -7
- package/dist/commands/env/env-cli.js +3 -2
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/health/checks.js +28 -15
- package/dist/commands/health/html-report.js +33 -10
- package/dist/commands/health.js +222 -22
- package/dist/commands/improve/collapse-detector.js +419 -0
- package/dist/commands/improve/consolidate.js +72 -54
- package/dist/commands/improve/distill.js +79 -13
- package/dist/commands/improve/extract.js +13 -6
- package/dist/commands/improve/homeostatic.js +109 -79
- package/dist/commands/improve/improve-cli.js +67 -1
- package/dist/commands/improve/improve.js +10 -0
- package/dist/commands/improve/loop-stages.js +39 -1
- package/dist/commands/improve/outcome-loop.js +33 -19
- package/dist/commands/improve/preparation.js +36 -11
- package/dist/commands/improve/salience.js +49 -32
- package/dist/commands/read/curate.js +9 -13
- package/dist/commands/read/knowledge.js +4 -0
- package/dist/commands/read/search-cli.js +6 -4
- package/dist/commands/read/search.js +12 -5
- package/dist/commands/read/show.js +6 -8
- package/dist/commands/sources/add-cli.js +1 -1
- package/dist/commands/sources/init.js +12 -0
- package/dist/commands/sources/stash-cli.js +1 -1
- package/dist/commands/tasks/default-tasks.js +12 -0
- package/dist/core/asset/asset-spec.js +3 -2
- package/dist/core/config/config-schema.js +39 -17
- package/dist/core/config/config.js +12 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/state/migrations.js +56 -0
- package/dist/core/state-db.js +146 -19
- package/dist/core/warn.js +21 -0
- package/dist/indexer/db/db.js +6 -0
- package/dist/indexer/ensure-index.js +36 -92
- package/dist/indexer/index-writer-lock.js +9 -11
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +16 -4
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +29 -1
- package/dist/indexer/search/ranking-contributors.js +33 -1
- package/dist/indexer/search/ranking.js +66 -0
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +1 -1
- package/dist/llm/client.js +12 -0
- package/dist/llm/embedder.js +26 -2
- package/dist/llm/embedders/local.js +7 -1
- package/dist/llm/feature-gate.js +6 -2
- package/dist/output/renderers.js +8 -13
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/scripts/migrate-storage.js +178 -35
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +46 -19
- package/dist/setup/detect.js +9 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/setup.js +1 -1
- package/dist/storage/repositories/index-db.js +10 -1
- package/dist/tasks/backends/index.js +9 -0
- package/dist/tasks/runner.js +9 -0
- package/package.json +2 -4
|
@@ -58,7 +58,7 @@ akm show knowledge:my-doc # Show content (local or remote)
|
|
|
58
58
|
| knowledge | `content` (with view modes: `full`, `toc`, `frontmatter`, `section`, `lines`) |
|
|
59
59
|
| workflow | `workflowTitle`, `workflowParameters`, `steps` |
|
|
60
60
|
| memory | `content` (recalled context) |
|
|
61
|
-
| env | `keys
|
|
61
|
+
| env | `keys` (key names only — values and comment text never returned) |
|
|
62
62
|
| secret | `name` only (the whole file is the value — never returned) |
|
|
63
63
|
| wiki | `content` (same view modes as knowledge). For any wiki task, run `akm wiki list`. To ingest sources, `akm wiki ingest <name>` dispatches the configured agent (defaults.agent or `--profile`) to execute the ingest workflow. |
|
|
64
64
|
|
|
@@ -122,15 +122,16 @@ search results. No `--llm` anywhere — akm never reasons about page content.
|
|
|
122
122
|
## Env files
|
|
123
123
|
|
|
124
124
|
A group of related CONFIGURATION for an app/service in one `.env` file at
|
|
125
|
-
`<stashDir>/env/<name>.env`, sourced/injected wholesale. Key names
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
`<stashDir>/env/<name>.env`, sourced/injected wholesale. Key names are
|
|
126
|
+
discoverable; values and comment text stay on disk and never reach stdout or
|
|
127
|
+
the index (comments can contain commented-out credentials). akm does not edit
|
|
128
|
+
entries — you edit the file with your own editor and akm loads it.
|
|
128
129
|
|
|
129
130
|
```sh
|
|
130
131
|
akm env create prod # Create an empty env file
|
|
131
132
|
akm env create prod --from-file ./.env # Ingest an existing .env
|
|
132
133
|
akm env list # List all env files across stashes with key names
|
|
133
|
-
akm show env:prod # Inspect key names
|
|
134
|
+
akm show env:prod # Inspect key names (never values or comments)
|
|
134
135
|
akm env run env:prod -- ./deploy.sh # Run a command with the whole .env injected (the safe path)
|
|
135
136
|
akm env run env:prod -- $SHELL # Open an interactive shell with values injected
|
|
136
137
|
akm env export env:prod --out ./env.sh # Write a sourceable script to a file (mode 0600)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { cancel as realCancel, confirm as realConfirm, intro as realIntro, isCancel as realIsCancel, log as realLog, multiselect as realMultiselect, note as realNote, outro as realOutro, select as realSelect, spinner as realSpinner, text as realText, } from "@clack/prompts";
|
|
5
|
+
// ── Test seam ────────────────────────────────────────────────────────────────
|
|
6
|
+
// Swap-and-restore override. Inert in production; only tests call the setter
|
|
7
|
+
// (via tests/_helpers/seams.ts `overrideSeam`, never directly).
|
|
8
|
+
let clackFake;
|
|
9
|
+
/** TEST-ONLY. Swap the clack prompt surface; pass undefined to restore. */
|
|
10
|
+
export function _setClackForTests(fake) {
|
|
11
|
+
clackFake = fake;
|
|
12
|
+
}
|
|
13
|
+
const realFns = {
|
|
14
|
+
intro: realIntro,
|
|
15
|
+
outro: realOutro,
|
|
16
|
+
cancel: realCancel,
|
|
17
|
+
confirm: realConfirm,
|
|
18
|
+
select: realSelect,
|
|
19
|
+
multiselect: realMultiselect,
|
|
20
|
+
text: realText,
|
|
21
|
+
spinner: realSpinner,
|
|
22
|
+
note: realNote,
|
|
23
|
+
isCancel: realIsCancel,
|
|
24
|
+
};
|
|
25
|
+
/** Delegator with the real export's exact type; reads the fake at call time. */
|
|
26
|
+
function bind(name) {
|
|
27
|
+
return ((...args) => {
|
|
28
|
+
const impl = (clackFake?.[name] ?? realFns[name]);
|
|
29
|
+
return impl(...args);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function bindLog(name) {
|
|
33
|
+
return ((...args) => {
|
|
34
|
+
const impl = (clackFake?.log?.[name] ?? realLog[name]);
|
|
35
|
+
return impl(...args);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export const intro = bind("intro");
|
|
39
|
+
export const outro = bind("outro");
|
|
40
|
+
export const cancel = bind("cancel");
|
|
41
|
+
export const confirm = bind("confirm");
|
|
42
|
+
export const select = bind("select");
|
|
43
|
+
export const multiselect = bind("multiselect");
|
|
44
|
+
export const text = bind("text");
|
|
45
|
+
export const spinner = bind("spinner");
|
|
46
|
+
export const note = bind("note");
|
|
47
|
+
export const isCancel = bind("isCancel");
|
|
48
|
+
export const log = {
|
|
49
|
+
message: bindLog("message"),
|
|
50
|
+
info: bindLog("info"),
|
|
51
|
+
success: bindLog("success"),
|
|
52
|
+
step: bindLog("step"),
|
|
53
|
+
warn: bindLog("warn"),
|
|
54
|
+
warning: bindLog("warning"),
|
|
55
|
+
error: bindLog("error"),
|
|
56
|
+
};
|
package/dist/cli/confirm.js
CHANGED
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
* `--quiet` NEVER suppresses the confirmation prompt — it is safety-critical
|
|
36
36
|
* output. The auto-migration banner is similarly exempt from `--quiet`.
|
|
37
37
|
*/
|
|
38
|
-
import * as p from "@clack/prompts";
|
|
39
38
|
import { UsageError } from "../core/errors.js";
|
|
39
|
+
import * as p from "./clack.js";
|
|
40
40
|
/**
|
|
41
41
|
* Prompt the user to confirm a destructive action.
|
|
42
42
|
*
|
package/dist/cli.js
CHANGED
|
@@ -544,13 +544,6 @@ const EXIT_HEALTH_WARN = EXIT_CODES.HEALTH_WARN;
|
|
|
544
544
|
// The wrapper sets `AKM_NODE_ENTRY=1` to opt into the startup block. The test
|
|
545
545
|
// harness never sets it, so importing cli.ts under Bun stays inert as before.
|
|
546
546
|
if (import.meta.main || process.env.AKM_NODE_ENTRY === "1") {
|
|
547
|
-
// Mark that this process is the real akm CLI: its `process.argv[1]` is the
|
|
548
|
-
// akm entrypoint, so the background auto-reindex may safely re-invoke it as a
|
|
549
|
-
// detached child. Hosts that merely import this module (the in-process test
|
|
550
|
-
// harness, library embeddings) never reach this block, so they fall back to
|
|
551
|
-
// an inline reindex instead of spawning the wrong program. See
|
|
552
|
-
// `ensureIndex` in src/indexer/ensure-index.ts.
|
|
553
|
-
process.env.AKM_CLI_ENTRY = "1";
|
|
554
547
|
// citty reads process.argv directly and does not accept a custom argv array,
|
|
555
548
|
// so we must replace process.argv with the normalized version before runMain.
|
|
556
549
|
process.argv = normalizeShowArgv(process.argv);
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* copy.
|
|
11
11
|
*
|
|
12
12
|
* `akm env` manages whole `.env` files under each stash's env/ directory.
|
|
13
|
-
* Values are NEVER written to stdout or structured output —
|
|
14
|
-
*
|
|
13
|
+
* Values and comment text are NEVER written to stdout or structured output —
|
|
14
|
+
* only key NAMES are surfaced (comments routinely contain commented-out
|
|
15
|
+
* credentials). akm does not manage individual entries;
|
|
15
16
|
* you edit the `.env` file yourself and akm loads it. Replaced the deprecated
|
|
16
17
|
* `vault` type (removed in 0.9.0).
|
|
17
18
|
*/
|
package/dist/commands/env/env.js
CHANGED
|
@@ -18,10 +18,13 @@
|
|
|
18
18
|
* round-trip through `dotenv`; the shell-load safety guarantee still lives on
|
|
19
19
|
* the READ path (see `buildShellExportScript` + `akm env export`).
|
|
20
20
|
*
|
|
21
|
-
* Invariant: env
|
|
22
|
-
* indexer, the `akm show` renderer, or any
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* Invariant: nothing from an env file except key NAMES may be written to
|
|
22
|
+
* stdout, returned through the indexer, the `akm show` renderer, or any
|
|
23
|
+
* structured output channel. Key NAMES are surfaced for discoverability;
|
|
24
|
+
* comment text is NOT — real .env files routinely carry commented-out
|
|
25
|
+
* `KEY=value` lines and free-text notes containing live credentials, so
|
|
26
|
+
* comments are treated exactly like values. The supported value-load paths
|
|
27
|
+
* are:
|
|
25
28
|
*
|
|
26
29
|
* - `akm env run <ref> -- <command>` — values injected into the child
|
|
27
30
|
* process env (never via a shell), see `injectIntoEnv` / `loadEnv`. This is
|
|
@@ -71,75 +74,19 @@ function scanKeys(text) {
|
|
|
71
74
|
return keys;
|
|
72
75
|
}
|
|
73
76
|
/**
|
|
74
|
-
*
|
|
75
|
-
* any leading whitespace stripped). Inline/trailing `#` after an assignment is
|
|
76
|
-
* never extracted.
|
|
77
|
-
*/
|
|
78
|
-
function scanComments(text) {
|
|
79
|
-
const comments = [];
|
|
80
|
-
for (const line of text.split(/\r?\n/)) {
|
|
81
|
-
const trimmed = line.trimStart();
|
|
82
|
-
if (trimmed.startsWith("#")) {
|
|
83
|
-
comments.push(trimmed.slice(1).trimStart());
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return comments;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Read and return ONLY non-secret metadata (keys + start-of-line comments).
|
|
77
|
+
* Read and return ONLY non-secret metadata: key names.
|
|
90
78
|
*
|
|
91
79
|
* The function reads the whole file into memory (same as any dotenv parser)
|
|
92
|
-
* but deliberately does not parse values — the LHS-only regex
|
|
93
|
-
*
|
|
94
|
-
*
|
|
80
|
+
* but deliberately does not parse values — the LHS-only regex scanner above
|
|
81
|
+
* ensures no value content is retained or returned. Comment text is never
|
|
82
|
+
* returned either: comments routinely contain commented-out `KEY=value`
|
|
83
|
+
* credentials and free-text secrets, so they never leave this function.
|
|
95
84
|
*/
|
|
96
85
|
export function listKeys(envPath) {
|
|
97
86
|
if (!fs.existsSync(envPath))
|
|
98
|
-
return { keys: []
|
|
99
|
-
const text = fs.readFileSync(envPath, "utf8");
|
|
100
|
-
return { keys: scanKeys(text), comments: scanComments(text) };
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Return structured `entries` pairing each key with the nearest preceding
|
|
104
|
-
* comment line (if any). This is an easier-to-consume shape than the parallel
|
|
105
|
-
* `keys[]` + `comments[]` of `listKeys` (QA #35).
|
|
106
|
-
*
|
|
107
|
-
* Values are never included — the same privacy guarantee as `listKeys`.
|
|
108
|
-
*/
|
|
109
|
-
export function listEntries(envPath) {
|
|
110
|
-
if (!fs.existsSync(envPath))
|
|
111
|
-
return [];
|
|
87
|
+
return { keys: [] };
|
|
112
88
|
const text = fs.readFileSync(envPath, "utf8");
|
|
113
|
-
|
|
114
|
-
const seen = new Set();
|
|
115
|
-
const entries = [];
|
|
116
|
-
let pendingComment;
|
|
117
|
-
for (const line of lines) {
|
|
118
|
-
const trimmed = line.trimStart();
|
|
119
|
-
if (trimmed.startsWith("#")) {
|
|
120
|
-
// Capture the most recent comment before a key
|
|
121
|
-
pendingComment = trimmed.slice(1).trimStart() || undefined;
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
const m = line.match(ASSIGN_RE);
|
|
125
|
-
if (m) {
|
|
126
|
-
const key = m[1];
|
|
127
|
-
if (!seen.has(key)) {
|
|
128
|
-
seen.add(key);
|
|
129
|
-
const entry = { key };
|
|
130
|
-
if (pendingComment)
|
|
131
|
-
entry.comment = pendingComment;
|
|
132
|
-
entries.push(entry);
|
|
133
|
-
}
|
|
134
|
-
pendingComment = undefined;
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
// Any non-comment, non-assignment line (including blank lines)
|
|
138
|
-
// breaks "nearest preceding comment line" association.
|
|
139
|
-
pendingComment = undefined;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return entries;
|
|
89
|
+
return { keys: scanKeys(text) };
|
|
143
90
|
}
|
|
144
91
|
/**
|
|
145
92
|
* Read all KEY=value pairs from an env file. Intended for programmatic callers
|
|
@@ -191,21 +191,34 @@ export const HEALTH_CHECKS = [
|
|
|
191
191
|
{
|
|
192
192
|
name: "semantic-search-runtime",
|
|
193
193
|
channel: "advisory",
|
|
194
|
-
run: (ctx) =>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
194
|
+
run: (ctx) => {
|
|
195
|
+
const blocked = ctx.semanticStatus?.status === "blocked";
|
|
196
|
+
// The generic "status: blocked" line is not actionable when the real
|
|
197
|
+
// problem is a configured remote embedding endpoint that is down while
|
|
198
|
+
// semanticSearchMode leaves semantic search enabled — every index run
|
|
199
|
+
// burns time failing against it and searches silently degrade to
|
|
200
|
+
// keyword-only. Name the endpoint and the two ways out.
|
|
201
|
+
const remoteReason = ctx.semanticStatus?.reason?.startsWith("remote-") === true;
|
|
202
|
+
const endpointAdvisory = blocked && remoteReason && ctx.embeddingEndpoint
|
|
203
|
+
? `Configured embedding endpoint ${ctx.embeddingEndpoint} is failing ` +
|
|
204
|
+
`(${ctx.semanticStatus?.reason}${ctx.semanticStatus?.message ? `: ${ctx.semanticStatus.message}` : ""}) ` +
|
|
205
|
+
`while semanticSearchMode is "${ctx.semanticSearchMode ?? "auto"}". Searches fall back to keyword-only. ` +
|
|
206
|
+
`Restore the endpoint, or set semanticSearchMode to "off" (or remove embedding.endpoint to use the local model).`
|
|
207
|
+
: undefined;
|
|
208
|
+
return {
|
|
209
|
+
name: "semantic-search-runtime",
|
|
210
|
+
kind: "deterministic",
|
|
211
|
+
status: !ctx.semanticStatus || !blocked ? "pass" : "warn",
|
|
212
|
+
confidence: "medium",
|
|
213
|
+
message: endpointAdvisory ??
|
|
214
|
+
(ctx.semanticStatus
|
|
215
|
+
? `Semantic search status: ${ctx.semanticStatus.status}`
|
|
216
|
+
: "No semantic-search runtime status recorded yet."),
|
|
217
|
+
evidence: ctx.semanticStatus
|
|
218
|
+
? { ...ctx.semanticStatus, ...(ctx.embeddingEndpoint ? { embeddingEndpoint: ctx.embeddingEndpoint } : {}) }
|
|
219
|
+
: undefined,
|
|
220
|
+
};
|
|
221
|
+
},
|
|
209
222
|
},
|
|
210
223
|
{
|
|
211
224
|
// session-log-failures: demoted to informational — the ERROR_PATTERNS regex
|
|
@@ -282,6 +282,7 @@ export function buildHealthHtmlReplacements(result, opts) {
|
|
|
282
282
|
};
|
|
283
283
|
const coverage = improve.coverage;
|
|
284
284
|
const degradation = improve.degradation;
|
|
285
|
+
const minting = improve.enrichmentMinting;
|
|
285
286
|
// #576: real per-stage LLM token/time accounting (replaces the GPU-time
|
|
286
287
|
// proxy). Optional-guarded so reports built from older health JSON without
|
|
287
288
|
// the aggregate still render.
|
|
@@ -557,7 +558,7 @@ export function buildHealthHtmlReplacements(result, opts) {
|
|
|
557
558
|
"Coverage rate",
|
|
558
559
|
pct(coverage.rate, 1),
|
|
559
560
|
"flat",
|
|
560
|
-
"
|
|
561
|
+
"Distinct accepted refs / total stash assets (denominator-fixed). Shows what fraction of the corpus has been touched.",
|
|
561
562
|
], [
|
|
562
563
|
"Eligible fraction",
|
|
563
564
|
pct(coverage.eligibleFraction, 1),
|
|
@@ -566,8 +567,22 @@ export function buildHealthHtmlReplacements(result, opts) {
|
|
|
566
567
|
], [
|
|
567
568
|
"Coverage accepted",
|
|
568
569
|
num(coverage.acceptedProposals),
|
|
569
|
-
"
|
|
570
|
-
"Total accepted proposals
|
|
570
|
+
"flat",
|
|
571
|
+
"Total accepted proposals in the window (raw volume — includes repeated rewrites of the same asset).",
|
|
572
|
+
], [
|
|
573
|
+
"Churn ratio",
|
|
574
|
+
Number.isFinite(coverage.churnRatio) ? num(coverage.churnRatio) : "—",
|
|
575
|
+
Number.isFinite(coverage.churnRatio) && coverage.churnRatio > 1.5 ? "down" : "flat",
|
|
576
|
+
"Accepted proposals / distinct refs touched. >1.5 = the loop is repeatedly rewriting the same assets (churn, not coverage).",
|
|
577
|
+
]);
|
|
578
|
+
}
|
|
579
|
+
// Enrichment-vs-minting policy rollup (reporting-only).
|
|
580
|
+
if (minting && Number.isFinite(minting.share)) {
|
|
581
|
+
summaryRows.push([
|
|
582
|
+
"Enrichment-lane minted share",
|
|
583
|
+
pct(minting.share, 1),
|
|
584
|
+
minting.share > 0.05 ? "down" : "flat",
|
|
585
|
+
`New assets minted by enrichment lanes / their accepted total (${minting.minted} minted vs ${minting.updated} updated). Enrichment lanes are ratified to edit existing assets only; WARN >5%, FAIL >15%.`,
|
|
571
586
|
]);
|
|
572
587
|
}
|
|
573
588
|
// WS-5: perf telemetry rows (only when at least one run reported telemetry).
|
|
@@ -606,18 +621,13 @@ export function buildHealthHtmlReplacements(result, opts) {
|
|
|
606
621
|
summaryRows.push([
|
|
607
622
|
"Corpus diversity (Gini)",
|
|
608
623
|
num(degradation.corpusCentroidDistance),
|
|
609
|
-
degradation.entrenchmentFlagged ? "down" : "flat",
|
|
610
|
-
"Gini coefficient of retrieval_salience for top-100 ranked assets.
|
|
624
|
+
degradation.entrenchmentFlagged || degradation.salienceUniformityFlagged ? "down" : "flat",
|
|
625
|
+
"Gini coefficient of retrieval_salience for top-100 ranked assets. Two-tailed: >0.35 = entrenchment risk; <0.08 = collapsed toward uniform (ranking no longer discriminates).",
|
|
611
626
|
], [
|
|
612
627
|
"Merge fidelity contradiction rate",
|
|
613
628
|
pct(degradation.mergeFidelityContradictionRate, 1),
|
|
614
629
|
"flat",
|
|
615
630
|
"Fraction of consolidated proposals that involved a contradiction, from consolidation result envelopes.",
|
|
616
|
-
], [
|
|
617
|
-
"High-generation fraction",
|
|
618
|
-
pct(degradation.highGenerationFraction, 1),
|
|
619
|
-
"flat",
|
|
620
|
-
"Fraction of assets with consecutive_no_ops >= 2 (proxy for high-generation assets in the salience table).",
|
|
621
631
|
]);
|
|
622
632
|
}
|
|
623
633
|
const summaryRowsHtml = summaryRows
|
|
@@ -726,6 +736,19 @@ export function buildHealthHtmlReplacements(result, opts) {
|
|
|
726
736
|
remedy: "akm health --format json | jq '.improve.degradation'",
|
|
727
737
|
});
|
|
728
738
|
}
|
|
739
|
+
// Low-tail companion: salience distribution collapsed toward uniform.
|
|
740
|
+
if (degradation?.salienceUniformityFlagged) {
|
|
741
|
+
pushItem({
|
|
742
|
+
key: "salience-uniformity-collapse",
|
|
743
|
+
prio: "P2",
|
|
744
|
+
cls: "warn",
|
|
745
|
+
title: "Salience distribution collapsed: retrieval_salience Gini < 0.08",
|
|
746
|
+
descHtml: "The top-100 salience scores are near-uniform (uniform baseline ≈ 0.1) — " +
|
|
747
|
+
"ranking currently carries little to no discrimination between assets. " +
|
|
748
|
+
`Corpus diversity proxy: ${esc(String(degradation.corpusCentroidDistance))}.`,
|
|
749
|
+
remedy: "akm health --format json | jq '.improve.degradation'",
|
|
750
|
+
});
|
|
751
|
+
}
|
|
729
752
|
// WS-5: over-budget consolidation advisory.
|
|
730
753
|
if (perf.overBudgetRuns > 0) {
|
|
731
754
|
pushItem({
|