akm-cli 0.7.5 → 0.8.0-rc.10
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} +192 -2
- 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 +133 -0
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2569 -1449
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +110 -0
- package/dist/commands/agent-support.js +68 -0
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +130 -534
- package/dist/commands/consolidate.js +2122 -0
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +660 -0
- package/dist/commands/distill.js +1075 -77
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +43 -0
- package/dist/commands/events.js +5 -23
- 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 +477 -0
- package/dist/commands/health.js +1302 -0
- package/dist/commands/help/help-accept.md +12 -0
- package/dist/commands/help/help-improve.md +69 -0
- package/dist/commands/help/help-proposals.md +18 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +11 -0
- package/dist/commands/history.js +54 -46
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +217 -0
- package/dist/commands/improve-profiles.js +166 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +2373 -0
- package/dist/commands/info.js +5 -2
- package/dist/commands/init.js +50 -2
- package/dist/commands/installed-stashes.js +102 -139
- package/dist/commands/knowledge.js +136 -0
- package/dist/commands/lint/agent-linter.js +49 -0
- package/dist/commands/lint/base-linter.js +479 -0
- package/dist/commands/lint/command-linter.js +49 -0
- package/dist/commands/lint/default-linter.js +16 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +196 -0
- package/dist/commands/lint/knowledge-linter.js +16 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +61 -0
- package/dist/commands/lint/registry.js +36 -0
- package/dist/commands/lint/skill-linter.js +45 -0
- package/dist/commands/lint/task-linter.js +50 -0
- package/dist/commands/lint/types.js +4 -0
- package/dist/commands/lint/workflow-linter.js +56 -0
- package/dist/commands/lint.js +4 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal.js +67 -12
- package/dist/commands/propose.js +86 -31
- package/dist/commands/reflect.js +1091 -73
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +5 -2
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +69 -6
- package/dist/commands/schema-repair.js +203 -0
- package/dist/commands/search.js +115 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +148 -25
- package/dist/commands/source-add.js +17 -45
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +14 -19
- package/dist/commands/tasks.js +437 -0
- package/dist/commands/url-checker.js +42 -0
- package/dist/core/action-contributors.js +28 -0
- package/dist/core/asset-ref.js +17 -2
- package/dist/core/asset-registry.js +12 -17
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +67 -1
- package/dist/core/common.js +182 -0
- package/dist/core/concurrent.js +25 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +534 -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 +364 -981
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +91 -138
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -8
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +20 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +806 -0
- package/dist/core/parse.js +158 -0
- package/dist/core/paths.js +280 -14
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +69 -0
- package/dist/core/proposals.js +512 -42
- package/dist/core/state-db.js +1068 -0
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +54 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +64 -1
- package/dist/core/write-source.js +3 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +163 -254
- package/dist/indexer/db.js +975 -103
- package/dist/indexer/ensure-index.js +64 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +376 -101
- package/dist/indexer/graph-db.js +391 -0
- package/dist/indexer/graph-dedup.js +95 -0
- package/dist/indexer/graph-extraction.js +550 -124
- package/dist/indexer/index-context.js +4 -0
- package/dist/indexer/indexer.js +523 -301
- package/dist/indexer/llm-cache.js +52 -0
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +167 -160
- package/dist/indexer/memory-inference.js +152 -74
- package/dist/indexer/metadata-contributors.js +29 -0
- package/dist/indexer/metadata.js +275 -196
- package/dist/indexer/path-resolver.js +92 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +331 -0
- package/dist/indexer/ranking.js +81 -0
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +111 -0
- package/dist/indexer/search-source.js +44 -10
- package/dist/indexer/semantic-status.js +6 -17
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +28 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +122 -230
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +7 -13
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +70 -5
- package/dist/integrations/agent/prompts.js +214 -80
- package/dist/integrations/agent/runner.js +151 -0
- package/dist/integrations/agent/sdk-runner.js +126 -0
- package/dist/integrations/agent/spawn.js +118 -23
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +32 -69
- package/dist/integrations/session-logs/index.js +69 -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 +282 -0
- package/dist/integrations/session-logs/providers/opencode.js +258 -0
- package/dist/integrations/session-logs/types.js +4 -0
- package/dist/llm/call-ai.js +62 -0
- package/dist/llm/client.js +77 -124
- 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 +95 -48
- package/dist/llm/graph-extract.js +676 -70
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +77 -71
- package/dist/llm/metadata-enhance.js +42 -29
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/output/cli-hints-full.md +292 -0
- package/dist/output/cli-hints-short.md +66 -0
- package/dist/output/cli-hints.js +7 -320
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +300 -257
- 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 +102 -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 -516
- 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 +1039 -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 +11 -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 -1092
- 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 +71 -50
- package/dist/registry/providers/static-index.js +53 -48
- 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 +17750 -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 +775 -37
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +5 -12
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +138 -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 +7 -0
- package/dist/tasks/backends/cron.js +203 -0
- package/dist/tasks/backends/exec-utils.js +28 -0
- package/dist/tasks/backends/index.js +24 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +187 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +215 -0
- package/dist/tasks/parser.js +211 -0
- package/dist/tasks/resolveAkmBin.js +87 -0
- package/dist/tasks/runner.js +458 -0
- package/dist/tasks/schedule.js +227 -0
- package/dist/tasks/schema.js +15 -0
- package/dist/tasks/validator.js +62 -0
- package/dist/version.js +3 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +15 -0
- package/dist/wiki/wiki.js +13 -61
- package/dist/workflows/authoring.js +8 -25
- 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 +11 -3
- package/dist/workflows/runs.js +77 -92
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +4 -8
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +10 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +48 -0
- package/docs/migration/v0.7-to-v0.8.md +1307 -0
- package/package.json +30 -12
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -381
- package/dist/commands/vault.js +0 -328
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
* Project-context resolution for search ranking.
|
|
6
|
+
*
|
|
7
|
+
* Extracts meaningful identifier tokens from the current working directory
|
|
8
|
+
* so the ranking pipeline can boost assets that are relevant to the active
|
|
9
|
+
* project. Token extraction tries, in order:
|
|
10
|
+
*
|
|
11
|
+
* 1. `.git/config` remote-origin URL basename (strips `.git` extension)
|
|
12
|
+
* 2. `package.json` `name` field (last `/`-separated segment, minus common
|
|
13
|
+
* framework suffixes)
|
|
14
|
+
* 3. Basename of the directory returned by `resolveWorkflowScopeAnchor`
|
|
15
|
+
* 4. `null` — no meaningful project context (home dir, /tmp, etc.)
|
|
16
|
+
*
|
|
17
|
+
* Tokens are lowercased, split on `[-_/]`, then filtered through a noise-word
|
|
18
|
+
* blocklist. A maximum of 5 tokens is kept.
|
|
19
|
+
*/
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import os from "node:os";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { resolveWorkflowScopeAnchor } from "../workflows/scope-key.js";
|
|
24
|
+
// Words that appear in almost every project name and carry no discriminating
|
|
25
|
+
// signal for ranking. Filtered out after token splitting.
|
|
26
|
+
const TOKEN_BLOCKLIST = new Set([
|
|
27
|
+
"my",
|
|
28
|
+
"the",
|
|
29
|
+
"app",
|
|
30
|
+
"lib",
|
|
31
|
+
"sdk",
|
|
32
|
+
"api",
|
|
33
|
+
"cli",
|
|
34
|
+
"tool",
|
|
35
|
+
"kit",
|
|
36
|
+
"core",
|
|
37
|
+
"main",
|
|
38
|
+
"index",
|
|
39
|
+
"src",
|
|
40
|
+
"test",
|
|
41
|
+
]);
|
|
42
|
+
// Common suffixes stripped from package.json `name` before tokenisation so
|
|
43
|
+
// that e.g. `akm-cli` contributes `akm` rather than `akm` + `cli` (which is
|
|
44
|
+
// in the blocklist anyway, but explicit stripping keeps the raw name shorter).
|
|
45
|
+
const STRIP_SUFFIXES = ["-cli", "-app", "-lib", "-sdk", "-plugin"];
|
|
46
|
+
const MAX_TOKENS = 5;
|
|
47
|
+
// Paths that are definitely NOT a meaningful project root (home dir, tmp).
|
|
48
|
+
// When `resolveWorkflowScopeAnchor` returns one of these we return `null`.
|
|
49
|
+
function isNoiseRoot(dir) {
|
|
50
|
+
const homedir = os.homedir();
|
|
51
|
+
const normalized = dir.replace(/\/+$/, "");
|
|
52
|
+
if (normalized === homedir || normalized === homedir.replace(/\/+$/, ""))
|
|
53
|
+
return true;
|
|
54
|
+
// /tmp or /tmp/… (any depth)
|
|
55
|
+
if (normalized === "/tmp" || normalized.startsWith("/tmp/"))
|
|
56
|
+
return true;
|
|
57
|
+
// Windows-style temp
|
|
58
|
+
const tmpdir = os.tmpdir();
|
|
59
|
+
if (normalized === tmpdir || normalized.startsWith(tmpdir + path.sep))
|
|
60
|
+
return true;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the project context for the given working directory.
|
|
65
|
+
*
|
|
66
|
+
* @param cwd - Directory to inspect. Defaults to `process.cwd()`.
|
|
67
|
+
* @param fsOverride - Optional FS override for unit testing.
|
|
68
|
+
* @returns `ProjectContext` with a non-empty `tokens` set, or `null` when no
|
|
69
|
+
* meaningful context can be derived (home dir, /tmp, extraction failed).
|
|
70
|
+
*/
|
|
71
|
+
export function resolveProjectContext(cwd, fsOverride) {
|
|
72
|
+
const effectiveCwd = cwd ?? process.cwd();
|
|
73
|
+
const readFile = fsOverride?.readFileSync ?? ((p, enc) => fs.readFileSync(p, enc));
|
|
74
|
+
// Attempt 1 — git remote-origin URL
|
|
75
|
+
const gitConfigPath = path.join(effectiveCwd, ".git", "config");
|
|
76
|
+
const gitTokens = tryExtractGitTokens(gitConfigPath, readFile);
|
|
77
|
+
if (gitTokens !== null && gitTokens.size > 0) {
|
|
78
|
+
return { tokens: gitTokens };
|
|
79
|
+
}
|
|
80
|
+
// Attempt 2 — package.json name
|
|
81
|
+
const pkgJsonPath = path.join(effectiveCwd, "package.json");
|
|
82
|
+
const pkgTokens = tryExtractPackageJsonTokens(pkgJsonPath, readFile);
|
|
83
|
+
if (pkgTokens !== null && pkgTokens.size > 0) {
|
|
84
|
+
return { tokens: pkgTokens };
|
|
85
|
+
}
|
|
86
|
+
// Attempt 3 — workflow scope anchor basename
|
|
87
|
+
try {
|
|
88
|
+
const anchor = resolveWorkflowScopeAnchor(effectiveCwd);
|
|
89
|
+
if (isNoiseRoot(anchor))
|
|
90
|
+
return null;
|
|
91
|
+
const baseName = path.basename(anchor);
|
|
92
|
+
const tokens = tokenize(baseName);
|
|
93
|
+
if (tokens.size > 0) {
|
|
94
|
+
return { tokens };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Ignore errors from scope anchor resolution (e.g. during testing).
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
// ── Private helpers ──────────────────────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* Parse `.git/config` for the `[remote "origin"]` section and extract the
|
|
105
|
+
* repo name from the `url =` line.
|
|
106
|
+
*
|
|
107
|
+
* url = git@github.com:itlackey/akm.git → "akm"
|
|
108
|
+
* url = https://github.com/itlackey/akm → "akm"
|
|
109
|
+
*/
|
|
110
|
+
function tryExtractGitTokens(gitConfigPath, readFile) {
|
|
111
|
+
try {
|
|
112
|
+
const content = readFile(gitConfigPath, "utf-8");
|
|
113
|
+
const urlMatch = extractRemoteOriginUrl(content);
|
|
114
|
+
if (!urlMatch)
|
|
115
|
+
return null;
|
|
116
|
+
// Strip .git extension, then take the last path segment.
|
|
117
|
+
const withoutGit = urlMatch.replace(/\.git$/, "");
|
|
118
|
+
const segments = withoutGit.replace(/\/$/, "").split(/[/:]/).filter(Boolean);
|
|
119
|
+
const repoName = segments[segments.length - 1] ?? "";
|
|
120
|
+
const tokens = tokenize(repoName);
|
|
121
|
+
return tokens.size > 0 ? tokens : null;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Extracts the `url =` value from the `[remote "origin"]` section of a git
|
|
129
|
+
* config file. Returns `null` when the section or key is absent.
|
|
130
|
+
*/
|
|
131
|
+
function extractRemoteOriginUrl(content) {
|
|
132
|
+
// Split into sections delimited by `[...]` headers.
|
|
133
|
+
const lines = content.split(/\r?\n/);
|
|
134
|
+
let inOrigin = false;
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
const trimmed = line.trim();
|
|
137
|
+
if (trimmed.startsWith("[")) {
|
|
138
|
+
// New section header
|
|
139
|
+
inOrigin = /^\[remote\s+"origin"\]$/i.test(trimmed);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (inOrigin) {
|
|
143
|
+
const m = trimmed.match(/^url\s*=\s*(.+)$/i);
|
|
144
|
+
if (m)
|
|
145
|
+
return m[1].trim();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Read `package.json` and extract the `name` field as tokens.
|
|
152
|
+
*/
|
|
153
|
+
function tryExtractPackageJsonTokens(pkgPath, readFile) {
|
|
154
|
+
try {
|
|
155
|
+
const content = readFile(pkgPath, "utf-8");
|
|
156
|
+
const parsed = JSON.parse(content);
|
|
157
|
+
if (typeof parsed.name !== "string" || !parsed.name)
|
|
158
|
+
return null;
|
|
159
|
+
// For scoped packages (@org/pkg-name) take the last `/`-separated segment.
|
|
160
|
+
const rawName = parsed.name.split("/").pop() ?? parsed.name;
|
|
161
|
+
// Strip common framework suffixes before tokenising.
|
|
162
|
+
let strippedName = rawName;
|
|
163
|
+
for (const suffix of STRIP_SUFFIXES) {
|
|
164
|
+
if (strippedName.endsWith(suffix)) {
|
|
165
|
+
strippedName = strippedName.slice(0, -suffix.length);
|
|
166
|
+
break; // only strip one suffix
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const tokens = tokenize(strippedName);
|
|
170
|
+
return tokens.size > 0 ? tokens : null;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Split a raw name string into lowercase tokens, then filter through the
|
|
178
|
+
* blocklist and cap at `MAX_TOKENS`.
|
|
179
|
+
*
|
|
180
|
+
* "akm-cli" → Set { "akm" }
|
|
181
|
+
* "my-app" → Set {} (all tokens blocked)
|
|
182
|
+
* "openpalm" → Set { "openpalm" }
|
|
183
|
+
*/
|
|
184
|
+
function tokenize(raw) {
|
|
185
|
+
const parts = raw
|
|
186
|
+
.toLowerCase()
|
|
187
|
+
.split(/[-_/]+/)
|
|
188
|
+
.filter(Boolean)
|
|
189
|
+
.filter((t) => !TOKEN_BLOCKLIST.has(t))
|
|
190
|
+
.slice(0, MAX_TOKENS);
|
|
191
|
+
return new Set(parts);
|
|
192
|
+
}
|
|
@@ -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 { computeGraphBoost } from "./graph-boost";
|
|
5
|
+
const TYPE_BOOST = {
|
|
6
|
+
skill: 0.4,
|
|
7
|
+
command: 0.35,
|
|
8
|
+
workflow: 0.35,
|
|
9
|
+
agent: 0.3,
|
|
10
|
+
script: 0.2,
|
|
11
|
+
knowledge: 0.22,
|
|
12
|
+
memory: -0.02,
|
|
13
|
+
};
|
|
14
|
+
const MAX_BOOST_SUM = 3.0;
|
|
15
|
+
const UTILITY_WEIGHT = 0.5;
|
|
16
|
+
const UTILITY_MAX_BOOST = 1.5;
|
|
17
|
+
/**
|
|
18
|
+
* Phase 2A / Rec 5: default recency half-life (days) used when no
|
|
19
|
+
* `utilityDecayConfig` is supplied to the ranking pipeline. Matches the
|
|
20
|
+
* pre-2A hardcoded `RECENCY_DECAY_DAYS = 30` constant — the formula is
|
|
21
|
+
* default-safe and collapses to `exp(-days / 30)` when no overrides apply.
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_RECENCY_HALF_LIFE_DAYS = 30;
|
|
24
|
+
/**
|
|
25
|
+
* Cap on the effective half-life after applying the feedback stability
|
|
26
|
+
* boost — prevents indefinite half-life inflation for memories with many
|
|
27
|
+
* positive feedback events. `effectiveHalfLife = min(halfLife * boost^count, halfLife * 4)`.
|
|
28
|
+
*/
|
|
29
|
+
const FEEDBACK_HALF_LIFE_CAP_MULTIPLIER = 4;
|
|
30
|
+
function beliefStateBoost(item) {
|
|
31
|
+
const entry = item.entry;
|
|
32
|
+
if (entry.type !== "memory")
|
|
33
|
+
return 0;
|
|
34
|
+
// Phase 1A: `asserted` and `deprecated` are first-class states.
|
|
35
|
+
// `asserted` carries stronger user-explicit authority than `active`.
|
|
36
|
+
// `deprecated` is a frozen historical state — penalized but milder than `superseded`.
|
|
37
|
+
if (entry.beliefState === "contradicted")
|
|
38
|
+
return -0.45;
|
|
39
|
+
if (entry.beliefState === "superseded")
|
|
40
|
+
return -0.25;
|
|
41
|
+
if (entry.beliefState === "archived")
|
|
42
|
+
return -0.6;
|
|
43
|
+
if (entry.beliefState === "deprecated")
|
|
44
|
+
return -0.15;
|
|
45
|
+
if (entry.beliefState === "asserted")
|
|
46
|
+
return 0.08;
|
|
47
|
+
if (entry.beliefState === "active")
|
|
48
|
+
return 0.06;
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
const exactNameRankingContributor = {
|
|
52
|
+
name: "exact-name-ranking",
|
|
53
|
+
appliesTo: () => true,
|
|
54
|
+
adjust(item, ctx) {
|
|
55
|
+
const entry = item.entry;
|
|
56
|
+
const nameLower = entry.name.toLowerCase();
|
|
57
|
+
const rawNameBase = nameLower.split("/").pop() ?? nameLower;
|
|
58
|
+
const nameBase = entry.type === "memory" && rawNameBase.endsWith(".derived")
|
|
59
|
+
? rawNameBase.slice(0, -".derived".length)
|
|
60
|
+
: rawNameBase;
|
|
61
|
+
if (nameBase === ctx.queryLower || nameLower === ctx.queryLower) {
|
|
62
|
+
return 2.0;
|
|
63
|
+
}
|
|
64
|
+
if (nameBase.includes(ctx.queryLower) || ctx.queryLower.includes(nameBase)) {
|
|
65
|
+
return 1.0;
|
|
66
|
+
}
|
|
67
|
+
const nameTokens = nameBase.split(/[-_\s]+/).filter(Boolean);
|
|
68
|
+
const matchCount = ctx.queryTokens.filter((qt) => nameTokens.some((nt) => nt === qt || nt.includes(qt))).length;
|
|
69
|
+
return matchCount > 0 ? Math.min(0.9, matchCount * 0.3) : 0;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const typeRankingContributor = {
|
|
73
|
+
name: "type-ranking",
|
|
74
|
+
appliesTo: () => true,
|
|
75
|
+
adjust(item) {
|
|
76
|
+
return TYPE_BOOST[item.entry.type] ?? 0;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const memoryRankingContributor = {
|
|
80
|
+
name: "memory-ranking",
|
|
81
|
+
appliesTo(item) {
|
|
82
|
+
return item.entry.type === "memory";
|
|
83
|
+
},
|
|
84
|
+
adjust(item) {
|
|
85
|
+
const derivedBoost = item.entry.name.toLowerCase().endsWith(".derived") ? 0.12 : -0.08;
|
|
86
|
+
return derivedBoost + beliefStateBoost(item);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
const tagRankingContributor = {
|
|
90
|
+
name: "tag-ranking",
|
|
91
|
+
appliesTo(item) {
|
|
92
|
+
return Array.isArray(item.entry.tags) && item.entry.tags.length > 0;
|
|
93
|
+
},
|
|
94
|
+
adjust(item, ctx) {
|
|
95
|
+
let tagBoost = 0;
|
|
96
|
+
for (const tag of item.entry.tags ?? []) {
|
|
97
|
+
if (ctx.queryTokens.some((token) => tag.toLowerCase() === token))
|
|
98
|
+
tagBoost += 0.15;
|
|
99
|
+
}
|
|
100
|
+
return Math.min(0.3, tagBoost);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const searchHintRankingContributor = {
|
|
104
|
+
name: "search-hint-ranking",
|
|
105
|
+
appliesTo(item) {
|
|
106
|
+
return Array.isArray(item.entry.searchHints) && item.entry.searchHints.length > 0;
|
|
107
|
+
},
|
|
108
|
+
adjust(item, ctx) {
|
|
109
|
+
let hintBoost = 0;
|
|
110
|
+
for (const hint of item.entry.searchHints ?? []) {
|
|
111
|
+
const hintLower = hint.toLowerCase();
|
|
112
|
+
for (const token of ctx.queryTokens) {
|
|
113
|
+
if (hintLower.includes(token)) {
|
|
114
|
+
hintBoost += 0.12;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return Math.min(0.24, hintBoost);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
const aliasRankingContributor = {
|
|
123
|
+
name: "alias-ranking",
|
|
124
|
+
appliesTo(item) {
|
|
125
|
+
return Array.isArray(item.entry.aliases) && item.entry.aliases.length > 0;
|
|
126
|
+
},
|
|
127
|
+
adjust(item, ctx) {
|
|
128
|
+
let boost = 0;
|
|
129
|
+
for (const alias of item.entry.aliases ?? []) {
|
|
130
|
+
const aliasLower = alias.toLowerCase();
|
|
131
|
+
if (aliasLower === ctx.queryLower) {
|
|
132
|
+
boost += 1.5;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
if (ctx.queryTokens.some((token) => aliasLower.includes(token)))
|
|
136
|
+
boost += 0.3;
|
|
137
|
+
}
|
|
138
|
+
return boost;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
const descriptionRankingContributor = {
|
|
142
|
+
name: "description-ranking",
|
|
143
|
+
appliesTo(item) {
|
|
144
|
+
return typeof item.entry.description === "string" && item.entry.description.length > 0;
|
|
145
|
+
},
|
|
146
|
+
adjust(item, ctx) {
|
|
147
|
+
const descLower = item.entry.description?.toLowerCase() ?? "";
|
|
148
|
+
const descMatchCount = ctx.queryTokens.filter((token) => descLower.includes(token)).length;
|
|
149
|
+
if (descMatchCount === ctx.queryTokens.length && ctx.queryTokens.length > 1)
|
|
150
|
+
return 0.25;
|
|
151
|
+
if (descMatchCount > 0)
|
|
152
|
+
return 0.1;
|
|
153
|
+
return 0;
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
const metadataRankingContributor = {
|
|
157
|
+
name: "metadata-ranking",
|
|
158
|
+
appliesTo: () => true,
|
|
159
|
+
adjust(item) {
|
|
160
|
+
let boost = item.entry.quality === "curated" ? 0.05 : 0;
|
|
161
|
+
if (typeof item.entry.confidence === "number") {
|
|
162
|
+
boost += Math.min(0.05, Math.max(0, item.entry.confidence) * 0.05);
|
|
163
|
+
}
|
|
164
|
+
return boost;
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
const graphRankingContributor = {
|
|
168
|
+
name: "graph-ranking",
|
|
169
|
+
appliesTo(_item, ctx) {
|
|
170
|
+
return ctx.graphContext !== null;
|
|
171
|
+
},
|
|
172
|
+
adjust(item, ctx) {
|
|
173
|
+
return ctx.graphContext ? computeGraphBoost(ctx.graphContext, item.filePath) : 0;
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Capture-mode boost — Phase 1B / Rec 7.
|
|
178
|
+
*
|
|
179
|
+
* Memories captured via the hot path (`akm remember`) get a modest additive
|
|
180
|
+
* boost so they outrank otherwise-equal background-derived memories. Memories
|
|
181
|
+
* without `captureMode` (legacy) return 0 and rank exactly as before.
|
|
182
|
+
*/
|
|
183
|
+
const captureModeRankingContributor = {
|
|
184
|
+
name: "capture-mode-ranking",
|
|
185
|
+
appliesTo(item) {
|
|
186
|
+
return item.entry.type === "memory" && item.entry.captureMode === "hot";
|
|
187
|
+
},
|
|
188
|
+
adjust() {
|
|
189
|
+
return 0.2;
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Lesson strength boost — Phase 7A / Advantage D4b.
|
|
194
|
+
*
|
|
195
|
+
* Each ref that has credited a lesson via `akm feedback --applied-to` adds
|
|
196
|
+
* 0.06 to the boost (capped at 0.3 ≈ five credits). Lessons without a
|
|
197
|
+
* `lessonStrength` array (or a number) return 0.
|
|
198
|
+
*/
|
|
199
|
+
const lessonStrengthContributor = {
|
|
200
|
+
name: "lesson-strength-ranking",
|
|
201
|
+
appliesTo(item) {
|
|
202
|
+
return (item.entry.type === "lesson" && typeof item.entry.lessonStrength === "number" && item.entry.lessonStrength > 0);
|
|
203
|
+
},
|
|
204
|
+
adjust(item) {
|
|
205
|
+
const strength = item.entry.lessonStrength ?? 0;
|
|
206
|
+
return Math.min(0.3, 0.06 * strength);
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Blend ratio for scoped vs. global utility signals.
|
|
211
|
+
*
|
|
212
|
+
* When a scoped row exists: `effectiveUtility = scoped * 0.7 + global * 0.3`
|
|
213
|
+
* This ensures the in-project signal strongly dominates while the global
|
|
214
|
+
* cold-start signal still helps when scoped history is sparse.
|
|
215
|
+
*/
|
|
216
|
+
const SCOPED_UTILITY_BLEND_SCOPED = 0.7;
|
|
217
|
+
const SCOPED_UTILITY_BLEND_GLOBAL = 1 - SCOPED_UTILITY_BLEND_SCOPED;
|
|
218
|
+
const utilityRankingContributor = {
|
|
219
|
+
name: "utility-ranking",
|
|
220
|
+
appliesTo(item, ctx) {
|
|
221
|
+
const utilScore = ctx.utilityScores.get(item.id);
|
|
222
|
+
const scopedScore = ctx.scopedUtilityScores?.get(item.id);
|
|
223
|
+
return Boolean((utilScore && utilScore.utility > 0) || (scopedScore && scopedScore.utility > 0));
|
|
224
|
+
},
|
|
225
|
+
apply(item, ctx) {
|
|
226
|
+
const utilScore = ctx.utilityScores.get(item.id);
|
|
227
|
+
const scopedScore = ctx.scopedUtilityScores?.get(item.id);
|
|
228
|
+
// Determine effective utility: prefer scoped when present, blend with global.
|
|
229
|
+
const globalUtility = utilScore?.utility ?? 0;
|
|
230
|
+
const scopedUtility = scopedScore?.utility ?? 0;
|
|
231
|
+
const effectiveUtility = scopedUtility > 0
|
|
232
|
+
? scopedUtility * SCOPED_UTILITY_BLEND_SCOPED + globalUtility * SCOPED_UTILITY_BLEND_GLOBAL
|
|
233
|
+
: globalUtility;
|
|
234
|
+
if (effectiveUtility <= 0)
|
|
235
|
+
return;
|
|
236
|
+
// Recency decay: use the global lastUsedAt for the decay factor (it's an
|
|
237
|
+
// ISO string with full resolution), falling back to scoped lastUsedAt (ms).
|
|
238
|
+
let recencyFactor = 1;
|
|
239
|
+
const lastUsedRaw = utilScore?.lastUsedAt ?? (scopedScore ? new Date(scopedScore.lastUsedAt).toISOString() : undefined);
|
|
240
|
+
if (lastUsedRaw) {
|
|
241
|
+
const lastUsedMs = new Date(lastUsedRaw).getTime();
|
|
242
|
+
const daysSinceLastUse = Number.isNaN(lastUsedMs)
|
|
243
|
+
? Infinity
|
|
244
|
+
: Math.max(0, (Date.now() - lastUsedMs) / (1000 * 60 * 60 * 24));
|
|
245
|
+
// Phase 2A / Rec 5: configurable forgetting curve with optional
|
|
246
|
+
// feedback-stability boost. Absent config + absent positive feedback
|
|
247
|
+
// collapses to `exp(-days / 30)` — pre-2A default-safe.
|
|
248
|
+
const halfLifeDays = ctx.utilityDecayConfig?.halfLifeDays ?? DEFAULT_RECENCY_HALF_LIFE_DAYS;
|
|
249
|
+
const stabilityBoost = ctx.utilityDecayConfig?.feedbackStabilityBoost ?? 1.5;
|
|
250
|
+
const positiveCount = ctx.positiveFeedbackCounts?.get(item.id) ?? 0;
|
|
251
|
+
// `boost^count` is 1 when count is 0 OR when boost is 1.0, so neither
|
|
252
|
+
// a missing feedback count nor a "no boost" config widens the half-life.
|
|
253
|
+
let stabilizedHalfLife = halfLifeDays * stabilityBoost ** positiveCount;
|
|
254
|
+
stabilizedHalfLife = Math.min(stabilizedHalfLife, halfLifeDays * FEEDBACK_HALF_LIFE_CAP_MULTIPLIER);
|
|
255
|
+
// Defensive: half-life must stay positive to avoid div-by-zero / Infinity.
|
|
256
|
+
const safeHalfLife = Math.max(0.0001, stabilizedHalfLife);
|
|
257
|
+
recencyFactor = Math.exp(-daysSinceLastUse / safeHalfLife);
|
|
258
|
+
}
|
|
259
|
+
const rawBoost = 1 + effectiveUtility * recencyFactor * UTILITY_WEIGHT;
|
|
260
|
+
item.score *= Math.min(rawBoost, UTILITY_MAX_BOOST);
|
|
261
|
+
item.utilityBoosted = true;
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* Project-context boost.
|
|
266
|
+
*
|
|
267
|
+
* Auto-boosts assets whose name, tags, aliases, or search hints contain tokens
|
|
268
|
+
* derived from the current working directory's project name. For example, when
|
|
269
|
+
* running `akm search` from the `akm` git repo, assets tagged `akm` or named
|
|
270
|
+
* `akm-*` receive an additive boost.
|
|
271
|
+
*
|
|
272
|
+
* The boost is capped at 0.5 so it can never overpower an exact-name match
|
|
273
|
+
* (which contributes 2.0). Each matching token adds 0.2 up to the cap.
|
|
274
|
+
*
|
|
275
|
+
* Skipped entirely when `projectContext` is absent or has no tokens (e.g.
|
|
276
|
+
* when running from home dir, /tmp, or when disabled via
|
|
277
|
+
* `--no-project-context` / `AKM_DISABLE_PROJECT_CONTEXT=1`).
|
|
278
|
+
*/
|
|
279
|
+
const projectContextRankingContributor = {
|
|
280
|
+
name: "project-context-ranking",
|
|
281
|
+
appliesTo(_item, ctx) {
|
|
282
|
+
return ctx.projectContext != null && ctx.projectContext.tokens.size > 0;
|
|
283
|
+
},
|
|
284
|
+
adjust(item, ctx) {
|
|
285
|
+
if (!ctx.projectContext)
|
|
286
|
+
return 0;
|
|
287
|
+
const fields = [
|
|
288
|
+
item.entry.name ?? "",
|
|
289
|
+
...(item.entry.tags ?? []),
|
|
290
|
+
...(item.entry.aliases ?? []),
|
|
291
|
+
...(item.entry.searchHints ?? []),
|
|
292
|
+
].map((s) => s.toLowerCase());
|
|
293
|
+
let hits = 0;
|
|
294
|
+
for (const token of ctx.projectContext.tokens) {
|
|
295
|
+
if (fields.some((f) => f.includes(token)))
|
|
296
|
+
hits++;
|
|
297
|
+
}
|
|
298
|
+
return Math.min(0.5, hits * 0.2);
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
export const defaultRankingContributors = [
|
|
302
|
+
exactNameRankingContributor,
|
|
303
|
+
typeRankingContributor,
|
|
304
|
+
memoryRankingContributor,
|
|
305
|
+
tagRankingContributor,
|
|
306
|
+
searchHintRankingContributor,
|
|
307
|
+
aliasRankingContributor,
|
|
308
|
+
descriptionRankingContributor,
|
|
309
|
+
metadataRankingContributor,
|
|
310
|
+
graphRankingContributor,
|
|
311
|
+
captureModeRankingContributor,
|
|
312
|
+
lessonStrengthContributor,
|
|
313
|
+
projectContextRankingContributor,
|
|
314
|
+
];
|
|
315
|
+
export const defaultUtilityRankingContributors = [utilityRankingContributor];
|
|
316
|
+
export function applyScoreContributors(item, ctx, contributors = defaultRankingContributors) {
|
|
317
|
+
let boostSum = 0;
|
|
318
|
+
for (const contributor of contributors) {
|
|
319
|
+
if (!contributor.appliesTo(item, ctx))
|
|
320
|
+
continue;
|
|
321
|
+
boostSum += contributor.adjust(item, ctx);
|
|
322
|
+
}
|
|
323
|
+
item.score *= 1 + Math.min(boostSum, MAX_BOOST_SUM);
|
|
324
|
+
}
|
|
325
|
+
export function applyUtilityContributors(item, ctx, contributors = defaultUtilityRankingContributors) {
|
|
326
|
+
for (const contributor of contributors) {
|
|
327
|
+
if (!contributor.appliesTo(item, ctx))
|
|
328
|
+
continue;
|
|
329
|
+
contributor.apply(item, ctx);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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 { getUtilityScoresByIds } from "./db";
|
|
5
|
+
import { applyScoreContributors, applyUtilityContributors } from "./ranking-contributors";
|
|
6
|
+
export function normalizeFtsScores(results) {
|
|
7
|
+
const ftsScoreMap = new Map();
|
|
8
|
+
if (results.length === 0)
|
|
9
|
+
return ftsScoreMap;
|
|
10
|
+
const bestBm25 = results[0].bm25Score;
|
|
11
|
+
const worstBm25 = results[results.length - 1].bm25Score;
|
|
12
|
+
const range = bestBm25 - worstBm25;
|
|
13
|
+
for (const result of results) {
|
|
14
|
+
const normalized = range !== 0 ? (result.bm25Score - worstBm25) / range : 1.0;
|
|
15
|
+
const ftsScore = 0.3 + normalized * 0.7;
|
|
16
|
+
ftsScoreMap.set(result.id, { score: ftsScore, result });
|
|
17
|
+
}
|
|
18
|
+
return ftsScoreMap;
|
|
19
|
+
}
|
|
20
|
+
export function combineSearchScores(options) {
|
|
21
|
+
const FTS_WEIGHT = 0.7;
|
|
22
|
+
const VEC_WEIGHT = 0.3;
|
|
23
|
+
const scored = [];
|
|
24
|
+
const seenIds = new Set();
|
|
25
|
+
for (const [id, { score: ftsScore, result }] of options.ftsScoreMap) {
|
|
26
|
+
seenIds.add(id);
|
|
27
|
+
const embedScore = options.embedScoreMap.get(id);
|
|
28
|
+
const combinedScore = embedScore !== undefined ? ftsScore * FTS_WEIGHT + embedScore * VEC_WEIGHT : ftsScore;
|
|
29
|
+
scored.push({
|
|
30
|
+
id,
|
|
31
|
+
entry: result.entry,
|
|
32
|
+
filePath: result.filePath,
|
|
33
|
+
score: combinedScore,
|
|
34
|
+
rankingMode: embedScore !== undefined ? "hybrid" : "fts",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
for (const [id, cosine] of options.embedScoreMap) {
|
|
38
|
+
if (seenIds.has(id))
|
|
39
|
+
continue;
|
|
40
|
+
const found = options.getEntryById(id);
|
|
41
|
+
if (!found)
|
|
42
|
+
continue;
|
|
43
|
+
if (options.typeFilter && found.entry.type !== options.typeFilter)
|
|
44
|
+
continue;
|
|
45
|
+
scored.push({
|
|
46
|
+
id,
|
|
47
|
+
entry: found.entry,
|
|
48
|
+
filePath: found.filePath,
|
|
49
|
+
score: cosine * VEC_WEIGHT,
|
|
50
|
+
rankingMode: "semantic",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return scored;
|
|
54
|
+
}
|
|
55
|
+
export function applyRankingRules(options) {
|
|
56
|
+
const queryTokens = options.query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
57
|
+
const queryLower = options.query.toLowerCase().trim();
|
|
58
|
+
const rankingContext = {
|
|
59
|
+
db: options.db,
|
|
60
|
+
query: options.query,
|
|
61
|
+
queryLower,
|
|
62
|
+
queryTokens,
|
|
63
|
+
graphContext: options.graphContext,
|
|
64
|
+
projectContext: options.projectContext,
|
|
65
|
+
};
|
|
66
|
+
for (const item of options.items) {
|
|
67
|
+
applyScoreContributors(item, rankingContext);
|
|
68
|
+
}
|
|
69
|
+
const { global: utilScoresMap, scoped: scopedUtilScoresMap } = getUtilityScoresByIds(options.db, options.items.map((item) => item.id), options.scopeKey);
|
|
70
|
+
const utilityContext = {
|
|
71
|
+
...rankingContext,
|
|
72
|
+
utilityScores: utilScoresMap,
|
|
73
|
+
scopedUtilityScores: scopedUtilScoresMap,
|
|
74
|
+
utilityDecayConfig: options.utilityDecayConfig,
|
|
75
|
+
positiveFeedbackCounts: options.positiveFeedbackCounts,
|
|
76
|
+
};
|
|
77
|
+
for (const item of options.items) {
|
|
78
|
+
applyUtilityContributors(item, utilityContext);
|
|
79
|
+
}
|
|
80
|
+
return options.items;
|
|
81
|
+
}
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Extracted from indexer.ts to break the circular dependency:
|
|
5
|
-
* db.ts -> indexer.ts -> db.ts
|
|
6
|
-
*
|
|
7
|
-
* This module imports only from metadata.ts (for the StashEntry type),
|
|
8
|
-
* so it can be safely imported by both db.ts and indexer.ts.
|
|
9
|
-
*/
|
|
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/.
|
|
10
4
|
/**
|
|
11
5
|
* Return per-field search text for multi-column FTS5 indexing.
|
|
12
6
|
*
|
|
@@ -45,6 +39,8 @@ export function buildSearchFields(entry) {
|
|
|
45
39
|
hintParts.push(entry.xrefs.join(" "));
|
|
46
40
|
if (entry.pageKind)
|
|
47
41
|
hintParts.push(entry.pageKind);
|
|
42
|
+
if (entry.whenToUse)
|
|
43
|
+
hintParts.push(entry.whenToUse);
|
|
48
44
|
const hints = hintParts.join(" ").toLowerCase();
|
|
49
45
|
const contentParts = [];
|
|
50
46
|
if (entry.toc) {
|