akm-cli 0.9.0-beta.6 → 0.9.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +663 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/help/help-improve.md +9 -6
- package/dist/assets/hints/cli-hints-full.md +6 -5
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/proactive-maintenance.json +25 -0
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/recombine-only.json +21 -0
- package/dist/assets/profiles/reflect-distill.json +30 -0
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +11 -3
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +45 -16
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/clack.js +56 -0
- package/dist/cli/config-migrate.js +7 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +25 -14
- package/dist/commands/agent/agent-dispatch.js +3 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +26 -7
- package/dist/commands/config-cli.js +26 -13
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +220 -227
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/env/secret-cli.js +140 -138
- package/dist/commands/feedback-cli.js +153 -147
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +76 -72
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/checks.js +103 -16
- package/dist/commands/health/html-report.js +447 -81
- package/dist/commands/health/improve-metrics.js +771 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/surfaces.js +216 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +26 -0
- package/dist/commands/health/windows.js +195 -0
- package/dist/commands/health.js +91 -1083
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/collapse-detector.js +421 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +1313 -1278
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +229 -0
- package/dist/commands/improve/distill/quality-gate.js +236 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +243 -599
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +179 -59
- package/dist/commands/improve/extract-prompt.js +55 -4
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +409 -43
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +160 -7
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +32 -8
- package/dist/commands/improve/improve-result-file.js +15 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +510 -2537
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1100 -0
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/outcome-loop.js +270 -0
- package/dist/commands/improve/preparation.js +2002 -0
- package/dist/commands/improve/proactive-maintenance.js +115 -0
- package/dist/commands/improve/procedural.js +398 -0
- package/dist/commands/improve/recombine.js +818 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +212 -45
- package/dist/commands/improve/salience.js +455 -0
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +51 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +38 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +13 -4
- package/dist/commands/proposal/drain.js +45 -51
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +24 -34
- package/dist/commands/proposal/proposal.js +7 -1
- package/dist/commands/proposal/propose.js +8 -3
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +93 -882
- package/dist/commands/read/curate.js +419 -103
- package/dist/commands/read/knowledge.js +10 -3
- package/dist/commands/read/remember-cli.js +133 -138
- package/dist/commands/read/search-cli.js +15 -8
- package/dist/commands/read/search.js +22 -11
- package/dist/commands/read/show.js +106 -14
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +66 -18
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +44 -46
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +29 -41
- package/dist/commands/sources/stash-skeleton.js +57 -8
- package/dist/commands/tasks/default-tasks.js +15 -2
- package/dist/commands/tasks/tasks-cli.js +20 -29
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +23 -38
- package/dist/commands/workflow-cli.js +15 -1
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +21 -4
- package/dist/core/asset/frontmatter.js +188 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +4 -23
- package/dist/core/concurrent.js +10 -1
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +18 -40
- package/dist/core/config/config-schema.js +389 -58
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +67 -22
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/errors.js +1 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/events.js +4 -7
- package/dist/core/improve-types.js +47 -8
- package/dist/core/logs-db.js +14 -75
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +21 -18
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +770 -0
- package/dist/core/state-db.js +142 -1091
- package/dist/core/structured.js +69 -0
- package/dist/core/time.js +53 -0
- package/dist/core/warn.js +21 -0
- package/dist/core/write-source.js +37 -0
- package/dist/indexer/db/db.js +356 -780
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +103 -24
- package/dist/indexer/feedback/utility-policy.js +75 -0
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +207 -4
- package/dist/indexer/index-writer-lock.js +106 -0
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +291 -310
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +89 -13
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/search/ranking-contributors.js +95 -9
- package/dist/indexer/search/ranking.js +79 -3
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/search/search-source.js +32 -21
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +39 -13
- package/dist/integrations/agent/config.js +20 -59
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +3 -19
- package/dist/integrations/agent/model-aliases.js +7 -2
- package/dist/integrations/agent/profiles.js +7 -1
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +69 -67
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
- package/dist/integrations/harnesses/claude/index.js +2 -0
- package/dist/integrations/harnesses/claude/session-log.js +11 -1
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +2 -0
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
- package/dist/integrations/harnesses/types.js +1 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/call-ai.js +2 -2
- package/dist/llm/client.js +57 -15
- package/dist/llm/embedder.js +67 -4
- package/dist/llm/embedders/cache.js +3 -1
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +73 -3
- package/dist/llm/feature-gate.js +16 -15
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +85 -14
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/text/helpers.js +29 -1
- package/dist/output/text/workflow.js +1 -0
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +27 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2718 -2354
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +891 -597
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +52 -1614
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +146 -0
- package/dist/storage/repositories/index-db.js +14 -8
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/registry-cache.js +93 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/index.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +15 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/cli.js +1 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +43 -118
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +5 -4
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +10 -10
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
- package/dist/indexer/passes/staleness-detect.js +0 -488
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Aggregate `llm_usage` events (#576) into the window total + per-stage
|
|
6
|
+
* breakdown reported by `akm health`.
|
|
7
|
+
*/
|
|
8
|
+
import { readEvents } from "../../core/events.js";
|
|
9
|
+
import { LLM_USAGE_EVENT } from "../../llm/usage-persist.js";
|
|
10
|
+
import { toFiniteNumber } from "./improve-metrics.js";
|
|
11
|
+
/** Stage key used for `llm_usage` events recorded outside any stage scope. */
|
|
12
|
+
const UNATTRIBUTED_STAGE = "unattributed";
|
|
13
|
+
function emptyLlmUsageStageAggregate() {
|
|
14
|
+
return {
|
|
15
|
+
calls: 0,
|
|
16
|
+
totalDurationMs: 0,
|
|
17
|
+
promptTokens: 0,
|
|
18
|
+
completionTokens: 0,
|
|
19
|
+
totalTokens: 0,
|
|
20
|
+
reasoningTokens: 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function emptyLlmUsageAggregate() {
|
|
24
|
+
return { ...emptyLlmUsageStageAggregate(), byStage: {} };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Aggregate `llm_usage` events (#576) into a window total plus a per-stage
|
|
28
|
+
* breakdown of call count, wall-time, and token usage. Token fields absent from
|
|
29
|
+
* a best-effort record contribute 0. Calls with no `stage` land under
|
|
30
|
+
* {@link UNATTRIBUTED_STAGE}.
|
|
31
|
+
*/
|
|
32
|
+
export function summarizeLlmUsage(events) {
|
|
33
|
+
const aggregate = emptyLlmUsageAggregate();
|
|
34
|
+
for (const event of events) {
|
|
35
|
+
const meta = event.metadata ?? {};
|
|
36
|
+
const stageKey = typeof meta.stage === "string" && meta.stage ? meta.stage : UNATTRIBUTED_STAGE;
|
|
37
|
+
let stage = aggregate.byStage[stageKey];
|
|
38
|
+
if (!stage) {
|
|
39
|
+
stage = emptyLlmUsageStageAggregate();
|
|
40
|
+
aggregate.byStage[stageKey] = stage;
|
|
41
|
+
}
|
|
42
|
+
const durationMs = toFiniteNumber(meta.durationMs);
|
|
43
|
+
const promptTokens = toFiniteNumber(meta.promptTokens);
|
|
44
|
+
const completionTokens = toFiniteNumber(meta.completionTokens);
|
|
45
|
+
const totalTokens = toFiniteNumber(meta.totalTokens);
|
|
46
|
+
const reasoningTokens = toFiniteNumber(meta.reasoningTokens);
|
|
47
|
+
for (const target of [aggregate, stage]) {
|
|
48
|
+
target.calls += 1;
|
|
49
|
+
target.totalDurationMs += durationMs;
|
|
50
|
+
target.promptTokens += promptTokens;
|
|
51
|
+
target.completionTokens += completionTokens;
|
|
52
|
+
target.totalTokens += totalTokens;
|
|
53
|
+
target.reasoningTokens += reasoningTokens;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return aggregate;
|
|
57
|
+
}
|
|
58
|
+
export function readLlmUsageAggregate(stateDbPath, since, until) {
|
|
59
|
+
const events = readEvents({ since, type: LLM_USAGE_EVENT }, { dbPath: stateDbPath }).events.filter((event) => {
|
|
60
|
+
if (until === undefined)
|
|
61
|
+
return true;
|
|
62
|
+
return new Date(event.ts ?? since).getTime() < new Date(until).getTime();
|
|
63
|
+
});
|
|
64
|
+
return summarizeLlmUsage(events);
|
|
65
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
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 { INTERESTING_DELTA_PATHS, readNumericPath } from "./windows.js";
|
|
5
|
+
function padRight(s, width) {
|
|
6
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
7
|
+
}
|
|
8
|
+
function renderTable(headers, rows) {
|
|
9
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
10
|
+
const lines = [];
|
|
11
|
+
lines.push(headers.map((h, i) => padRight(h, widths[i] ?? 0)).join(" "));
|
|
12
|
+
for (const row of rows) {
|
|
13
|
+
lines.push(row.map((cell, i) => padRight(cell ?? "", widths[i] ?? 0)).join(" "));
|
|
14
|
+
}
|
|
15
|
+
return lines.join("\n");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Render `--detail per-run` rows as a TSV-ish aligned table. The column
|
|
19
|
+
* shape was originally inherited from the retired
|
|
20
|
+
* `scripts/improve-stats/runs-detail` bash helper; keep the same shape
|
|
21
|
+
* so operator muscle memory carries over.
|
|
22
|
+
*
|
|
23
|
+
* Columns: ts | ok | actions | refl_ok/fail/cd/skip |
|
|
24
|
+
* distill_q/llm-fail/qrej/cfg/skip | cons_proc/promo/merge/del |
|
|
25
|
+
* mem_cons/written/skip | graph_f/e/r | orphans | lint_f/fl
|
|
26
|
+
*/
|
|
27
|
+
export function renderRunsDetailMd(runs) {
|
|
28
|
+
const headers = [
|
|
29
|
+
"ts",
|
|
30
|
+
"ok",
|
|
31
|
+
"actions",
|
|
32
|
+
"refl_ok/fail/cd/skip",
|
|
33
|
+
"distill_q/llm-fail/qrej/cfg/skip",
|
|
34
|
+
"cons_proc/promo/merge/del",
|
|
35
|
+
"mem_cons/written/skip",
|
|
36
|
+
"graph_f/e/r",
|
|
37
|
+
"orphans",
|
|
38
|
+
"lint_f/fl",
|
|
39
|
+
];
|
|
40
|
+
const rows = runs.map((r) => {
|
|
41
|
+
const totalActions = r.actions.reflect.ok +
|
|
42
|
+
r.actions.reflect.failed +
|
|
43
|
+
r.actions.reflect.cooldown +
|
|
44
|
+
r.actions.reflect.skipped +
|
|
45
|
+
r.actions.distill.queued +
|
|
46
|
+
r.actions.distill.llmFailed +
|
|
47
|
+
r.actions.distill.qualityRejected +
|
|
48
|
+
r.actions.distill.configDisabled +
|
|
49
|
+
r.actions.distill.skipped +
|
|
50
|
+
r.actions.memoryPrune +
|
|
51
|
+
r.actions.memoryInference +
|
|
52
|
+
r.actions.graphExtraction +
|
|
53
|
+
r.actions.error;
|
|
54
|
+
return [
|
|
55
|
+
r.startedAt,
|
|
56
|
+
String(r.ok),
|
|
57
|
+
String(totalActions),
|
|
58
|
+
`${r.actions.reflect.ok}/${r.actions.reflect.failed}/${r.actions.reflect.cooldown}/${r.actions.reflect.skipped}`,
|
|
59
|
+
`${r.actions.distill.queued}/${r.actions.distill.llmFailed}/${r.actions.distill.qualityRejected}/${r.actions.distill.configDisabled}/${r.actions.distill.skipped}`,
|
|
60
|
+
`${r.consolidation.processed}/${r.consolidation.promoted}/${r.consolidation.merged}/${r.consolidation.deleted}`,
|
|
61
|
+
`${r.memoryInference.considered}/${r.memoryInference.written}/${r.memoryInference.skippedNoFacts}`,
|
|
62
|
+
`${r.graphExtraction.extractedFiles}/${r.graphExtraction.entities}/${r.graphExtraction.relations}`,
|
|
63
|
+
String(r.orphansPurged),
|
|
64
|
+
`${r.lintFixed}/${r.lintFlagged}`,
|
|
65
|
+
];
|
|
66
|
+
});
|
|
67
|
+
return renderTable(headers, rows);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Render a window-compare comparison as a side-by-side metric table with a
|
|
71
|
+
* delta column. Bad-direction deltas (e.g. +pct on failed counts) get a `!`
|
|
72
|
+
* marker prefix.
|
|
73
|
+
*/
|
|
74
|
+
export function renderWindowCompareMd(windows, deltas) {
|
|
75
|
+
if (windows.length === 0)
|
|
76
|
+
return "";
|
|
77
|
+
const headers = ["metric", ...windows.map((w) => w.name), "delta"];
|
|
78
|
+
const badIfPositive = new Set([
|
|
79
|
+
"improve.actions.reflect.failed",
|
|
80
|
+
"improve.actions.distill.llmFailed",
|
|
81
|
+
"improve.graphExtraction.failures",
|
|
82
|
+
"improve.graphExtraction.nonArrayBatchFailures",
|
|
83
|
+
"improve.wallTime.medianMs",
|
|
84
|
+
"improve.wallTime.p95Ms",
|
|
85
|
+
"improve.memoryInference.skippedNoFacts",
|
|
86
|
+
]);
|
|
87
|
+
const rows = [];
|
|
88
|
+
for (const path of INTERESTING_DELTA_PATHS) {
|
|
89
|
+
const values = windows.map((w) => String(readNumericPath(w, path)));
|
|
90
|
+
const delta = deltas?.[path];
|
|
91
|
+
let deltaStr = "—";
|
|
92
|
+
if (delta) {
|
|
93
|
+
const pct = delta.pctChange;
|
|
94
|
+
const num = typeof pct === "number" ? pct : pct;
|
|
95
|
+
const sign = typeof num === "number" && num > 0 ? "+" : "";
|
|
96
|
+
const formatted = typeof num === "number" ? `${sign}${num}%` : String(num);
|
|
97
|
+
const marker = badIfPositive.has(path) && typeof num === "number" && num > 0 ? "!" : "";
|
|
98
|
+
deltaStr = marker + formatted;
|
|
99
|
+
}
|
|
100
|
+
rows.push([path, ...values, deltaStr]);
|
|
101
|
+
}
|
|
102
|
+
return renderTable(headers, rows);
|
|
103
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* State.db-backed health metrics for `akm health`: the round-trip probe,
|
|
6
|
+
* auto-accept calibration, denominator-fixed coverage, the enrichment-vs-
|
|
7
|
+
* minting rollup, and the WS-5 per-run degradation metrics.
|
|
8
|
+
*/
|
|
9
|
+
import { appendEvent, readEvents } from "../../core/events.js";
|
|
10
|
+
import { queryImproveRuns } from "../../storage/repositories/improve-runs-repository.js";
|
|
11
|
+
import { listProposalGateDecisions, listStateProposals } from "../../storage/repositories/proposals-repository.js";
|
|
12
|
+
import { gateDecisionsToSamples, summarizeCalibration } from "../improve/calibration.js";
|
|
13
|
+
import { roundRate, toFiniteNumber } from "./improve-metrics.js";
|
|
14
|
+
import { ENRICHMENT_LANES, } from "./types.js";
|
|
15
|
+
/** Event type appended + read back by the state.db round-trip probe. */
|
|
16
|
+
const HEALTH_PROBE_EVENT = "health_probe";
|
|
17
|
+
export function probeStateDbRoundTrip(stateDbPath) {
|
|
18
|
+
const before = readEvents({}, { dbPath: stateDbPath }).nextOffset;
|
|
19
|
+
const started = Date.now();
|
|
20
|
+
appendEvent({ eventType: HEALTH_PROBE_EVENT, ref: "health:probe", metadata: { source: "akm health" } }, { dbPath: stateDbPath });
|
|
21
|
+
const after = readEvents({ sinceOffset: before, type: HEALTH_PROBE_EVENT, ref: "health:probe" }, { dbPath: stateDbPath });
|
|
22
|
+
const durationMs = Date.now() - started;
|
|
23
|
+
if (after.events.length === 0 || after.nextOffset <= before) {
|
|
24
|
+
return { ok: false, durationMs, error: "probe event was not readable after append" };
|
|
25
|
+
}
|
|
26
|
+
return { ok: true, durationMs };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read the auto-accept gate calibration summary (#612) over `[since, until)`.
|
|
30
|
+
* Reads every proposal's `gateDecision` from the open state.db, projects the
|
|
31
|
+
* acted-on (auto-accepted / auto-rejected) decisions into calibration samples
|
|
32
|
+
* within the window, and aggregates them deterministically.
|
|
33
|
+
*/
|
|
34
|
+
export function readCalibration(db, since, until) {
|
|
35
|
+
const decisions = listProposalGateDecisions(db);
|
|
36
|
+
const samples = gateDecisionsToSamples(decisions, { since, ...(until !== undefined ? { until } : {}) });
|
|
37
|
+
return summarizeCalibration(samples);
|
|
38
|
+
}
|
|
39
|
+
// ── WS-5 Observability helpers ───────────────────────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* Compute WS-5 denominator-fixed coverage metrics.
|
|
42
|
+
*
|
|
43
|
+
* `coverage = accepted_proposals / total_assets` (Part V §3).
|
|
44
|
+
* The denominator is the TOTAL stash size (not the moving eligible set) so
|
|
45
|
+
* more-inclusive WS-1 ranking cannot spuriously inflate coverage.
|
|
46
|
+
* `eligibleFraction = eligible_assets / total_assets` is reported separately.
|
|
47
|
+
*
|
|
48
|
+
* Proposals are counted only when their `updatedAt` falls within `[since, until)`
|
|
49
|
+
* so the rate is genuinely window-scoped (matching the JSDoc on the type).
|
|
50
|
+
*
|
|
51
|
+
* @param db - Open state.db connection.
|
|
52
|
+
* @param totalAssets - Total stash asset count (eligible + derived) from the
|
|
53
|
+
* most recent run's memorySummary. 0 = denominator unknown, returns NaN rates.
|
|
54
|
+
* @param eligibleAssets - Eligible (non-derived) asset count from the most recent run.
|
|
55
|
+
* @param since - Window start (ISO-8601). Proposals accepted before this are excluded.
|
|
56
|
+
* @param until - Window end (ISO-8601, exclusive). Absent = open-ended (up to now).
|
|
57
|
+
* @param stashDir - Optional: scope accepted proposals to one stash. Absent = all stashes.
|
|
58
|
+
*/
|
|
59
|
+
export function computeDenominatorFixedCoverage(db, totalAssets, eligibleAssets, since, until, stashDir) {
|
|
60
|
+
let acceptedProposals = 0;
|
|
61
|
+
let distinctRefs = 0;
|
|
62
|
+
try {
|
|
63
|
+
const proposals = listStateProposals(db, {
|
|
64
|
+
status: "accepted",
|
|
65
|
+
...(stashDir ? { stashDir } : {}),
|
|
66
|
+
}).filter((p) => {
|
|
67
|
+
const updatedAt = p.updatedAt ?? "";
|
|
68
|
+
if (updatedAt < since)
|
|
69
|
+
return false;
|
|
70
|
+
if (until !== undefined && updatedAt >= until)
|
|
71
|
+
return false;
|
|
72
|
+
return true;
|
|
73
|
+
});
|
|
74
|
+
acceptedProposals = proposals.length;
|
|
75
|
+
// Coverage counts DISTINCT refs: N accepted rewrites of one asset are
|
|
76
|
+
// churn, not coverage. The raw proposal count is kept alongside so the
|
|
77
|
+
// churn ratio (proposals ÷ distinct refs) stays visible.
|
|
78
|
+
distinctRefs = new Set(proposals.map((p) => p.ref)).size;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Fail open: table may not exist on older installs.
|
|
82
|
+
}
|
|
83
|
+
const churnRatio = distinctRefs > 0 ? roundRate(acceptedProposals / distinctRefs) : Number.NaN;
|
|
84
|
+
if (totalAssets === 0) {
|
|
85
|
+
return {
|
|
86
|
+
rate: Number.NaN,
|
|
87
|
+
eligibleFraction: Number.NaN,
|
|
88
|
+
acceptedProposals,
|
|
89
|
+
distinctRefs,
|
|
90
|
+
churnRatio,
|
|
91
|
+
totalAssets: 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
rate: roundRate(distinctRefs / totalAssets),
|
|
96
|
+
eligibleFraction: roundRate(eligibleAssets / totalAssets),
|
|
97
|
+
acceptedProposals,
|
|
98
|
+
distinctRefs,
|
|
99
|
+
churnRatio,
|
|
100
|
+
totalAssets,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Compute the enrichment-vs-minting rollup over the window's accepted,
|
|
105
|
+
* lane-attributed proposals (reporting-only; see {@link EnrichmentMintingRollup}).
|
|
106
|
+
*
|
|
107
|
+
* SQL-side `json_extract` keeps the (potentially large) `backupContent` blobs
|
|
108
|
+
* out of process memory. Pre-Phase-6C rows without an `eligibilitySource`
|
|
109
|
+
* cannot be lane-classified and are excluded. Fails open (undefined) when the
|
|
110
|
+
* proposals table is absent.
|
|
111
|
+
*/
|
|
112
|
+
export function computeEnrichmentMintingRollup(db, since, until) {
|
|
113
|
+
try {
|
|
114
|
+
const rows = db
|
|
115
|
+
.prepare(`SELECT
|
|
116
|
+
json_extract(metadata_json, '$.eligibilitySource') AS lane,
|
|
117
|
+
CASE WHEN json_extract(metadata_json, '$.backupContent') IS NULL THEN 1 ELSE 0 END AS is_minted,
|
|
118
|
+
COUNT(*) AS cnt
|
|
119
|
+
FROM proposals
|
|
120
|
+
WHERE status = 'accepted'
|
|
121
|
+
AND updated_at >= ?
|
|
122
|
+
AND (? IS NULL OR updated_at < ?)
|
|
123
|
+
AND json_extract(metadata_json, '$.eligibilitySource') IS NOT NULL
|
|
124
|
+
AND json_extract(metadata_json, '$.eligibilitySource') != ''
|
|
125
|
+
GROUP BY lane, is_minted`)
|
|
126
|
+
.all(since, until ?? null, until ?? null);
|
|
127
|
+
if (rows.length === 0)
|
|
128
|
+
return undefined;
|
|
129
|
+
const byLane = {};
|
|
130
|
+
for (const row of rows) {
|
|
131
|
+
byLane[row.lane] ??= { minted: 0, updated: 0 };
|
|
132
|
+
const entry = byLane[row.lane];
|
|
133
|
+
if (row.is_minted === 1)
|
|
134
|
+
entry.minted += row.cnt;
|
|
135
|
+
else
|
|
136
|
+
entry.updated += row.cnt;
|
|
137
|
+
}
|
|
138
|
+
let minted = 0;
|
|
139
|
+
let updated = 0;
|
|
140
|
+
for (const lane of ENRICHMENT_LANES) {
|
|
141
|
+
const entry = byLane[lane];
|
|
142
|
+
if (!entry)
|
|
143
|
+
continue;
|
|
144
|
+
minted += entry.minted;
|
|
145
|
+
updated += entry.updated;
|
|
146
|
+
}
|
|
147
|
+
const decided = minted + updated;
|
|
148
|
+
return {
|
|
149
|
+
minted,
|
|
150
|
+
updated,
|
|
151
|
+
share: decided > 0 ? roundRate(minted / decided) : Number.NaN,
|
|
152
|
+
byLane,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Fail open: proposals table may not exist on older installs.
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Compute WS-5 per-run degradation metrics (Part V §4).
|
|
162
|
+
*
|
|
163
|
+
* Health VIEWS only — reads from state.db tables populated by prior improve
|
|
164
|
+
* runs. Gracefully returns partial data when tables are absent (pre-WS-1/2).
|
|
165
|
+
*
|
|
166
|
+
* @param db - Open state.db connection.
|
|
167
|
+
* @param since - Window start (ISO-8601).
|
|
168
|
+
* @param until - Window end (ISO-8601).
|
|
169
|
+
*/
|
|
170
|
+
export function computeDegradationMetrics(db, since, until) {
|
|
171
|
+
// (a) Corpus diversity — salience rank distribution of the top-100 assets.
|
|
172
|
+
// We use the Gini coefficient of retrieval_salience scores as an intra-corpus
|
|
173
|
+
// diversity proxy. A Gini close to 1 = highly concentrated (entrenched top
|
|
174
|
+
// assets), Gini near 0 = flat/diverse. This is a single-snapshot metric;
|
|
175
|
+
// consecutive-run centroid distance requires cross-run history not yet stored.
|
|
176
|
+
let corpusCentroidDistance = Number.NaN;
|
|
177
|
+
let entrenchmentFlagged;
|
|
178
|
+
let salienceUniformityFlagged;
|
|
179
|
+
try {
|
|
180
|
+
const rows = db
|
|
181
|
+
.prepare(`SELECT retrieval_salience FROM asset_salience
|
|
182
|
+
ORDER BY rank_score DESC LIMIT 100`)
|
|
183
|
+
.all();
|
|
184
|
+
if (rows.length >= 5) {
|
|
185
|
+
const vals = rows.map((r) => r.retrieval_salience).sort((a, b) => a - b);
|
|
186
|
+
const n = vals.length;
|
|
187
|
+
const sumAbsDiff = vals.reduce((acc, xi, i) => {
|
|
188
|
+
return acc + vals.slice(i + 1).reduce((a, xj) => a + Math.abs(xi - xj), 0);
|
|
189
|
+
}, 0);
|
|
190
|
+
const mean = vals.reduce((a, b) => a + b, 0) / n;
|
|
191
|
+
// Gini = (sum |xi - xj|) / (2 n^2 mean); 0 = perfect equality, 1 = perfect inequality.
|
|
192
|
+
const gini = mean > 0 ? sumAbsDiff / (2 * n * n * mean) : 0;
|
|
193
|
+
// Re-express as a diversity proxy in [0,1]: high gini = low diversity.
|
|
194
|
+
// corpusCentroidDistance approximation: gini is "distance from uniform".
|
|
195
|
+
// Note: retrieval_salience values are in [0,1], so the max achievable Gini
|
|
196
|
+
// with this formula is ~0.5 (when one asset dominates and others are near 0).
|
|
197
|
+
// Two-tailed: >0.35 flags entrenchment (robustly above the ~0.1 uniform
|
|
198
|
+
// baseline); <0.08 flags uniformity collapse — the distribution no longer
|
|
199
|
+
// discriminates between assets (live 2026-07 value 0.040 sat unflagged
|
|
200
|
+
// in this tail under the old one-tailed check).
|
|
201
|
+
corpusCentroidDistance = roundRate(gini);
|
|
202
|
+
entrenchmentFlagged = gini > 0.35;
|
|
203
|
+
salienceUniformityFlagged = gini < 0.08;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Table not present (pre-WS-1 install) — leave NaN.
|
|
208
|
+
}
|
|
209
|
+
// (b) Merge fidelity — fraction of consolidate accepted proposals in the window
|
|
210
|
+
// whose ref also has a consolidate skip-reason of "contradict_target_missing"
|
|
211
|
+
// or an event indicating contradiction. Uses the improve_runs result_json
|
|
212
|
+
// consolidation.contradicted count as a proxy.
|
|
213
|
+
// Simple implementation: contradictionRate = total_contradicted / max(1, total_processed)
|
|
214
|
+
// sourced from the window's consolidation envelope.
|
|
215
|
+
// (The full "merge proposal → later contradiction" correlation requires cross-run
|
|
216
|
+
// history; this is the available proxy.)
|
|
217
|
+
let mergeFidelityContradictionRate = 0;
|
|
218
|
+
try {
|
|
219
|
+
const runs = queryImproveRuns(db, since, until);
|
|
220
|
+
let totalContradicted = 0;
|
|
221
|
+
let totalProcessed = 0;
|
|
222
|
+
for (const row of runs) {
|
|
223
|
+
try {
|
|
224
|
+
const result = JSON.parse(row.result_json);
|
|
225
|
+
const cons = result.consolidation;
|
|
226
|
+
if (cons) {
|
|
227
|
+
totalContradicted += toFiniteNumber(cons.contradicted);
|
|
228
|
+
totalProcessed += toFiniteNumber(cons.processed);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Skip malformed rows.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (totalProcessed > 0) {
|
|
236
|
+
mergeFidelityContradictionRate = roundRate(totalContradicted / totalProcessed);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// Fail open.
|
|
241
|
+
}
|
|
242
|
+
// (c) highGenerationFraction was DELETED (meta-review 05 DRIFT-3): it
|
|
243
|
+
// approximated "LLM-merge generations" from consecutive_no_ops — which counts
|
|
244
|
+
// the opposite condition (cycles where nothing was changed) — and its own
|
|
245
|
+
// in-code TODO admitted the proxy. Display-only, never actionable; removed
|
|
246
|
+
// rather than instrumented.
|
|
247
|
+
// (d) Oracle spot-check — up to 5 recently accepted proposals in the window.
|
|
248
|
+
const oracleSpotCheck = [];
|
|
249
|
+
try {
|
|
250
|
+
const accepted = listStateProposals(db, { status: "accepted" }).filter((p) => {
|
|
251
|
+
const updatedAt = p.updatedAt ?? "";
|
|
252
|
+
return updatedAt >= since && updatedAt < until;
|
|
253
|
+
});
|
|
254
|
+
// Sample up to 5: pick evenly spaced (not just the first 5).
|
|
255
|
+
const step = Math.max(1, Math.floor(accepted.length / 5));
|
|
256
|
+
for (let i = 0; i < accepted.length && oracleSpotCheck.length < 5; i += step) {
|
|
257
|
+
const p = accepted[i];
|
|
258
|
+
if (p) {
|
|
259
|
+
oracleSpotCheck.push({
|
|
260
|
+
proposalId: p.id,
|
|
261
|
+
ref: p.ref,
|
|
262
|
+
source: p.source ?? "unknown",
|
|
263
|
+
acceptedAt: p.updatedAt ?? p.createdAt ?? "",
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Fail open.
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
corpusCentroidDistance,
|
|
273
|
+
entrenchmentFlagged,
|
|
274
|
+
salienceUniformityFlagged,
|
|
275
|
+
mergeFidelityContradictionRate,
|
|
276
|
+
oracleSpotCheck,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* `stash-git-exposure` surfaces advisory for `akm health` (08-F1).
|
|
6
|
+
*
|
|
7
|
+
* Versioning the stash is a supported use case (private-remote backup), so a
|
|
8
|
+
* tracked `env/`/`secrets/` directory is NOT wrong by itself. The leak moment
|
|
9
|
+
* is when secret assets are git-TRACKED **and** a remote is configured — only
|
|
10
|
+
* then can a `git push` exfiltrate tokens/keys. Warn on exactly that
|
|
11
|
+
* combination; stay silent on the tracked-but-no-remote opt-in so the advisory
|
|
12
|
+
* catches the exposure without nagging the intentional backup.
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
const realGit = (stashDir, args) => {
|
|
16
|
+
const result = spawnSync("git", ["-C", stashDir, ...args], { encoding: "utf8", timeout: 5_000 });
|
|
17
|
+
return { ok: result.status === 0, stdout: (result.stdout ?? "").trim() };
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Build the `stash-git-exposure` advisory, or `undefined` when there is nothing
|
|
21
|
+
* to warn about (not a git repo, no tracked env/secret assets, or no remote).
|
|
22
|
+
*/
|
|
23
|
+
export function collectStashExposureAdvisory(stashDir, git = realGit) {
|
|
24
|
+
// Not a git work tree → nothing to expose.
|
|
25
|
+
if (!git(stashDir, ["rev-parse", "--is-inside-work-tree"]).ok)
|
|
26
|
+
return undefined;
|
|
27
|
+
const tracked = git(stashDir, ["ls-files", "--", "env", "secrets"]);
|
|
28
|
+
if (!tracked.ok || tracked.stdout.length === 0)
|
|
29
|
+
return undefined; // no secret assets tracked
|
|
30
|
+
const remotes = git(stashDir, ["remote"]);
|
|
31
|
+
if (!remotes.ok || remotes.stdout.length === 0)
|
|
32
|
+
return undefined; // no push target → no leak path
|
|
33
|
+
const trackedFiles = tracked.stdout.split("\n").filter(Boolean);
|
|
34
|
+
const preview = trackedFiles.slice(0, 5).join(", ") + (trackedFiles.length > 5 ? `, +${trackedFiles.length - 5} more` : "");
|
|
35
|
+
return {
|
|
36
|
+
name: "stash-git-exposure",
|
|
37
|
+
kind: "deterministic",
|
|
38
|
+
status: "warn",
|
|
39
|
+
confidence: "high",
|
|
40
|
+
message: `${trackedFiles.length} env/secret file(s) are git-tracked AND a remote is configured — ` +
|
|
41
|
+
`a 'git push' can leak tokens/keys (${preview}). ` +
|
|
42
|
+
"Run 'git rm --cached' on them (a .gitignore rule alone does NOT untrack already-tracked " +
|
|
43
|
+
"files) and then add env/+secrets/ to .gitignore to prevent recurrence (akm init scaffolds it).",
|
|
44
|
+
evidence: { trackedSecretFiles: trackedFiles },
|
|
45
|
+
};
|
|
46
|
+
}
|