akm-cli 0.7.5 → 0.8.0-rc.11
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 +178 -256
- 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
|
@@ -1,34 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Extracted from source-search.ts to break the circular import:
|
|
5
|
-
* source-search.ts → sources/providers/filesystem.ts → db-search.ts (no cycle)
|
|
6
|
-
*
|
|
7
|
-
* source-search.ts imports this module for the `searchLocal` export.
|
|
8
|
-
* sources/providers/filesystem.ts also imports `searchLocal` from here.
|
|
9
|
-
*
|
|
10
|
-
* Renamed from `local-search.ts` to signal that this is the DB-layer search
|
|
11
|
-
* implementation, not a "local vs. remote" distinction.
|
|
12
|
-
*/
|
|
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/.
|
|
13
4
|
import fs from "node:fs";
|
|
5
|
+
import { buildActionFromContributors, defaultActionContributors } from "../core/action-contributors";
|
|
14
6
|
import { makeAssetRef } from "../core/asset-ref";
|
|
15
7
|
import { defaultRendererRegistry } from "../core/asset-registry";
|
|
16
8
|
import { getDbPath } from "../core/paths";
|
|
17
9
|
import { warn } from "../core/warn";
|
|
18
|
-
import {
|
|
10
|
+
import { getCurrentWorkflowScopeKey } from "../workflows/scope-key";
|
|
11
|
+
import { closeDatabase, getAllEntries, getEntryById, getEntryCount, getMeta, getPositiveFeedbackCountsByIds, openExistingDatabase, sanitizeFtsQuery, searchFts, searchVec, } from "./db";
|
|
19
12
|
import { ensureIndex } from "./ensure-index";
|
|
20
|
-
import {
|
|
21
|
-
import { computeGraphBoost, loadGraphBoostContext } from "./graph-boost";
|
|
13
|
+
import { collectGraphRelatedHit, computeGraphBoost, loadGraphBoostContext, } from "./graph-boost";
|
|
22
14
|
import { isProposedQuality } from "./metadata";
|
|
15
|
+
import { resolveProjectContext } from "./project-context";
|
|
16
|
+
import { applyRankingRules, combineSearchScores, normalizeFtsScores } from "./ranking";
|
|
17
|
+
import { enrichSearchHit } from "./search-hit-enrichers";
|
|
23
18
|
import { buildEditHint, findSourceForPath, isEditable } from "./search-source";
|
|
24
19
|
import { deriveSemanticProviderFingerprint, getEffectiveSemanticStatus, isSemanticRuntimeReady, readSemanticStatus, } from "./semantic-status";
|
|
25
|
-
export async function rendererForType(type, registry = defaultRendererRegistry) {
|
|
26
|
-
const name = registry.rendererNameFor(type);
|
|
27
|
-
return name ? getRenderer(name) : undefined;
|
|
28
|
-
}
|
|
29
20
|
export function buildLocalAction(type, ref, registry = defaultRendererRegistry) {
|
|
30
|
-
|
|
31
|
-
return builder ? builder(ref) : `akm show ${ref}`;
|
|
21
|
+
return buildActionFromContributors({ type, ref }, defaultActionContributors(registry)) ?? `akm show ${ref}`;
|
|
32
22
|
}
|
|
33
23
|
function resolveSearchHitRef(entry, refName, source) {
|
|
34
24
|
if (source?.wikiName) {
|
|
@@ -39,11 +29,30 @@ function resolveSearchHitRef(entry, refName, source) {
|
|
|
39
29
|
function resolveSearchHitOrigin(source) {
|
|
40
30
|
return source?.wikiName ? null : (source?.registryId ?? null);
|
|
41
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Phase 2A / Rec 5: gate for the per-search `getPositiveFeedbackCountsByIds`
|
|
34
|
+
* lookup. Returns `true` only when the user has explicitly opted into
|
|
35
|
+
* `improve.utilityDecay` AND configured a `feedbackStabilityBoost > 1.0`.
|
|
36
|
+
* Either condition being false makes the DB query pure overhead (the ranking
|
|
37
|
+
* contributor ignores `positiveFeedbackCounts` when `utilityDecayConfig` is
|
|
38
|
+
* absent, and `1.0^count == 1` collapses the boost into a no-op).
|
|
39
|
+
*
|
|
40
|
+
* Exported for unit testing — keeps the gate decision pinned so a future edit
|
|
41
|
+
* can't quietly broaden the hot path.
|
|
42
|
+
*/
|
|
43
|
+
export function shouldQueryPositiveFeedbackCounts(utilityDecayRaw) {
|
|
44
|
+
if (utilityDecayRaw === undefined)
|
|
45
|
+
return false;
|
|
46
|
+
const boost = utilityDecayRaw.feedbackStabilityBoost ?? 1.5;
|
|
47
|
+
return boost > 1.0;
|
|
48
|
+
}
|
|
42
49
|
// ── Main search entrypoint ───────────────────────────────────────────────────
|
|
43
50
|
export async function searchLocal(input) {
|
|
44
51
|
const { query, searchType, limit, stashDir, sources, config } = input;
|
|
45
52
|
const filters = input.filters;
|
|
46
53
|
const includeProposed = input.includeProposed === true;
|
|
54
|
+
const beliefFilter = input.beliefFilter ?? "all";
|
|
55
|
+
const restrictToSources = input.restrictToSources === true;
|
|
47
56
|
const rendererRegistry = input.rendererRegistry ?? defaultRendererRegistry;
|
|
48
57
|
const allSourceDirs = sources.map((s) => s.path);
|
|
49
58
|
const rawStatus = readSemanticStatus();
|
|
@@ -54,8 +63,18 @@ export async function searchLocal(input) {
|
|
|
54
63
|
if (rawStatus && rawStatus.providerFingerprint !== currentFingerprint) {
|
|
55
64
|
warnings.push("Embedding config changed. Run 'akm index --full' to rebuild the semantic index with the new provider.");
|
|
56
65
|
}
|
|
66
|
+
else if (!config.embedding?.endpoint || !config.embedding?.model) {
|
|
67
|
+
// #480: when semantic mode is `auto` but no embedding provider is
|
|
68
|
+
// configured (e.g. `akm setup --yes` ran without picking one), telling
|
|
69
|
+
// the user to "run akm setup" is misleading — they just did. Surface
|
|
70
|
+
// the actual remediation: configure an embedding endpoint OR switch
|
|
71
|
+
// semanticSearchMode to `off` to silence the warning.
|
|
72
|
+
warnings.push("Semantic search is enabled (semanticSearchMode='auto') but no embedding provider is configured. " +
|
|
73
|
+
'Either: (a) `akm config set embedding \'{"endpoint":"...","model":"..."}\'`, or ' +
|
|
74
|
+
"(b) `akm config set semanticSearchMode off` to use keyword-only search.");
|
|
75
|
+
}
|
|
57
76
|
else {
|
|
58
|
-
warnings.push("Semantic search is pending verification. Run 'akm
|
|
77
|
+
warnings.push("Semantic search is pending verification. Run 'akm index --full' to build the semantic index now, or wait for the next background index pass.");
|
|
59
78
|
}
|
|
60
79
|
}
|
|
61
80
|
if (config.semanticSearchMode === "auto" && semanticStatus === "blocked") {
|
|
@@ -69,6 +88,7 @@ export async function searchLocal(input) {
|
|
|
69
88
|
hits: [],
|
|
70
89
|
tip: "No search index available. Run 'akm index' to build one.",
|
|
71
90
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
91
|
+
mode: "keyword",
|
|
72
92
|
};
|
|
73
93
|
}
|
|
74
94
|
const db = openExistingDatabase(dbPath);
|
|
@@ -79,9 +99,10 @@ export async function searchLocal(input) {
|
|
|
79
99
|
hits: [],
|
|
80
100
|
tip: "Index is empty. Run 'akm index' to populate it.",
|
|
81
101
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
102
|
+
mode: "keyword",
|
|
82
103
|
};
|
|
83
104
|
}
|
|
84
|
-
const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed);
|
|
105
|
+
const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources);
|
|
85
106
|
return {
|
|
86
107
|
hits,
|
|
87
108
|
tip: hits.length === 0
|
|
@@ -90,6 +111,7 @@ export async function searchLocal(input) {
|
|
|
90
111
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
91
112
|
embedMs,
|
|
92
113
|
rankMs,
|
|
114
|
+
mode: embedMs !== undefined && embedMs > 0 ? "semantic" : "keyword",
|
|
93
115
|
};
|
|
94
116
|
}
|
|
95
117
|
finally {
|
|
@@ -97,7 +119,7 @@ export async function searchLocal(input) {
|
|
|
97
119
|
}
|
|
98
120
|
}
|
|
99
121
|
// ── Database search ─────────────────────────────────────────────────────────
|
|
100
|
-
async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry, filters, includeProposed = false) {
|
|
122
|
+
async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry, filters, includeProposed = false, beliefFilter = "all", restrictToSources = false) {
|
|
101
123
|
const hasSearchableTokens = query.length > 0 && sanitizeFtsQuery(query).length > 0;
|
|
102
124
|
// Empty queries — including ones that sanitize down to no searchable FTS
|
|
103
125
|
// tokens such as "." — should enumerate matching entries instead of
|
|
@@ -113,18 +135,26 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
113
135
|
seenFilePaths.add(ie.filePath);
|
|
114
136
|
return true;
|
|
115
137
|
});
|
|
138
|
+
// Source filter: when the caller narrowed `sources` via `--source <name>`,
|
|
139
|
+
// drop entries whose filePath does not live under any of the requested
|
|
140
|
+
// sources. The FTS index spans every configured source, so without this
|
|
141
|
+
// filter a narrowed --source request would still leak results.
|
|
142
|
+
const sourceFiltered = restrictToSources
|
|
143
|
+
? uniqueEntries.filter((ie) => findSourceForPath(ie.filePath, sources) !== undefined)
|
|
144
|
+
: uniqueEntries;
|
|
116
145
|
// Scope filter: drop entries whose stored scope does not satisfy every
|
|
117
146
|
// supplied scope key. Filtering happens BEFORE the limit slice so a
|
|
118
147
|
// restrictive filter still returns up to `limit` results.
|
|
119
148
|
const scopeFiltered = filters
|
|
120
|
-
?
|
|
121
|
-
:
|
|
149
|
+
? sourceFiltered.filter((ie) => entryMatchesScope(ie.entry.scope, filters))
|
|
150
|
+
: sourceFiltered;
|
|
122
151
|
// Proposed-quality filter (v1 spec §4.2): exclude entries with
|
|
123
152
|
// `quality: "proposed"` unless the caller explicitly opts in.
|
|
124
153
|
const qualityFiltered = includeProposed
|
|
125
154
|
? scopeFiltered
|
|
126
155
|
: scopeFiltered.filter((ie) => !isProposedQuality(ie.entry.quality));
|
|
127
|
-
const
|
|
156
|
+
const beliefFiltered = qualityFiltered.filter((ie) => matchBeliefFilter(ie.entry.type, ie.entry.beliefState, beliefFilter));
|
|
157
|
+
const selected = beliefFiltered.slice(0, limit);
|
|
128
158
|
const hits = await Promise.all(selected.map((ie) => buildDbHit({
|
|
129
159
|
entry: ie.entry,
|
|
130
160
|
path: ie.filePath,
|
|
@@ -136,6 +166,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
136
166
|
sources,
|
|
137
167
|
config,
|
|
138
168
|
rendererRegistry,
|
|
169
|
+
db,
|
|
139
170
|
})));
|
|
140
171
|
return { hits };
|
|
141
172
|
}
|
|
@@ -151,22 +182,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
151
182
|
// ── Score normalization ──────────────────────────────────────────────
|
|
152
183
|
// Normalized BM25 + cosine similarity with weighted addition
|
|
153
184
|
// (FTS 0.7, vector 0.3) for well-differentiated combined scores.
|
|
154
|
-
|
|
155
|
-
const ftsScoreMap = new Map();
|
|
156
|
-
if (ftsResults.length > 0) {
|
|
157
|
-
// BM25 scores are negative; most negative = best match
|
|
158
|
-
const bestBm25 = ftsResults[0].bm25Score; // most negative (best)
|
|
159
|
-
const worstBm25 = ftsResults[ftsResults.length - 1].bm25Score; // least negative (worst)
|
|
160
|
-
const range = bestBm25 - worstBm25; // negative range
|
|
161
|
-
for (const r of ftsResults) {
|
|
162
|
-
// Normalize: best match = 1.0, worst match approaches 0
|
|
163
|
-
// When range is 0 (all same score), all get 1.0
|
|
164
|
-
const normalized = range !== 0 ? (r.bm25Score - worstBm25) / range : 1.0;
|
|
165
|
-
// Scale to 0.3-1.0 range so even the worst FTS hit has a meaningful base score
|
|
166
|
-
const ftsScore = 0.3 + normalized * 0.7;
|
|
167
|
-
ftsScoreMap.set(r.id, { score: ftsScore, result: r });
|
|
168
|
-
}
|
|
169
|
-
}
|
|
185
|
+
const ftsScoreMap = normalizeFtsScores(ftsResults);
|
|
170
186
|
// Build embedding score map (cosine similarities already 0-1)
|
|
171
187
|
const embedScoreMap = new Map();
|
|
172
188
|
if (embeddingScores) {
|
|
@@ -175,46 +191,12 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
175
191
|
}
|
|
176
192
|
}
|
|
177
193
|
// ── Combine FTS + vector scores ──────────────────────────────────────
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
for (const [id, { score: ftsScore, result }] of ftsScoreMap) {
|
|
185
|
-
seenIds.add(id);
|
|
186
|
-
const embedScore = embedScoreMap.get(id);
|
|
187
|
-
let combinedScore;
|
|
188
|
-
let rankingMode;
|
|
189
|
-
if (embedScore !== undefined) {
|
|
190
|
-
combinedScore = ftsScore * FTS_WEIGHT + embedScore * VEC_WEIGHT;
|
|
191
|
-
rankingMode = "hybrid";
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
combinedScore = ftsScore;
|
|
195
|
-
rankingMode = "fts";
|
|
196
|
-
}
|
|
197
|
-
scored.push({ id, entry: result.entry, filePath: result.filePath, score: combinedScore, rankingMode });
|
|
198
|
-
}
|
|
199
|
-
// Add vec-only results not already in FTS results
|
|
200
|
-
if (embeddingScores) {
|
|
201
|
-
for (const [id, cosine] of embeddingScores) {
|
|
202
|
-
if (seenIds.has(id))
|
|
203
|
-
continue;
|
|
204
|
-
const found = getEntryById(db, id);
|
|
205
|
-
if (found) {
|
|
206
|
-
if (typeFilter && found.entry.type !== typeFilter)
|
|
207
|
-
continue;
|
|
208
|
-
scored.push({
|
|
209
|
-
id,
|
|
210
|
-
entry: found.entry,
|
|
211
|
-
filePath: found.filePath,
|
|
212
|
-
score: cosine * VEC_WEIGHT, // Only vector score, no FTS
|
|
213
|
-
rankingMode: "semantic",
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
194
|
+
const scored = combineSearchScores({
|
|
195
|
+
ftsScoreMap,
|
|
196
|
+
embedScoreMap,
|
|
197
|
+
getEntryById: (id) => getEntryById(db, id) ?? undefined,
|
|
198
|
+
typeFilter,
|
|
199
|
+
});
|
|
218
200
|
// ── Scoring Phase ──────────────────────────────────────────────────────
|
|
219
201
|
// Apply boosts as multiplicative factors (all boosts in a single phase
|
|
220
202
|
// so that sort order and displayed scores are always consistent).
|
|
@@ -223,8 +205,6 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
223
205
|
// user's intent. An exact name match is the strongest signal. Actionable
|
|
224
206
|
// asset types (skills, commands, agents) are more useful than passive
|
|
225
207
|
// reference docs. Curated metadata is more reliable than auto-generated.
|
|
226
|
-
const queryTokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
227
|
-
const queryLower = query.toLowerCase().trim();
|
|
228
208
|
// Graph boost context (#207). Built once per query and reused across
|
|
229
209
|
// every scored entry so the disk read + JSON parse only happens once
|
|
230
210
|
// per search invocation. `null` when no graph file is present, when
|
|
@@ -237,195 +217,97 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
237
217
|
// Search across all source dirs; the graph file lives next to the
|
|
238
218
|
// primary source root. Cache misses are silent — the helper handles
|
|
239
219
|
// missing files internally and returns `null` instead of throwing.
|
|
240
|
-
|
|
241
|
-
if (!primaryDir)
|
|
220
|
+
if (allSourceDirs.length === 0)
|
|
242
221
|
return null;
|
|
243
|
-
return loadGraphBoostContext(
|
|
222
|
+
return loadGraphBoostContext(allSourceDirs, query, config, db);
|
|
244
223
|
})();
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// Proportional to how many query tokens match (0.3 per token, max 0.9)
|
|
270
|
-
boostSum += Math.min(0.9, matchCount * 0.3);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
// ── 2. Type relevance boost ──
|
|
274
|
-
// Actionable assets (skills, commands, agents) are generally more useful
|
|
275
|
-
// than passive reference material when the user is searching for something
|
|
276
|
-
// to use. Knowledge docs are reference — valuable but secondary.
|
|
277
|
-
const TYPE_BOOST = {
|
|
278
|
-
skill: 0.4,
|
|
279
|
-
command: 0.35,
|
|
280
|
-
workflow: 0.35,
|
|
281
|
-
agent: 0.3,
|
|
282
|
-
script: 0.2,
|
|
283
|
-
memory: 0.1,
|
|
284
|
-
knowledge: 0,
|
|
285
|
-
};
|
|
286
|
-
boostSum += TYPE_BOOST[entry.type] ?? 0;
|
|
287
|
-
// ── 2.5. Derived-vs-raw memory preference ──
|
|
288
|
-
// Raw memories are user notes and may be incomplete or unvetted. Compressed
|
|
289
|
-
// `.derived` memories are the higher-signal retrieval target, but the
|
|
290
|
-
// preference should stay modest so stronger relevance signals still dominate.
|
|
291
|
-
if (entry.type === "memory") {
|
|
292
|
-
if (entry.name.toLowerCase().endsWith(".derived")) {
|
|
293
|
-
boostSum += 0.18;
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
boostSum -= 0.08;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
// ── 3. Tag exact match ──
|
|
300
|
-
// Exact tag equality is a strong signal — the author explicitly tagged
|
|
301
|
-
// this asset with the user's search term.
|
|
302
|
-
if (entry.tags) {
|
|
303
|
-
let tagBoost = 0;
|
|
304
|
-
for (const tag of entry.tags) {
|
|
305
|
-
if (queryTokens.some((t) => tag.toLowerCase() === t)) {
|
|
306
|
-
tagBoost += 0.15;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
boostSum += Math.min(0.3, tagBoost);
|
|
310
|
-
}
|
|
311
|
-
// ── 4. Search hint match ──
|
|
312
|
-
// Hints are author-curated retrieval cues (e.g. "use when deploying to k8s").
|
|
313
|
-
if (entry.searchHints) {
|
|
314
|
-
let hintBoost = 0;
|
|
315
|
-
for (const hint of entry.searchHints) {
|
|
316
|
-
const hintLower = hint.toLowerCase();
|
|
317
|
-
for (const token of queryTokens) {
|
|
318
|
-
if (hintLower.includes(token)) {
|
|
319
|
-
hintBoost += 0.12;
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
boostSum += Math.min(0.24, hintBoost);
|
|
325
|
-
}
|
|
326
|
-
// ── 5. Alias match ──
|
|
327
|
-
// Aliases are alternate names the author defined for discovery.
|
|
328
|
-
if (entry.aliases) {
|
|
329
|
-
for (const alias of entry.aliases) {
|
|
330
|
-
const aliasLower = alias.toLowerCase();
|
|
331
|
-
if (aliasLower === queryLower) {
|
|
332
|
-
boostSum += 1.5; // Nearly as strong as exact name match
|
|
333
|
-
break;
|
|
334
|
-
}
|
|
335
|
-
if (queryTokens.some((t) => aliasLower.includes(t))) {
|
|
336
|
-
boostSum += 0.3;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
// ── 6. Description relevance ──
|
|
341
|
-
// All query tokens appearing in description suggests strong relevance.
|
|
342
|
-
if (entry.description) {
|
|
343
|
-
const descLower = entry.description.toLowerCase();
|
|
344
|
-
const descMatchCount = queryTokens.filter((t) => descLower.includes(t)).length;
|
|
345
|
-
if (descMatchCount === queryTokens.length && queryTokens.length > 1) {
|
|
346
|
-
// All query tokens found in description — high relevance
|
|
347
|
-
boostSum += 0.25;
|
|
348
|
-
}
|
|
349
|
-
else if (descMatchCount > 0) {
|
|
350
|
-
boostSum += 0.1;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
// ── 7. Metadata quality signals ──
|
|
354
|
-
// Curated metadata is the only boost-bearing quality marker. `generated`
|
|
355
|
-
// and `proposed` (and unknown values) get no boost. `proposed` is also
|
|
356
|
-
// filtered out by default downstream (v1 spec §4.2).
|
|
357
|
-
const qualityBoost = entry.quality === "curated" ? 0.05 : 0;
|
|
358
|
-
boostSum += qualityBoost;
|
|
359
|
-
const confidenceBoost = typeof entry.confidence === "number" ? Math.min(0.05, Math.max(0, entry.confidence) * 0.05) : 0;
|
|
360
|
-
boostSum += confidenceBoost;
|
|
361
|
-
// ── 8. Graph signal (opt-in, #207) ──
|
|
362
|
-
// When the graph-extraction pass has produced a `graph.json`,
|
|
363
|
-
// contribute an additive boost based on how many of this entry's
|
|
364
|
-
// extracted entities match the query (or are one hop away from a
|
|
365
|
-
// match). Computed inside the same loop so all boosts are in one
|
|
366
|
-
// place and the per-call cost is one map lookup when the graph is
|
|
367
|
-
// absent. There is no parallel scoring track — `boostSum` is the
|
|
368
|
-
// single accumulator and the existing `MAX_BOOST_SUM` cap below
|
|
369
|
-
// applies to graph contributions exactly as it does to every other
|
|
370
|
-
// boost.
|
|
371
|
-
if (graphContext) {
|
|
372
|
-
boostSum += computeGraphBoost(graphContext, item.filePath);
|
|
373
|
-
}
|
|
374
|
-
const cappedBoost = Math.min(boostSum, MAX_BOOST_SUM);
|
|
375
|
-
item.score = item.score * (1 + cappedBoost);
|
|
224
|
+
// Resolve project-context tokens from the current working directory once
|
|
225
|
+
// per search invocation. Returns null when running from home dir / /tmp,
|
|
226
|
+
// or when the caller has set AKM_DISABLE_PROJECT_CONTEXT=1.
|
|
227
|
+
const projectContext = process.env.AKM_DISABLE_PROJECT_CONTEXT === "1" ? null : resolveProjectContext(process.cwd());
|
|
228
|
+
// Phase 2A / Rec 5: resolve forgetting-curve config and skip the feedback
|
|
229
|
+
// count query when the boost cannot make a difference (default ≤ 1.0 means
|
|
230
|
+
// boost^count == 1 — zero overhead for the common case).
|
|
231
|
+
const utilityDecayRaw = config.improve?.utilityDecay;
|
|
232
|
+
const halfLifeDays = utilityDecayRaw?.halfLifeDays ?? 30;
|
|
233
|
+
const feedbackStabilityBoost = utilityDecayRaw?.feedbackStabilityBoost ?? 1.5;
|
|
234
|
+
const utilityDecayConfig = utilityDecayRaw !== undefined ? { halfLifeDays, feedbackStabilityBoost } : undefined;
|
|
235
|
+
// Gate the feedback-count query on the user having explicitly opted into
|
|
236
|
+
// utilityDecay. Without an opt-in, `utilityDecayConfig` is undefined and the
|
|
237
|
+
// ranking contributor ignores `positiveFeedbackCounts` — so running the DB
|
|
238
|
+
// query here would be pure overhead. The boost > 1.0 sub-gate then skips the
|
|
239
|
+
// query when the configured boost is a no-op (1.5^count when boost==1 is 1).
|
|
240
|
+
const positiveFeedbackCounts = shouldQueryPositiveFeedbackCounts(utilityDecayRaw)
|
|
241
|
+
? getPositiveFeedbackCountsByIds(db, scored.map((item) => item.id))
|
|
242
|
+
: undefined;
|
|
243
|
+
// Resolve per-project scope key for scoped utility scoring.
|
|
244
|
+
// AKM_DISABLE_SCOPED_UTILITY=1 opts out (e.g. for registry searches or tests).
|
|
245
|
+
let scopeKey;
|
|
246
|
+
try {
|
|
247
|
+
scopeKey = process.env.AKM_DISABLE_SCOPED_UTILITY === "1" ? undefined : getCurrentWorkflowScopeKey();
|
|
376
248
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
// utility factor based on aggregated usage telemetry.
|
|
380
|
-
// Batch-load all utility scores in one query to avoid N+1.
|
|
381
|
-
const UTILITY_WEIGHT = 0.5;
|
|
382
|
-
const UTILITY_MAX_BOOST = 1.5; // Cap at 1.5x multiplier
|
|
383
|
-
const RECENCY_DECAY_DAYS = 30;
|
|
384
|
-
const utilScoresMap = getUtilityScoresByIds(db, scored.map((s) => s.id));
|
|
385
|
-
for (const item of scored) {
|
|
386
|
-
const utilScore = utilScoresMap.get(item.id);
|
|
387
|
-
if (utilScore && utilScore.utility > 0) {
|
|
388
|
-
// Compute recency factor: exponential decay based on days since last use
|
|
389
|
-
let recencyFactor = 1;
|
|
390
|
-
if (utilScore.lastUsedAt) {
|
|
391
|
-
const lastUsedMs = new Date(utilScore.lastUsedAt).getTime();
|
|
392
|
-
const daysSinceLastUse = Number.isNaN(lastUsedMs)
|
|
393
|
-
? Infinity
|
|
394
|
-
: Math.max(0, (Date.now() - lastUsedMs) / (1000 * 60 * 60 * 24));
|
|
395
|
-
recencyFactor = Math.exp(-daysSinceLastUse / RECENCY_DECAY_DAYS);
|
|
396
|
-
}
|
|
397
|
-
// Compute raw utility boost and cap it
|
|
398
|
-
const rawBoost = 1 + utilScore.utility * recencyFactor * UTILITY_WEIGHT;
|
|
399
|
-
const cappedBoost = Math.min(rawBoost, UTILITY_MAX_BOOST);
|
|
400
|
-
item.score = item.score * cappedBoost;
|
|
401
|
-
item.utilityBoosted = true;
|
|
402
|
-
}
|
|
249
|
+
catch {
|
|
250
|
+
// Non-fatal — ranking proceeds without scoped utility on any error.
|
|
403
251
|
}
|
|
252
|
+
applyRankingRules({
|
|
253
|
+
db,
|
|
254
|
+
query,
|
|
255
|
+
items: scored,
|
|
256
|
+
graphContext,
|
|
257
|
+
projectContext,
|
|
258
|
+
utilityDecayConfig,
|
|
259
|
+
positiveFeedbackCounts,
|
|
260
|
+
scopeKey,
|
|
261
|
+
});
|
|
404
262
|
// ── minScore floor ──────────────────────────────────────────────────────
|
|
405
263
|
// Drop semantic-only hits (cosine-only, no FTS match) whose score falls
|
|
406
264
|
// below the configured floor. FTS hits and hybrid hits are always kept.
|
|
407
265
|
// Default floor: 0.2. Set search.minScore = 0 in config to disable.
|
|
408
266
|
const minScore = config.search?.minScore ?? 0.2;
|
|
409
267
|
const preFilter = minScore > 0 ? scored.filter((item) => item.rankingMode !== "semantic" || item.score >= minScore) : scored;
|
|
410
|
-
// Deterministic tiebreaker on equal scores
|
|
411
|
-
|
|
268
|
+
// Deterministic tiebreaker on equal scores.
|
|
269
|
+
//
|
|
270
|
+
// CRITICAL: sort on the SAME clamped+rounded value the user sees (see the
|
|
271
|
+
// `finalScore`/round-to-4dp logic below at buildDbHit), NOT the raw pre-clamp
|
|
272
|
+
// `item.score`. The boost loop can push scores above 1.0 (utility, graph,
|
|
273
|
+
// project boosts) and carries ~15 significant digits. Two entries that DISPLAY
|
|
274
|
+
// an identical score (e.g. both clamp to 1.0000) can still differ in their raw
|
|
275
|
+
// pre-clamp score by a timing-dependent epsilon — utility recency uses
|
|
276
|
+
// `Date.now()` and `last_used_at`, so the same query run twice in one process
|
|
277
|
+
// can yield raw scores that diverge at the 6th decimal. Sorting on the raw
|
|
278
|
+
// value lets that invisible epsilon decide the order, so the visible name
|
|
279
|
+
// tiebreaker never engages and the order flips run-to-run (Issue #14). Quantize
|
|
280
|
+
// to the display value first; only then does `localeCompare` break true ties.
|
|
281
|
+
const displayScore = (s) => Math.round(Math.min(1, Math.max(0, s)) * 10000) / 10000;
|
|
282
|
+
preFilter.sort((a, b) => displayScore(b.score) - displayScore(a.score) || a.entry.name.localeCompare(b.entry.name));
|
|
412
283
|
// Deduplicate by file path — keep only the highest-scored entry per file.
|
|
413
284
|
// Multiple .stash.json entries can map to the same file (e.g. entries without
|
|
414
285
|
// a filename field all collapse to files[0]). Showing the same path/ref
|
|
415
286
|
// multiple times clutters results.
|
|
416
287
|
const deduped = deduplicateByPath(preFilter);
|
|
288
|
+
// Source filter: when the caller narrowed `sources` via `--source <name>`,
|
|
289
|
+
// drop hits whose filePath does not live under any of the requested
|
|
290
|
+
// sources. The FTS/vector index spans every configured source, so without
|
|
291
|
+
// this filter a narrowed --source request would still leak results from
|
|
292
|
+
// other sources that happened to match the query text.
|
|
293
|
+
const sourceFiltered = restrictToSources
|
|
294
|
+
? deduped.filter((item) => findSourceForPath(item.filePath, sources) !== undefined)
|
|
295
|
+
: deduped;
|
|
417
296
|
// Scope filter: drop hits whose stored scope does not satisfy every supplied
|
|
418
297
|
// key. Applied AFTER ranking — filtering narrows the result set without
|
|
419
298
|
// touching the single FTS5+boosts scoring pipeline.
|
|
420
|
-
const scopeFiltered = filters
|
|
299
|
+
const scopeFiltered = filters
|
|
300
|
+
? sourceFiltered.filter((item) => entryMatchesScope(item.entry.scope, filters))
|
|
301
|
+
: sourceFiltered;
|
|
421
302
|
// Proposed-quality filter (v1 spec §4.2): exclude entries with
|
|
422
303
|
// `quality: "proposed"` unless the caller passed `--include-proposed`.
|
|
423
304
|
// Applied AFTER ranking for the same reason as scope filtering.
|
|
424
305
|
const qualityFiltered = includeProposed
|
|
425
306
|
? scopeFiltered
|
|
426
307
|
: scopeFiltered.filter((item) => !isProposedQuality(item.entry.quality));
|
|
308
|
+
const beliefFiltered = qualityFiltered.filter((item) => matchBeliefFilter(item.entry.type, item.entry.beliefState, beliefFilter));
|
|
427
309
|
const rankMs = Date.now() - tRank0;
|
|
428
|
-
const selected =
|
|
310
|
+
const selected = beliefFiltered.slice(0, limit);
|
|
429
311
|
const hits = await Promise.all(selected.map(({ entry, filePath, score, rankingMode, utilityBoosted }) => {
|
|
430
312
|
// CLAUDE.md locks SearchHit.score in [0,1]. The boost loop above can
|
|
431
313
|
// exceed 1.0 (this was a pre-existing breach that #207's graph boost
|
|
@@ -444,11 +326,29 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
444
326
|
sources,
|
|
445
327
|
config,
|
|
446
328
|
utilityBoosted,
|
|
329
|
+
graphContext,
|
|
447
330
|
rendererRegistry,
|
|
331
|
+
db,
|
|
448
332
|
});
|
|
449
333
|
}));
|
|
450
334
|
return { embedMs, rankMs, hits };
|
|
451
335
|
}
|
|
336
|
+
function matchBeliefFilter(type, beliefState, filter) {
|
|
337
|
+
if (filter === "all")
|
|
338
|
+
return true;
|
|
339
|
+
if (type !== "memory")
|
|
340
|
+
return true;
|
|
341
|
+
if (filter === "current") {
|
|
342
|
+
// Phase 1A: `asserted` is a "current" state (stronger authority than `active`);
|
|
343
|
+
// `deprecated` is excluded from current results.
|
|
344
|
+
return beliefState === undefined || beliefState === "active" || beliefState === "asserted";
|
|
345
|
+
}
|
|
346
|
+
// historical
|
|
347
|
+
return (beliefState === "contradicted" ||
|
|
348
|
+
beliefState === "superseded" ||
|
|
349
|
+
beliefState === "deprecated" ||
|
|
350
|
+
beliefState === "archived");
|
|
351
|
+
}
|
|
452
352
|
// ── Vector scorer ───────────────────────────────────────────────────────────
|
|
453
353
|
async function tryVecScores(db, query, k, config) {
|
|
454
354
|
const semanticStatus = getEffectiveSemanticStatus(config, readSemanticStatus());
|
|
@@ -489,7 +389,9 @@ export async function buildDbHit(input) {
|
|
|
489
389
|
const confidenceBoost = typeof input.entry.confidence === "number" ? Math.min(0.05, Math.max(0, input.entry.confidence) * 0.05) : 0;
|
|
490
390
|
// Round to 4 decimal places, no boost multiplication
|
|
491
391
|
const score = Math.round(input.score * 10000) / 10000;
|
|
492
|
-
const
|
|
392
|
+
const graphBoost = input.graphContext ? computeGraphBoost(input.graphContext, input.path) : 0;
|
|
393
|
+
const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost, input.utilityBoosted, graphBoost);
|
|
394
|
+
const graphHit = input.graphContext ? collectGraphRelatedHit(input.graphContext, input.path) : null;
|
|
493
395
|
const source = findSourceForPath(input.path, input.sources);
|
|
494
396
|
const ref = resolveSearchHitRef(input.entry, input.entry.name, source);
|
|
495
397
|
const editable = isEditable(input.path, input.config);
|
|
@@ -514,16 +416,21 @@ export async function buildDbHit(input) {
|
|
|
514
416
|
// Surface optional quality (v1 spec §4.2). Omitted when entry has
|
|
515
417
|
// no `quality` field so payloads stay compact for the common case.
|
|
516
418
|
...(input.entry.quality ? { quality: input.entry.quality } : {}),
|
|
419
|
+
...(input.entry.beliefState ? { beliefState: input.entry.beliefState } : {}),
|
|
420
|
+
...(input.entry.currentBeliefRefs ? { currentBeliefRefs: input.entry.currentBeliefRefs } : {}),
|
|
421
|
+
...(graphHit ? { graph: { entities: graphHit.entities, relations: graphHit.relations } } : {}),
|
|
517
422
|
};
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
423
|
+
await enrichSearchHit(hit, {
|
|
424
|
+
type: input.entry.type,
|
|
425
|
+
stashDir: entryStashDir,
|
|
426
|
+
rendererRegistry,
|
|
427
|
+
db: input.db,
|
|
428
|
+
});
|
|
522
429
|
return hit;
|
|
523
430
|
}
|
|
524
431
|
export function buildWhyMatched(entry, query,
|
|
525
432
|
// "hybrid" ranking mode
|
|
526
|
-
rankingMode, qualityBoost, confidenceBoost, utilityBoosted) {
|
|
433
|
+
rankingMode, qualityBoost, confidenceBoost, utilityBoosted, graphBoost) {
|
|
527
434
|
const reasons = [
|
|
528
435
|
rankingMode === "hybrid"
|
|
529
436
|
? "hybrid (fts + semantic)"
|
|
@@ -565,8 +472,23 @@ rankingMode, qualityBoost, confidenceBoost, utilityBoosted) {
|
|
|
565
472
|
reasons.push("curated metadata boost");
|
|
566
473
|
if (confidenceBoost > 0)
|
|
567
474
|
reasons.push("metadata confidence boost");
|
|
475
|
+
if (entry.beliefState === "active")
|
|
476
|
+
reasons.push("active belief state");
|
|
477
|
+
if (entry.beliefState === "asserted")
|
|
478
|
+
reasons.push("asserted belief state");
|
|
479
|
+
if (entry.beliefState === "contradicted")
|
|
480
|
+
reasons.push("contradicted belief state");
|
|
481
|
+
if (entry.beliefState === "superseded")
|
|
482
|
+
reasons.push("superseded belief state");
|
|
483
|
+
if (entry.beliefState === "deprecated")
|
|
484
|
+
reasons.push("deprecated belief state");
|
|
485
|
+
if (entry.beliefState === "archived")
|
|
486
|
+
reasons.push("archived belief state");
|
|
568
487
|
if (utilityBoosted)
|
|
569
488
|
reasons.push("usage history boost");
|
|
489
|
+
if (typeof graphBoost === "number" && graphBoost > 0) {
|
|
490
|
+
reasons.push(`graph boost +${graphBoost.toFixed(2)}`);
|
|
491
|
+
}
|
|
570
492
|
return reasons;
|
|
571
493
|
}
|
|
572
494
|
// ── Utilities ────────────────────────────────────────────────────────────────
|
|
@@ -585,7 +507,7 @@ export function deriveSize(bytes) {
|
|
|
585
507
|
* precondition is always met regardless of caller.
|
|
586
508
|
*/
|
|
587
509
|
function deduplicateByPath(items) {
|
|
588
|
-
const sorted = [...items].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
510
|
+
const sorted = [...items].sort((a, b) => (b.score ?? 0) - (a.score ?? 0) || a.filePath.localeCompare(b.filePath));
|
|
589
511
|
const seen = new Set();
|
|
590
512
|
return sorted.filter((item) => {
|
|
591
513
|
if (seen.has(item.filePath))
|