akm-cli 0.8.0-rc1 → 0.8.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/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
- package/README.md +22 -6
- package/SECURITY.md +93 -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 +2162 -1258
- 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 +1533 -144
- 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 +204 -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 +977 -51
- package/dist/commands/help/help-accept.md +6 -3
- package/dist/commands/help/help-improve.md +36 -8
- package/dist/commands/help/help-proposals.md +7 -4
- package/dist/commands/help/help-reject.md +5 -2
- 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 +184 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1725 -332
- package/dist/commands/info.js +3 -0
- 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 +233 -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 +17 -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 +662 -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 +84 -14
- 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 +114 -48
- 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 +401 -30
- 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/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
- package/dist/output/cli-hints-full.md +60 -32
- package/dist/output/cli-hints-short.md +10 -7
- package/dist/output/cli-hints.js +5 -2
- 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 +3 -0
- package/dist/tasks/backends/schtasks.js +3 -0
- 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 +3 -0
- package/dist/wiki/wiki.js +3 -0
- package/dist/workflows/authoring.js +3 -0
- 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/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -307
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
|
@@ -0,0 +1,331 @@
|
|
|
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 fs from "node:fs";
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
import { output, parseAllFlagValues, runWithJsonErrors } from "../cli/shared";
|
|
7
|
+
import { parseAssetRef } from "../core/asset-ref";
|
|
8
|
+
import { assembleAsset } from "../core/asset-serialize";
|
|
9
|
+
import { writeFileAtomic } from "../core/common";
|
|
10
|
+
import { FEEDBACK_FAILURE_MODES, loadConfig } from "../core/config";
|
|
11
|
+
import { UsageError } from "../core/errors";
|
|
12
|
+
import { appendEvent } from "../core/events";
|
|
13
|
+
import { parseFrontmatter, parseFrontmatterBlock } from "../core/frontmatter";
|
|
14
|
+
import { warn } from "../core/warn";
|
|
15
|
+
import { applyFeedbackToUtilityScore, closeDatabase, findEntryIdByRef, openExistingDatabase } from "../indexer/db";
|
|
16
|
+
import { ensureIndex } from "../indexer/ensure-index";
|
|
17
|
+
import { resolveSourceEntries } from "../indexer/search-source";
|
|
18
|
+
import { insertUsageEvent } from "../indexer/usage-events";
|
|
19
|
+
// ── Tag validation ────────────────────────────────────────────────────────────
|
|
20
|
+
const TAG_KEY_RE = /^[a-z_][a-z0-9_]*$/;
|
|
21
|
+
const MAX_FEEDBACK_TAGS = 10;
|
|
22
|
+
function validateFeedbackTags(raw) {
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const tag of raw) {
|
|
26
|
+
const parts = tag.split(":");
|
|
27
|
+
if (parts.length < 2 || parts[0] === "" || parts.slice(1).join("") === "") {
|
|
28
|
+
throw new UsageError(`Invalid tag "${tag}". Tags must be in key:value format where key matches [a-z_][a-z0-9_]* and value is non-empty.`, "INVALID_FLAG_VALUE");
|
|
29
|
+
}
|
|
30
|
+
const key = parts[0];
|
|
31
|
+
if (!TAG_KEY_RE.test(key)) {
|
|
32
|
+
throw new UsageError(`Invalid tag key "${key}" in "${tag}". Key must match [a-z_][a-z0-9_]*.`, "INVALID_FLAG_VALUE");
|
|
33
|
+
}
|
|
34
|
+
if (seen.has(tag))
|
|
35
|
+
continue;
|
|
36
|
+
seen.add(tag);
|
|
37
|
+
out.push(tag);
|
|
38
|
+
}
|
|
39
|
+
if (out.length > MAX_FEEDBACK_TAGS) {
|
|
40
|
+
throw new UsageError(`Too many tags: ${out.length}. Maximum is ${MAX_FEEDBACK_TAGS}.`, "INVALID_FLAG_VALUE");
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
// ── Lesson strength helper ────────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Phase 7A: append a feedback ref to a lesson's `lessonStrength[]`
|
|
47
|
+
* frontmatter array. Returns `{ strength }` (post-update count) on success,
|
|
48
|
+
* or `null` when the lesson cannot be located. Idempotent: if the ref is
|
|
49
|
+
* already credited, no write occurs.
|
|
50
|
+
*
|
|
51
|
+
* The function looks up the lesson's file via the indexer DB so the write
|
|
52
|
+
* targets the canonical on-disk location. Frontmatter is rewritten in
|
|
53
|
+
* place (no asset-spec round-trip) because we're modifying a single key on
|
|
54
|
+
* an existing asset — the same pattern memory-inference uses for
|
|
55
|
+
* `inferenceProcessed`.
|
|
56
|
+
*/
|
|
57
|
+
function appendLessonStrength(type, name, feedbackRef) {
|
|
58
|
+
const ref = `${type}:${name}`;
|
|
59
|
+
let filePath;
|
|
60
|
+
const db = openExistingDatabase();
|
|
61
|
+
try {
|
|
62
|
+
const entryId = findEntryIdByRef(db, ref);
|
|
63
|
+
if (entryId === undefined) {
|
|
64
|
+
warn(`[feedback] --applied-to: lesson ${ref} is not in the index.`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const row = db.prepare("SELECT file_path FROM entries WHERE id = ?").get(entryId);
|
|
68
|
+
if (!row?.file_path) {
|
|
69
|
+
warn(`[feedback] --applied-to: cannot resolve file path for ${ref}.`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
filePath = row.file_path;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
closeDatabase(db);
|
|
76
|
+
}
|
|
77
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
78
|
+
warn(`[feedback] --applied-to: lesson file missing on disk for ${ref}.`);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
82
|
+
const parsed = parseFrontmatter(raw);
|
|
83
|
+
const data = { ...parsed.data };
|
|
84
|
+
const existing = data.lessonStrength;
|
|
85
|
+
const strengthList = Array.isArray(existing)
|
|
86
|
+
? existing.filter((x) => typeof x === "string" && x.trim().length > 0).map((x) => x.trim())
|
|
87
|
+
: typeof existing === "string" && existing.trim().length > 0
|
|
88
|
+
? [existing.trim()]
|
|
89
|
+
: [];
|
|
90
|
+
if (strengthList.includes(feedbackRef)) {
|
|
91
|
+
// Already credited — idempotent no-op.
|
|
92
|
+
return { strength: strengthList.length };
|
|
93
|
+
}
|
|
94
|
+
strengthList.push(feedbackRef);
|
|
95
|
+
data.lessonStrength = strengthList;
|
|
96
|
+
const block = parseFrontmatterBlock(raw);
|
|
97
|
+
const body = block?.content ?? raw;
|
|
98
|
+
const next = assembleAsset(data, body);
|
|
99
|
+
try {
|
|
100
|
+
// Preserve the existing file's permission bits (markdown assets are
|
|
101
|
+
// typically 0o644); writeFileAtomic defaults to 0o600 otherwise.
|
|
102
|
+
const mode = fs.statSync(filePath).mode & 0o777;
|
|
103
|
+
writeFileAtomic(filePath, next, mode);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
warn(`[feedback] --applied-to: failed to write ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return { strength: strengthList.length };
|
|
110
|
+
}
|
|
111
|
+
// ── Command definition ────────────────────────────────────────────────────────
|
|
112
|
+
export const feedbackCommand = defineCommand({
|
|
113
|
+
meta: {
|
|
114
|
+
name: "feedback",
|
|
115
|
+
description: "Record positive or negative feedback for any indexed stash asset.\n\n" +
|
|
116
|
+
"Positive feedback boosts an asset's EMA utility score, making it rank higher\n" +
|
|
117
|
+
"in future searches without requiring a full reindex.\n\n" +
|
|
118
|
+
"Negative feedback records a negative signal in usage_events and state.db events.\n" +
|
|
119
|
+
"It does NOT immediately lower the asset's ranking — the EMA utility score is\n" +
|
|
120
|
+
"updated the next time `akm index` runs (incremental or full). Run `akm index`\n" +
|
|
121
|
+
"after recording negative feedback to have it reflected in search results.",
|
|
122
|
+
},
|
|
123
|
+
args: {
|
|
124
|
+
// Optional in citty so run() is invoked even when omitted; we re-validate
|
|
125
|
+
// and throw a structured UsageError below so exit code is 2 (USAGE) rather
|
|
126
|
+
// than citty's default 0 (help banner).
|
|
127
|
+
ref: { type: "positional", description: "Asset ref (type:name)", required: false },
|
|
128
|
+
positive: { type: "boolean", description: "Record positive feedback (boosts ranking immediately)", default: false },
|
|
129
|
+
negative: {
|
|
130
|
+
type: "boolean",
|
|
131
|
+
description: "Record negative feedback (suppresses ranking after next `akm index`). " +
|
|
132
|
+
"Reindexing is required for the signal to affect search results.",
|
|
133
|
+
default: false,
|
|
134
|
+
},
|
|
135
|
+
reason: {
|
|
136
|
+
type: "string",
|
|
137
|
+
description: "Reason for the feedback (required for negative feedback by default; used by distillation)",
|
|
138
|
+
},
|
|
139
|
+
note: { type: "string", description: "Alias for --reason (backward-compatible, prefer --reason)" },
|
|
140
|
+
"failure-mode": {
|
|
141
|
+
type: "string",
|
|
142
|
+
description: `Structured failure-mode taxonomy for negative feedback (F-3 / #384). ` +
|
|
143
|
+
`Accepted values: ${FEEDBACK_FAILURE_MODES.join(", ")}. ` +
|
|
144
|
+
"Stored alongside --reason in event metadata for aggregation by the distill pipeline.",
|
|
145
|
+
},
|
|
146
|
+
tag: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description: "Tag to attach to the feedback (repeatable, e.g. --tag slice:train --tag team:platform)",
|
|
149
|
+
},
|
|
150
|
+
"applied-to": {
|
|
151
|
+
type: "string",
|
|
152
|
+
description: "Credit a lesson that helped resolve this task. Accepts a `lesson:<name>` ref. " +
|
|
153
|
+
"When combined with --positive, appends this feedback ref to the target lesson's " +
|
|
154
|
+
"`lessonStrength[]` frontmatter array (dedup, idempotent). Ignored on non-lesson targets.",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
run({ args }) {
|
|
158
|
+
return runWithJsonErrors(async () => {
|
|
159
|
+
const ref = (args.ref ?? "").trim();
|
|
160
|
+
if (!ref) {
|
|
161
|
+
throw new UsageError("Asset ref is required. Usage: akm feedback <ref> --positive|--negative", "MISSING_REQUIRED_ARGUMENT", "Pass a ref like `skill:deploy` and either --positive or --negative.");
|
|
162
|
+
}
|
|
163
|
+
parseAssetRef(ref);
|
|
164
|
+
if (args.positive && args.negative) {
|
|
165
|
+
throw new UsageError("Specify either --positive or --negative, not both.");
|
|
166
|
+
}
|
|
167
|
+
if (!args.positive && !args.negative) {
|
|
168
|
+
throw new UsageError("Specify --positive or --negative.");
|
|
169
|
+
}
|
|
170
|
+
const signal = args.positive ? "positive" : "negative";
|
|
171
|
+
// `--note` is a deprecated back-compat alias for `--reason` (removed in
|
|
172
|
+
// 0.9.0). Warn on stderr when it is used as the sole source (i.e. without
|
|
173
|
+
// an explicit `--reason`). Warnings go to stderr only so JSON stdout
|
|
174
|
+
// consumers are unaffected.
|
|
175
|
+
if (args.note !== undefined && args.reason === undefined) {
|
|
176
|
+
warn("warning: '--note' is deprecated for 'akm feedback'; use '--reason'. Removed in 0.9.0.");
|
|
177
|
+
}
|
|
178
|
+
const reason = args.reason ?? args.note;
|
|
179
|
+
// F-3 / #384: Validate --failure-mode against the curated enum.
|
|
180
|
+
const failureMode = args["failure-mode"]?.trim() || undefined;
|
|
181
|
+
if (failureMode) {
|
|
182
|
+
if (args.positive) {
|
|
183
|
+
throw new UsageError("--failure-mode is only valid for negative feedback.", "INVALID_FLAG_VALUE", "Remove --failure-mode or switch to --negative.");
|
|
184
|
+
}
|
|
185
|
+
const cfg = loadConfig();
|
|
186
|
+
const allowedModes = cfg.feedback?.allowedFailureModes ?? FEEDBACK_FAILURE_MODES;
|
|
187
|
+
if (allowedModes.length > 0 && !allowedModes.includes(failureMode)) {
|
|
188
|
+
throw new UsageError(`Invalid --failure-mode "${failureMode}". Accepted values: ${allowedModes.join(", ")}.`, "INVALID_FLAG_VALUE", `Use one of: ${allowedModes.join(", ")}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (args.negative === true && !reason?.trim()) {
|
|
192
|
+
// F-3 / #384: Default requireReason is now true. Load config to allow
|
|
193
|
+
// operators to opt out via feedback.requireReason: false in akm.json.
|
|
194
|
+
const cfg = loadConfig();
|
|
195
|
+
const requireReason = cfg.feedback?.requireReason ?? true; // Default: true (F-3 / #384)
|
|
196
|
+
if (requireReason) {
|
|
197
|
+
throw new UsageError("Negative feedback requires --reason (structured failure signals are needed for distillation). " +
|
|
198
|
+
"Use --failure-mode for a curated taxonomy or --reason for free text. " +
|
|
199
|
+
"Set feedback.requireReason: false in akm.json to downgrade to a warning.", "MISSING_REQUIRED_ARGUMENT", `Hint: akm feedback ${ref} --negative --reason "..." [--failure-mode incorrect|outdated|dangerous|incomplete|redundant]`);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
warn("Warning: negative feedback without --reason provides less distillation signal.");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const rawTags = parseAllFlagValues("--tag");
|
|
206
|
+
const validatedTags = validateFeedbackTags(rawTags);
|
|
207
|
+
const metadataObj = {
|
|
208
|
+
signal,
|
|
209
|
+
...(reason?.trim() ? { reason: reason.trim() } : {}),
|
|
210
|
+
...(failureMode ? { failureMode } : {}),
|
|
211
|
+
...(validatedTags.length > 0 ? { tags: validatedTags } : {}),
|
|
212
|
+
};
|
|
213
|
+
const metadataStr = Object.keys(metadataObj).length > 1 ? JSON.stringify(metadataObj) : undefined;
|
|
214
|
+
// Auto-index when stale so the index is current before recording feedback.
|
|
215
|
+
const sources = resolveSourceEntries();
|
|
216
|
+
if (sources.length > 0) {
|
|
217
|
+
await ensureIndex(sources[0].path);
|
|
218
|
+
}
|
|
219
|
+
let utilityResult;
|
|
220
|
+
const db = openExistingDatabase();
|
|
221
|
+
try {
|
|
222
|
+
const entryId = findEntryIdByRef(db, ref);
|
|
223
|
+
if (entryId === undefined) {
|
|
224
|
+
throw new UsageError(`Ref "${ref}" is not in the index. ` +
|
|
225
|
+
"Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.");
|
|
226
|
+
}
|
|
227
|
+
// Persist the feedback signal into usage_events. For positive signals,
|
|
228
|
+
// the EMA utility score is updated immediately on the next read path.
|
|
229
|
+
// For negative signals, the score is adjusted the next time `akm index`
|
|
230
|
+
// runs — the signal is durable in the DB but does NOT suppress ranking
|
|
231
|
+
// in search results until after reindexing.
|
|
232
|
+
insertUsageEvent(db, {
|
|
233
|
+
event_type: "feedback",
|
|
234
|
+
entry_ref: ref,
|
|
235
|
+
entry_id: entryId,
|
|
236
|
+
signal,
|
|
237
|
+
metadata: metadataStr,
|
|
238
|
+
});
|
|
239
|
+
// Apply feedback-derived utility score adjustment immediately so that
|
|
240
|
+
// positive/negative signals influence search ranking without requiring
|
|
241
|
+
// a full reindex. We query the total accumulated feedback counts from
|
|
242
|
+
// usage_events so the delta reflects the entire signal history.
|
|
243
|
+
// Uses MemRL bounded-step EMA (F-5 / #386, arXiv:2601.03192).
|
|
244
|
+
try {
|
|
245
|
+
const counts = db
|
|
246
|
+
.prepare(`SELECT
|
|
247
|
+
SUM(CASE WHEN signal = 'positive' THEN 1 ELSE 0 END) AS pos,
|
|
248
|
+
SUM(CASE WHEN signal = 'negative' THEN 1 ELSE 0 END) AS neg
|
|
249
|
+
FROM usage_events
|
|
250
|
+
WHERE event_type = 'feedback' AND entry_id = ?`)
|
|
251
|
+
.get(entryId);
|
|
252
|
+
const pos = counts?.pos ?? 0;
|
|
253
|
+
const neg = counts?.neg ?? 0;
|
|
254
|
+
utilityResult = applyFeedbackToUtilityScore(db, entryId, pos, neg);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// best-effort — feedback recording succeeds even if utility update fails
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
closeDatabase(db);
|
|
262
|
+
}
|
|
263
|
+
appendEvent({
|
|
264
|
+
eventType: "feedback",
|
|
265
|
+
ref,
|
|
266
|
+
metadata: metadataObj,
|
|
267
|
+
});
|
|
268
|
+
// F-5 / #386: When a high-utility asset crosses below the review threshold,
|
|
269
|
+
// auto-create a review-needed escalation proposal so a human can confirm
|
|
270
|
+
// whether the negative feedback is valid before the asset falls out of
|
|
271
|
+
// the improve loop. Best-effort — failure is logged but does not fail the
|
|
272
|
+
// feedback command.
|
|
273
|
+
// Emit a structured event rather than a proposal so the review-needed
|
|
274
|
+
// signal is queryable via `akm events list --type improve_review_needed`
|
|
275
|
+
// without risking accidental asset overwrite if the proposal is accepted.
|
|
276
|
+
if (utilityResult?.crossedReviewThreshold) {
|
|
277
|
+
try {
|
|
278
|
+
appendEvent({
|
|
279
|
+
eventType: "improve_review_needed",
|
|
280
|
+
ref,
|
|
281
|
+
metadata: {
|
|
282
|
+
previousUtility: utilityResult.previousUtility,
|
|
283
|
+
nextUtility: utilityResult.nextUtility,
|
|
284
|
+
reason: reason?.trim() ?? null,
|
|
285
|
+
failureMode: failureMode ?? null,
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
catch (escalationErr) {
|
|
290
|
+
warn(`[feedback] Could not emit review-needed event for ${ref}: ${escalationErr instanceof Error ? escalationErr.message : String(escalationErr)}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Phase 7A / Advantage D4b: --applied-to credits a lesson. When the
|
|
294
|
+
// target is a `lesson:<name>` ref and the signal is positive, append
|
|
295
|
+
// the feedback ref to the target lesson's `lessonStrength[]`
|
|
296
|
+
// frontmatter array (dedup, idempotent). Non-lesson targets are
|
|
297
|
+
// ignored. Failures here are warnings — feedback recording is the
|
|
298
|
+
// primary contract and must not regress on lesson-write errors.
|
|
299
|
+
const appliedToRaw = args["applied-to"]?.trim();
|
|
300
|
+
let appliedToResult = null;
|
|
301
|
+
if (appliedToRaw && signal === "positive") {
|
|
302
|
+
try {
|
|
303
|
+
const parsedApplied = parseAssetRef(appliedToRaw);
|
|
304
|
+
if (parsedApplied.type === "lesson") {
|
|
305
|
+
const updated = appendLessonStrength(parsedApplied.type, parsedApplied.name, ref);
|
|
306
|
+
if (updated) {
|
|
307
|
+
appliedToResult = { lessonRef: appliedToRaw, strength: updated.strength };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
warn(`[feedback] --applied-to failed for ${appliedToRaw}: ${err instanceof Error ? err.message : String(err)}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else if (appliedToRaw && signal !== "positive") {
|
|
316
|
+
warn("[feedback] --applied-to is ignored without --positive; lesson credit is only recorded on positive signals.");
|
|
317
|
+
}
|
|
318
|
+
output("feedback", {
|
|
319
|
+
ok: true,
|
|
320
|
+
ref,
|
|
321
|
+
signal,
|
|
322
|
+
reason: reason?.trim() ?? null,
|
|
323
|
+
failureMode: failureMode ?? null,
|
|
324
|
+
tags: validatedTags,
|
|
325
|
+
...(appliedToResult
|
|
326
|
+
? { appliedTo: { ref: appliedToResult.lessonRef, lessonStrength: appliedToResult.strength } }
|
|
327
|
+
: {}),
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
});
|
package/dist/commands/graph.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
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 fs from "node:fs";
|
|
2
5
|
import path from "node:path";
|
|
3
6
|
import { parseAssetRef } from "../core/asset-ref";
|
|
4
7
|
import { loadConfig } from "../core/config";
|
|
5
8
|
import { NotFoundError, UsageError } from "../core/errors";
|
|
6
|
-
import {
|
|
9
|
+
import { getDbPath } from "../core/paths";
|
|
10
|
+
import { warn } from "../core/warn";
|
|
11
|
+
import { closeDatabase, findEntryIdByRef, getEntryById, openDatabase, openExistingDatabase } from "../indexer/db";
|
|
7
12
|
import { listRelatedPathsForFile } from "../indexer/graph-boost";
|
|
8
13
|
import { loadStoredGraphSnapshot } from "../indexer/graph-db";
|
|
14
|
+
import { runGraphExtractionPass } from "../indexer/graph-extraction";
|
|
9
15
|
import { lookup } from "../indexer/indexer";
|
|
10
16
|
import { resolveAssetPath } from "../indexer/path-resolver";
|
|
11
17
|
import { findSourceForPath, resolveSourceEntries } from "../indexer/search-source";
|
|
@@ -40,6 +46,7 @@ function loadGraph(source) {
|
|
|
40
46
|
entities: snapshot.entities,
|
|
41
47
|
relations: snapshot.relations,
|
|
42
48
|
...(snapshot.quality ? { quality: snapshot.quality } : {}),
|
|
49
|
+
...(snapshot.telemetry ? { telemetry: snapshot.telemetry } : {}),
|
|
43
50
|
},
|
|
44
51
|
stashPath,
|
|
45
52
|
graphPath: snapshot.graphPath,
|
|
@@ -63,6 +70,29 @@ function countEntitiesByFile(nodes) {
|
|
|
63
70
|
}
|
|
64
71
|
return counts;
|
|
65
72
|
}
|
|
73
|
+
function aggregateEntityStats(nodes) {
|
|
74
|
+
const stats = new Map();
|
|
75
|
+
for (const node of nodes) {
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
for (const entity of node.entities) {
|
|
78
|
+
if (seen.has(entity))
|
|
79
|
+
continue;
|
|
80
|
+
seen.add(entity);
|
|
81
|
+
const existing = stats.get(entity);
|
|
82
|
+
const nodeConf = typeof node.confidence === "number" && Number.isFinite(node.confidence) ? node.confidence : undefined;
|
|
83
|
+
if (existing) {
|
|
84
|
+
existing.fileCount += 1;
|
|
85
|
+
if (nodeConf !== undefined && (existing.confidence === undefined || nodeConf > existing.confidence)) {
|
|
86
|
+
existing.confidence = nodeConf;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
stats.set(entity, { fileCount: 1, ...(nodeConf !== undefined ? { confidence: nodeConf } : {}) });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return stats;
|
|
95
|
+
}
|
|
66
96
|
export function akmGraphSummary(options) {
|
|
67
97
|
const { graph, stashPath, graphPath } = loadGraph(options?.source);
|
|
68
98
|
return {
|
|
@@ -77,6 +107,7 @@ export function akmGraphSummary(options) {
|
|
|
77
107
|
? graph.relations.length
|
|
78
108
|
: graph.files.reduce((sum, node) => sum + node.relations.length, 0),
|
|
79
109
|
...(graph.quality ? { quality: graph.quality } : {}),
|
|
110
|
+
...(graph.telemetry ? { telemetry: graph.telemetry } : {}),
|
|
80
111
|
};
|
|
81
112
|
}
|
|
82
113
|
export function akmGraphEntities(options) {
|
|
@@ -85,9 +116,13 @@ export function akmGraphEntities(options) {
|
|
|
85
116
|
if (limit !== undefined && (!Number.isFinite(limit) || limit <= 0)) {
|
|
86
117
|
throw new UsageError("--limit must be a positive integer.", "INVALID_FLAG_VALUE");
|
|
87
118
|
}
|
|
88
|
-
const
|
|
89
|
-
const entities = [...
|
|
90
|
-
.map(([name,
|
|
119
|
+
const stats = aggregateEntityStats(graph.files);
|
|
120
|
+
const entities = [...stats.entries()]
|
|
121
|
+
.map(([name, info]) => ({
|
|
122
|
+
name,
|
|
123
|
+
fileCount: info.fileCount,
|
|
124
|
+
...(info.confidence !== undefined ? { confidence: info.confidence } : {}),
|
|
125
|
+
}))
|
|
91
126
|
.sort((a, b) => b.fileCount - a.fileCount || a.name.localeCompare(b.name));
|
|
92
127
|
const sliced = typeof limit === "number" ? entities.slice(0, limit) : entities;
|
|
93
128
|
return {
|
|
@@ -110,12 +145,22 @@ export function akmGraphRelations(options) {
|
|
|
110
145
|
for (const node of graph.files) {
|
|
111
146
|
for (const rel of node.relations) {
|
|
112
147
|
const key = `${rel.from}\u0000${rel.to}\u0000${rel.type ?? ""}`;
|
|
148
|
+
const relConf = typeof rel.confidence === "number" && Number.isFinite(rel.confidence) ? rel.confidence : undefined;
|
|
113
149
|
const existing = counts.get(key);
|
|
114
150
|
if (existing) {
|
|
115
151
|
existing.count += 1;
|
|
152
|
+
if (relConf !== undefined && (existing.confidence === undefined || relConf > existing.confidence)) {
|
|
153
|
+
existing.confidence = relConf;
|
|
154
|
+
}
|
|
116
155
|
}
|
|
117
156
|
else {
|
|
118
|
-
counts.set(key, {
|
|
157
|
+
counts.set(key, {
|
|
158
|
+
from: rel.from,
|
|
159
|
+
to: rel.to,
|
|
160
|
+
...(rel.type ? { type: rel.type } : {}),
|
|
161
|
+
count: 1,
|
|
162
|
+
...(relConf !== undefined ? { confidence: relConf } : {}),
|
|
163
|
+
});
|
|
119
164
|
}
|
|
120
165
|
}
|
|
121
166
|
}
|
|
@@ -194,6 +239,216 @@ export async function akmGraphRelated(options) {
|
|
|
194
239
|
...(related.length === 0 ? { tip: "No related graph neighbors were found for this asset." } : {}),
|
|
195
240
|
};
|
|
196
241
|
}
|
|
242
|
+
function normalizeGraphName(value) {
|
|
243
|
+
return value.trim().toLowerCase();
|
|
244
|
+
}
|
|
245
|
+
function buildRefByPath(stashRoot, db) {
|
|
246
|
+
const rows = db
|
|
247
|
+
.prepare("SELECT file_path, entry_json FROM entries WHERE stash_dir = ? OR file_path LIKE ?")
|
|
248
|
+
.all(stashRoot, `${stashRoot}%`);
|
|
249
|
+
const map = new Map();
|
|
250
|
+
for (const row of rows) {
|
|
251
|
+
if (map.has(row.file_path))
|
|
252
|
+
continue;
|
|
253
|
+
try {
|
|
254
|
+
const entry = JSON.parse(row.entry_json);
|
|
255
|
+
if (typeof entry.type === "string" && typeof entry.name === "string") {
|
|
256
|
+
map.set(row.file_path, { ref: `${entry.type}:${entry.name}`, type: entry.type });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// ignore corrupt entry_json
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return map;
|
|
264
|
+
}
|
|
265
|
+
export function akmGraphEntity(options) {
|
|
266
|
+
const name = options.name?.trim();
|
|
267
|
+
if (!name) {
|
|
268
|
+
throw new UsageError("`akm graph entity` requires <name>.", "MISSING_REQUIRED_ARGUMENT");
|
|
269
|
+
}
|
|
270
|
+
const limit = options.limit;
|
|
271
|
+
if (limit !== undefined && (!Number.isFinite(limit) || limit <= 0)) {
|
|
272
|
+
throw new UsageError("--limit must be a positive integer.", "INVALID_FLAG_VALUE");
|
|
273
|
+
}
|
|
274
|
+
const { graph, stashPath, graphPath } = loadGraph(options.source);
|
|
275
|
+
const target = normalizeGraphName(name);
|
|
276
|
+
let db;
|
|
277
|
+
let refByPath;
|
|
278
|
+
try {
|
|
279
|
+
db = openExistingDatabase();
|
|
280
|
+
refByPath = buildRefByPath(stashPath, db);
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
if (db)
|
|
284
|
+
closeDatabase(db);
|
|
285
|
+
}
|
|
286
|
+
const matches = [];
|
|
287
|
+
for (const node of graph.files) {
|
|
288
|
+
const found = node.entities.some((entity) => normalizeGraphName(entity) === target);
|
|
289
|
+
if (!found)
|
|
290
|
+
continue;
|
|
291
|
+
const lookup = refByPath.get(node.path);
|
|
292
|
+
const conf = typeof node.confidence === "number" && Number.isFinite(node.confidence) ? node.confidence : undefined;
|
|
293
|
+
matches.push({
|
|
294
|
+
...(lookup?.ref ? { ref: lookup.ref } : {}),
|
|
295
|
+
path: node.path,
|
|
296
|
+
type: node.type,
|
|
297
|
+
...(conf !== undefined ? { confidence: conf } : {}),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
matches.sort((a, b) => {
|
|
301
|
+
const ca = a.confidence ?? 0;
|
|
302
|
+
const cb = b.confidence ?? 0;
|
|
303
|
+
if (cb !== ca)
|
|
304
|
+
return cb - ca;
|
|
305
|
+
return a.path.localeCompare(b.path);
|
|
306
|
+
});
|
|
307
|
+
const sliced = typeof limit === "number" ? matches.slice(0, limit) : matches;
|
|
308
|
+
return {
|
|
309
|
+
schemaVersion: 1,
|
|
310
|
+
shape: "graph-entity",
|
|
311
|
+
stashPath,
|
|
312
|
+
graphPath,
|
|
313
|
+
generatedAt: graph.generatedAt,
|
|
314
|
+
entity: name,
|
|
315
|
+
total: matches.length,
|
|
316
|
+
matches: sliced,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
export function akmGraphOrphans(options) {
|
|
320
|
+
const limit = options?.limit;
|
|
321
|
+
if (limit !== undefined && (!Number.isFinite(limit) || limit <= 0)) {
|
|
322
|
+
throw new UsageError("--limit must be a positive integer.", "INVALID_FLAG_VALUE");
|
|
323
|
+
}
|
|
324
|
+
const { graph, stashPath, graphPath } = loadGraph(options?.source);
|
|
325
|
+
let db;
|
|
326
|
+
let refByPath;
|
|
327
|
+
try {
|
|
328
|
+
db = openExistingDatabase();
|
|
329
|
+
refByPath = buildRefByPath(stashPath, db);
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
if (db)
|
|
333
|
+
closeDatabase(db);
|
|
334
|
+
}
|
|
335
|
+
const orphans = [];
|
|
336
|
+
for (const node of graph.files) {
|
|
337
|
+
if ((node.status ?? (node.entities.length > 0 ? "extracted" : "empty")) === "extracted")
|
|
338
|
+
continue;
|
|
339
|
+
const lookup = refByPath.get(node.path);
|
|
340
|
+
orphans.push({
|
|
341
|
+
...(lookup?.ref ? { ref: lookup.ref } : {}),
|
|
342
|
+
path: node.path,
|
|
343
|
+
type: node.type,
|
|
344
|
+
...(node.status ? { status: node.status } : {}),
|
|
345
|
+
...(node.reason ? { reason: node.reason } : {}),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
orphans.sort((a, b) => a.type.localeCompare(b.type) || a.path.localeCompare(b.path));
|
|
349
|
+
const sliced = typeof limit === "number" ? orphans.slice(0, limit) : orphans;
|
|
350
|
+
return {
|
|
351
|
+
schemaVersion: 1,
|
|
352
|
+
shape: "graph-orphans",
|
|
353
|
+
stashPath,
|
|
354
|
+
graphPath,
|
|
355
|
+
generatedAt: graph.generatedAt,
|
|
356
|
+
totalConsidered: graph.files.length,
|
|
357
|
+
total: orphans.length,
|
|
358
|
+
orphans: sliced,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Re-run graph extraction, optionally scoped to specific asset refs.
|
|
363
|
+
*
|
|
364
|
+
* When `refs` is provided, only those files are re-extracted (incremental).
|
|
365
|
+
* When no refs are given, the full eligible set is re-extracted.
|
|
366
|
+
*/
|
|
367
|
+
export async function akmGraphUpdate(options) {
|
|
368
|
+
const config = options.config ?? loadConfig();
|
|
369
|
+
const sources = resolveSourceEntries(options.stashDir, config);
|
|
370
|
+
if (sources.length === 0) {
|
|
371
|
+
throw new NotFoundError("No stash sources are configured.", "STASH_NOT_FOUND");
|
|
372
|
+
}
|
|
373
|
+
if (options.source && options.source !== "primary") {
|
|
374
|
+
const matched = sources.find((s) => s.registryId === options.source || s.path === options.source);
|
|
375
|
+
if (!matched) {
|
|
376
|
+
throw new NotFoundError(`Source not found: ${options.source}`, "SOURCE_NOT_FOUND", "Run `akm list` to see source names.");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const scoped = Array.isArray(options.refs) && options.refs.length > 0;
|
|
380
|
+
let candidatePaths;
|
|
381
|
+
if (scoped && options.refs) {
|
|
382
|
+
// Resolve each ref to an absolute file path via the index DB.
|
|
383
|
+
const dbPath = getDbPath();
|
|
384
|
+
let db;
|
|
385
|
+
const resolvedPaths = new Set();
|
|
386
|
+
try {
|
|
387
|
+
db = openDatabase(dbPath);
|
|
388
|
+
for (const ref of options.refs) {
|
|
389
|
+
const trimmed = ref.trim();
|
|
390
|
+
if (!trimmed)
|
|
391
|
+
continue;
|
|
392
|
+
const entryId = findEntryIdByRef(db, trimmed);
|
|
393
|
+
if (entryId === undefined) {
|
|
394
|
+
warn(`[graph] ref not found in index, skipping: ${trimmed}`);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const row = getEntryById(db, entryId);
|
|
398
|
+
if (!row?.filePath) {
|
|
399
|
+
warn(`[graph] could not resolve path for ref, skipping: ${trimmed}`);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
resolvedPaths.add(row.filePath);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
if (db)
|
|
407
|
+
closeDatabase(db);
|
|
408
|
+
}
|
|
409
|
+
if (resolvedPaths.size === 0) {
|
|
410
|
+
warn("[graph] none of the provided refs resolved to indexed paths — no extraction performed.");
|
|
411
|
+
return {
|
|
412
|
+
shape: "graph-update",
|
|
413
|
+
ok: true,
|
|
414
|
+
filesExtracted: 0,
|
|
415
|
+
entitiesUpserted: 0,
|
|
416
|
+
relationsUpserted: 0,
|
|
417
|
+
durationMs: 0,
|
|
418
|
+
scoped: true,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
candidatePaths = resolvedPaths;
|
|
422
|
+
}
|
|
423
|
+
const extractionFn = options.graphExtractionFn ?? runGraphExtractionPass;
|
|
424
|
+
const passOptions = candidatePaths ? { candidatePaths } : {};
|
|
425
|
+
let db;
|
|
426
|
+
const startMs = Date.now();
|
|
427
|
+
try {
|
|
428
|
+
db = openDatabase(getDbPath());
|
|
429
|
+
const onProgress = (event) => {
|
|
430
|
+
if (!event.currentPath)
|
|
431
|
+
return;
|
|
432
|
+
const file = path.basename(event.currentPath);
|
|
433
|
+
warn(`[graph] extracting ${event.processed}/${event.total} ${file}`);
|
|
434
|
+
};
|
|
435
|
+
const result = await extractionFn(config, sources, undefined, db, false, onProgress, passOptions);
|
|
436
|
+
const durationMs = Date.now() - startMs;
|
|
437
|
+
return {
|
|
438
|
+
shape: "graph-update",
|
|
439
|
+
ok: true,
|
|
440
|
+
filesExtracted: result.quality.extractedFiles,
|
|
441
|
+
entitiesUpserted: result.quality.entityCount,
|
|
442
|
+
relationsUpserted: result.quality.relationCount,
|
|
443
|
+
durationMs,
|
|
444
|
+
scoped,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
finally {
|
|
448
|
+
if (db)
|
|
449
|
+
closeDatabase(db);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
197
452
|
async function resolveGraphTarget(ref, source) {
|
|
198
453
|
const parsedRef = parseAssetRef(ref);
|
|
199
454
|
const filePath = (await resolveAssetPath(parsedRef, {
|