akm-cli 0.9.1 → 0.9.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -28
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +5 -6
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +53 -22
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +901 -177
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +85 -35
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +31 -33
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +203 -229
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +64 -71
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +159 -36
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -224
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Database-backed (SQLite + FTS5/vector) source search implementation.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from source-search.ts to break the circular import:
|
|
8
|
+
* source-search.ts → sources/providers/filesystem.ts → db-search.ts (no cycle)
|
|
9
|
+
*
|
|
10
|
+
* source-search.ts imports this module for the `searchLocal` export.
|
|
11
|
+
* sources/providers/filesystem.ts also imports `searchLocal` from here.
|
|
12
|
+
*
|
|
13
|
+
* Renamed from `local-search.ts` to signal that this is the DB-layer search
|
|
14
|
+
* implementation, not a "local vs. remote" distinction.
|
|
15
|
+
*/
|
|
4
16
|
import path from "node:path";
|
|
5
17
|
import { buildActionFromContributors, defaultActionContributors } from "../../core/action-contributors.js";
|
|
6
18
|
import { stashDirFor } from "../../core/asset/asset-placement.js";
|
|
7
19
|
import { displayRef } from "../../core/asset/resolve-ref.js";
|
|
8
20
|
import { classifyPathAccess } from "../../core/path-access.js";
|
|
9
21
|
import { getDbPath } from "../../core/paths.js";
|
|
22
|
+
import { systemErrorCode } from "../../core/system-error.js";
|
|
10
23
|
import { defaultRendererRegistry } from "../../core/type-presentation.js";
|
|
11
|
-
import {
|
|
24
|
+
import { normalizeEmbeddingEndpoint } from "../../llm/embedders/remote.js";
|
|
12
25
|
import { assertIndexPathReadable, closeDatabase, openExistingDatabase, } from "../../storage/repositories/index-connection.js";
|
|
13
26
|
import { getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntryById, getEntryCount, getPositiveFeedbackCountsByIds, } from "../../storage/repositories/index-entries-repository.js";
|
|
14
27
|
import { searchFts } from "../../storage/repositories/index-fts-repository.js";
|
|
@@ -19,8 +32,9 @@ import { ensureIndex } from "../ensure-index.js";
|
|
|
19
32
|
import { collectGraphRelatedHit, loadGraphBoostContext } from "../graph/graph-boost.js";
|
|
20
33
|
import { isProposedQuality } from "../passes/metadata.js";
|
|
21
34
|
import { resolveProjectContext } from "../walk/project-context.js";
|
|
22
|
-
import { parseRefPrefixQuery, parseRetiredTypePrefixQuery,
|
|
23
|
-
import { applyRankingRules, combineSearchScores, normalizeFtsScores } from "./ranking.js";
|
|
35
|
+
import { buildLexicalQueryPlan, parseRefPrefixQuery, parseRetiredTypePrefixQuery, } from "./fts-query.js";
|
|
36
|
+
import { applyRankingRules, combineSearchScores, lexicalNameMatchTier, normalizeFtsScores } from "./ranking.js";
|
|
37
|
+
import { typeBoostFor } from "./ranking-contributors.js";
|
|
24
38
|
import { attachSearchHitAttribution, copySearchHitAttribution, getSearchHitAttribution } from "./search-attribution.js";
|
|
25
39
|
import { enrichSearchHit } from "./search-hit-enrichers.js";
|
|
26
40
|
import { buildEditHint, findSourceForPath, isEditable } from "./search-source.js";
|
|
@@ -188,7 +202,9 @@ export async function searchLocal(input) {
|
|
|
188
202
|
const staleHint = buildStaleIndexHint(db);
|
|
189
203
|
if (staleHint)
|
|
190
204
|
warnings.push(staleHint);
|
|
191
|
-
const { hits, embedMs, rankMs,
|
|
205
|
+
const { hits, embedMs, rankMs, mode, semanticWarning } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources, includeExcludedTypes, disableProjectContext, disableScopedUtility);
|
|
206
|
+
if (semanticWarning)
|
|
207
|
+
warnings.push(semanticWarning);
|
|
192
208
|
return {
|
|
193
209
|
hits,
|
|
194
210
|
tip: hits.length === 0 ? emptyResultTip(query) : undefined,
|
|
@@ -197,7 +213,7 @@ export async function searchLocal(input) {
|
|
|
197
213
|
rankMs,
|
|
198
214
|
// Report the mode the search ACTUALLY used, carried explicitly from the
|
|
199
215
|
// vector scorer — not inferred from elapsed embedding milliseconds.
|
|
200
|
-
mode
|
|
216
|
+
mode,
|
|
201
217
|
};
|
|
202
218
|
}
|
|
203
219
|
finally {
|
|
@@ -205,8 +221,38 @@ export async function searchLocal(input) {
|
|
|
205
221
|
}
|
|
206
222
|
}
|
|
207
223
|
// ── Database search ─────────────────────────────────────────────────────────
|
|
224
|
+
/**
|
|
225
|
+
* Keep one deterministic ranking order before stable path deduplication. Exact
|
|
226
|
+
* names survive the public score ceiling, while raw contributor differences
|
|
227
|
+
* are quantized so utility-recency epsilon cannot reorder visible ties.
|
|
228
|
+
*/
|
|
229
|
+
function buildSearchResultComparator(query) {
|
|
230
|
+
const queryTokens = buildLexicalQueryPlan(query).tokens.map((token) => token.toLowerCase());
|
|
231
|
+
const displayScore = (score) => Math.round(Math.min(1, Math.max(0, score)) * 10000) / 10000;
|
|
232
|
+
const stableRankScore = (score) => Math.round(score * 10000) / 10000;
|
|
233
|
+
return (a, b) => {
|
|
234
|
+
const aNameTier = lexicalNameMatchTier(a.entry, queryTokens);
|
|
235
|
+
const bNameTier = lexicalNameMatchTier(b.entry, queryTokens);
|
|
236
|
+
if (aNameTier === 3 || bNameTier === 3) {
|
|
237
|
+
const nameDiff = bNameTier - aNameTier;
|
|
238
|
+
if (nameDiff !== 0)
|
|
239
|
+
return nameDiff;
|
|
240
|
+
}
|
|
241
|
+
const scoreDiff = displayScore(b.score) - displayScore(a.score);
|
|
242
|
+
if (scoreDiff !== 0)
|
|
243
|
+
return scoreDiff;
|
|
244
|
+
const rawScoreDiff = stableRankScore(b.score) - stableRankScore(a.score);
|
|
245
|
+
if (rawScoreDiff !== 0)
|
|
246
|
+
return rawScoreDiff;
|
|
247
|
+
const nameDiff = bNameTier - aNameTier;
|
|
248
|
+
if (nameDiff !== 0)
|
|
249
|
+
return nameDiff;
|
|
250
|
+
const typeDiff = typeBoostFor(b.entry.type) - typeBoostFor(a.entry.type);
|
|
251
|
+
return typeDiff || a.filePath.localeCompare(b.filePath);
|
|
252
|
+
};
|
|
253
|
+
}
|
|
208
254
|
async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry, filters, includeProposed = false, beliefFilter = "all", restrictToSources = false, includeExcludedTypes = false, disableProjectContext = false, disableScopedUtility = false) {
|
|
209
|
-
const hasSearchableTokens = query.length > 0 &&
|
|
255
|
+
const hasSearchableTokens = query.length > 0 && buildLexicalQueryPlan(query).tokens.length > 0;
|
|
210
256
|
// #627 — resolve the default type-exclusion policy. It applies ONLY on the
|
|
211
257
|
// untyped ('any') path and only when the caller did not opt back in via
|
|
212
258
|
// `includeExcludedTypes`. When the config key is ABSENT a built-in default of
|
|
@@ -246,7 +292,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
246
292
|
conceptIdPrefix: refPrefix.conceptIdPrefix,
|
|
247
293
|
...(refPrefix.bundle !== undefined ? { bundle: refPrefix.bundle } : {}),
|
|
248
294
|
})),
|
|
249
|
-
|
|
295
|
+
mode: "keyword",
|
|
250
296
|
};
|
|
251
297
|
}
|
|
252
298
|
// Empty queries — including ones that sanitize down to no searchable FTS
|
|
@@ -260,23 +306,13 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
260
306
|
typeFilter: searchType === "any" ? undefined : searchType,
|
|
261
307
|
excludeTypes: defaultExcludes,
|
|
262
308
|
})),
|
|
263
|
-
|
|
309
|
+
mode: "keyword",
|
|
264
310
|
};
|
|
265
311
|
}
|
|
266
312
|
// Start the async embedding request without awaiting, then run FTS
|
|
267
313
|
// synchronously while the HTTP/local embedding request is in-flight.
|
|
268
314
|
const typeFilter = searchType === "any" ? undefined : searchType;
|
|
269
|
-
const
|
|
270
|
-
const embeddingPromise = tryVecScores(db, query, limit * 3, config);
|
|
271
|
-
const ftsResults = searchFts(db, query, limit * 3, typeFilter, defaultExcludes);
|
|
272
|
-
const embeddingScores = await embeddingPromise;
|
|
273
|
-
const embedMs = Date.now() - tEmbed0;
|
|
274
|
-
// The vector scorer returns a (possibly empty) Map when the embedding + vector
|
|
275
|
-
// search actually executed, or null when semantic was not runnable (disabled,
|
|
276
|
-
// no embeddings, or the embed call threw). This is the AUTHORITATIVE "semantic
|
|
277
|
-
// mode was used" signal — carried out to telemetry instead of guessing from
|
|
278
|
-
// elapsed milliseconds (which timed the concurrent FTS work too).
|
|
279
|
-
const usedSemantic = embeddingScores !== null;
|
|
315
|
+
const { ftsResults, embeddingScores, embedMs, mode, semanticWarning } = await collectSearchSignals(db, query, limit * 3, typeFilter, defaultExcludes, config);
|
|
280
316
|
const tRank0 = Date.now();
|
|
281
317
|
// ── Score normalization ──────────────────────────────────────────────
|
|
282
318
|
// Normalized BM25 + cosine similarity with weighted addition
|
|
@@ -378,21 +414,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
378
414
|
const preFilter = minScore > 0
|
|
379
415
|
? scored.filter((item) => item.rankingMode !== "semantic" || (item.preCeilingScore ?? item.score) >= minScore)
|
|
380
416
|
: scored;
|
|
381
|
-
|
|
382
|
-
//
|
|
383
|
-
// CRITICAL: sort on the SAME clamped+rounded value the user sees (see the
|
|
384
|
-
// `finalScore`/round-to-4dp logic below at buildDbHit), NOT the raw pre-clamp
|
|
385
|
-
// `item.score`. The boost loop can push scores above 1.0 (utility, graph,
|
|
386
|
-
// project boosts) and carries ~15 significant digits. Two entries that DISPLAY
|
|
387
|
-
// an identical score (e.g. both clamp to 1.0000) can still differ in their raw
|
|
388
|
-
// pre-clamp score by a timing-dependent epsilon — utility recency uses
|
|
389
|
-
// `Date.now()` and `last_used_at`, so the same query run twice in one process
|
|
390
|
-
// can yield raw scores that diverge at the 6th decimal. Sorting on the raw
|
|
391
|
-
// value lets that invisible epsilon decide the order, so the visible name
|
|
392
|
-
// tiebreaker never engages and the order flips run-to-run (Issue #14). Quantize
|
|
393
|
-
// to the display value first; only then does `localeCompare` break true ties.
|
|
394
|
-
const displayScore = (s) => Math.round(Math.min(1, Math.max(0, s)) * 10000) / 10000;
|
|
395
|
-
preFilter.sort((a, b) => displayScore(b.score) - displayScore(a.score) || a.entry.name.localeCompare(b.entry.name));
|
|
417
|
+
preFilter.sort(buildSearchResultComparator(query));
|
|
396
418
|
// Deduplicate by file path — keep only the highest-scored entry per file.
|
|
397
419
|
const deduped = deduplicateByPath(preFilter);
|
|
398
420
|
// Source → scope → proposed-quality → derived-twin belief inheritance →
|
|
@@ -426,6 +448,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
426
448
|
score: Math.round(finalScore * 10000) / 10000,
|
|
427
449
|
query,
|
|
428
450
|
rankingMode,
|
|
451
|
+
lexicalMatch: ranked.lexicalMatch,
|
|
429
452
|
defaultStashDir: stashDir,
|
|
430
453
|
allSourceDirs,
|
|
431
454
|
sources,
|
|
@@ -437,7 +460,25 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
437
460
|
db,
|
|
438
461
|
});
|
|
439
462
|
}));
|
|
440
|
-
return { embedMs, rankMs, hits,
|
|
463
|
+
return { embedMs, rankMs, hits, mode, semanticWarning };
|
|
464
|
+
}
|
|
465
|
+
async function collectSearchSignals(db, query, candidateLimit, typeFilter, excludeTypes, config) {
|
|
466
|
+
const startedAt = Date.now();
|
|
467
|
+
const embeddingPromise = tryVecScores(db, query, candidateLimit, config);
|
|
468
|
+
const ftsResults = searchFts(db, query, candidateLimit, typeFilter, excludeTypes);
|
|
469
|
+
const embeddingResult = await embeddingPromise;
|
|
470
|
+
const mode = embeddingResult.warning
|
|
471
|
+
? "fts-fallback"
|
|
472
|
+
: embeddingResult.scores !== null
|
|
473
|
+
? "semantic"
|
|
474
|
+
: "keyword";
|
|
475
|
+
return {
|
|
476
|
+
ftsResults,
|
|
477
|
+
embeddingScores: embeddingResult.scores,
|
|
478
|
+
embedMs: Date.now() - startedAt,
|
|
479
|
+
mode,
|
|
480
|
+
semanticWarning: embeddingResult.warning,
|
|
481
|
+
};
|
|
441
482
|
}
|
|
442
483
|
/**
|
|
443
484
|
* The no-hits tip. A query in the retired `<type>:` / `<type>:<prefix>/` browse
|
|
@@ -537,7 +578,7 @@ async function enumerateEntries(opts) {
|
|
|
537
578
|
* What this does NOT unify — and deliberately leaves divergent — is CANDIDATE-
|
|
538
579
|
* POOL construction, which is inherent search-vs-browse semantics: the scored
|
|
539
580
|
* path's pool is `searchFts`/vector matches for the query's own tokens (FTS
|
|
540
|
-
*
|
|
581
|
+
* includes structured fields and bounded adapter content), while the
|
|
541
582
|
* enumerate path's pool is `getAllEntries` for the type, independent of query
|
|
542
583
|
* text. A derived twin sharing no indexed token with the query is therefore an
|
|
543
584
|
* enumerate-path candidate but never a scored-path candidate — see
|
|
@@ -623,10 +664,10 @@ function matchBeliefFilter(beliefState, filter) {
|
|
|
623
664
|
async function tryVecScores(db, query, k, config) {
|
|
624
665
|
const semanticStatus = getEffectiveSemanticStatus(config, readSemanticStatus());
|
|
625
666
|
if (!isSemanticRuntimeReady(semanticStatus))
|
|
626
|
-
return null;
|
|
667
|
+
return { scores: null };
|
|
627
668
|
const hasEmbeddings = getMeta(db, "hasEmbeddings");
|
|
628
669
|
if (hasEmbeddings !== "1")
|
|
629
|
-
return null;
|
|
670
|
+
return { scores: null };
|
|
630
671
|
try {
|
|
631
672
|
const { embed } = await import("../../llm/embedder.js");
|
|
632
673
|
const queryEmbedding = await embed(query, config.embedding);
|
|
@@ -638,12 +679,64 @@ async function tryVecScores(db, query, k, config) {
|
|
|
638
679
|
const raw = 1 - (distance * distance) / 2;
|
|
639
680
|
scores.set(id, Number.isFinite(raw) ? Math.max(0, raw) : 0);
|
|
640
681
|
}
|
|
641
|
-
return scores;
|
|
682
|
+
return { scores };
|
|
642
683
|
}
|
|
643
684
|
catch (error) {
|
|
644
|
-
|
|
645
|
-
|
|
685
|
+
return { scores: null, warning: buildVectorFallbackWarning(config, error) };
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function buildVectorFallbackWarning(config, error) {
|
|
689
|
+
const endpoint = safeEmbeddingEndpoint(config);
|
|
690
|
+
const reason = classifyVectorFailure(error);
|
|
691
|
+
const target = endpoint
|
|
692
|
+
? `embedding endpoint ${endpoint}`
|
|
693
|
+
: config.embedding?.endpoint
|
|
694
|
+
? "configured embedding endpoint"
|
|
695
|
+
: "local embedding model";
|
|
696
|
+
const unavailable = reason === "connection failed" ? `cannot reach ${target}` : `${target} is unavailable`;
|
|
697
|
+
return `Vector search unavailable: ${unavailable} (${reason}) — falling back to keyword search.`;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Name the useful endpoint without ever carrying URL userinfo, query secrets,
|
|
701
|
+
* or fragments into a warning. Invalid authored values fail closed.
|
|
702
|
+
*/
|
|
703
|
+
function safeEmbeddingEndpoint(config) {
|
|
704
|
+
const endpoint = config.embedding?.endpoint;
|
|
705
|
+
if (!endpoint)
|
|
706
|
+
return undefined;
|
|
707
|
+
try {
|
|
708
|
+
const parsed = new URL(normalizeEmbeddingEndpoint(endpoint));
|
|
709
|
+
parsed.username = "";
|
|
710
|
+
parsed.password = "";
|
|
711
|
+
parsed.search = "";
|
|
712
|
+
parsed.hash = "";
|
|
713
|
+
return parsed.toString();
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/** Map untrusted runtime failures onto a small, non-secret diagnostic set. */
|
|
720
|
+
function classifyVectorFailure(error) {
|
|
721
|
+
const code = systemErrorCode(error);
|
|
722
|
+
const message = error instanceof Error ? error.message : "";
|
|
723
|
+
if (code === "ECONNREFUSED" ||
|
|
724
|
+
code === "ECONNRESET" ||
|
|
725
|
+
code === "ENETUNREACH" ||
|
|
726
|
+
code === "EHOSTUNREACH" ||
|
|
727
|
+
code === "ENOTFOUND" ||
|
|
728
|
+
code === "EAI_AGAIN" ||
|
|
729
|
+
/typo in the url or port|connection (?:refused|failed|reset)|fetch failed/i.test(message)) {
|
|
730
|
+
return "connection failed";
|
|
646
731
|
}
|
|
732
|
+
if (code === "ETIMEDOUT" || /timed? out|timeout/i.test(message))
|
|
733
|
+
return "request timed out";
|
|
734
|
+
const httpStatus = message.match(/Embedding (?:batch )?request failed \((\d{3})\)/i)?.[1];
|
|
735
|
+
if (httpStatus)
|
|
736
|
+
return `HTTP ${httpStatus}`;
|
|
737
|
+
if (/unexpected embedding response|missing data\[0\]\.embedding/i.test(message))
|
|
738
|
+
return "invalid embedding response";
|
|
739
|
+
return "request failed";
|
|
647
740
|
}
|
|
648
741
|
// ── Hit building ────────────────────────────────────────────────────────────
|
|
649
742
|
export async function buildDbHit(input) {
|
|
@@ -661,7 +754,7 @@ export async function buildDbHit(input) {
|
|
|
661
754
|
// Round to 4 decimal places, no boost multiplication
|
|
662
755
|
const score = Math.round(input.score * 10000) / 10000;
|
|
663
756
|
const graphBoost = getSearchHitAttribution(input.attributionSource ?? {})?.graphExtraction?.boost ?? 0;
|
|
664
|
-
const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost, input.utilityBoosted, graphBoost);
|
|
757
|
+
const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost, input.utilityBoosted, graphBoost, input.lexicalMatch);
|
|
665
758
|
const graphHit = input.graphContext ? collectGraphRelatedHit(input.graphContext, absolutePath) : null;
|
|
666
759
|
const source = findSourceForPath(absolutePath, input.sources);
|
|
667
760
|
const defaultBundleId = input.config?.defaultBundle ??
|
|
@@ -693,8 +786,7 @@ export async function buildDbHit(input) {
|
|
|
693
786
|
...(input.entry.currentBeliefRefs ? { currentBeliefRefs: input.entry.currentBeliefRefs } : {}),
|
|
694
787
|
...(graphHit ? { graph: { entities: graphHit.entities, relations: graphHit.relations } } : {}),
|
|
695
788
|
};
|
|
696
|
-
|
|
697
|
-
copySearchHitAttribution(input.attributionSource, hit);
|
|
789
|
+
attachDbHitAttribution(hit, input);
|
|
698
790
|
if (input.entry.derivedFrom) {
|
|
699
791
|
attachSearchHitAttribution(hit, {
|
|
700
792
|
memoryInference: { exposure: "direct" },
|
|
@@ -703,14 +795,27 @@ export async function buildDbHit(input) {
|
|
|
703
795
|
await enrichSearchHit(hit, {
|
|
704
796
|
type: input.entry.type,
|
|
705
797
|
stashDir: entryStashDir,
|
|
798
|
+
bundleId: input.bundleId,
|
|
706
799
|
rendererRegistry,
|
|
707
800
|
db: input.db,
|
|
708
801
|
});
|
|
709
802
|
return hit;
|
|
710
803
|
}
|
|
804
|
+
function attachDbHitAttribution(hit, input) {
|
|
805
|
+
if (input.lexicalMatch) {
|
|
806
|
+
attachSearchHitAttribution(hit, {
|
|
807
|
+
lexical: {
|
|
808
|
+
execution: input.lexicalMatch,
|
|
809
|
+
nameMatchTier: lexicalNameMatchTier(input.entry, buildLexicalQueryPlan(input.query).tokens),
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
if (input.attributionSource)
|
|
814
|
+
copySearchHitAttribution(input.attributionSource, hit);
|
|
815
|
+
}
|
|
711
816
|
export function buildWhyMatched(entry, query,
|
|
712
817
|
// "hybrid" ranking mode
|
|
713
|
-
rankingMode, qualityBoost, confidenceBoost, utilityBoosted, graphBoost) {
|
|
818
|
+
rankingMode, qualityBoost, confidenceBoost, utilityBoosted, graphBoost, lexicalMatch) {
|
|
714
819
|
const reasons = [
|
|
715
820
|
rankingMode === "hybrid"
|
|
716
821
|
? "hybrid (fts + semantic)"
|
|
@@ -718,6 +823,8 @@ rankingMode, qualityBoost, confidenceBoost, utilityBoosted, graphBoost) {
|
|
|
718
823
|
? "semantic similarity"
|
|
719
824
|
: "fts bm25 relevance",
|
|
720
825
|
];
|
|
826
|
+
if (lexicalMatch === "relaxed")
|
|
827
|
+
reasons.push("lexical recovery after strict query returned no hits");
|
|
721
828
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
722
829
|
const queryLower = query.toLowerCase().trim();
|
|
723
830
|
const name = entry.name.toLowerCase();
|
|
@@ -782,14 +889,13 @@ export function deriveSize(bytes) {
|
|
|
782
889
|
return "large";
|
|
783
890
|
}
|
|
784
891
|
/**
|
|
785
|
-
* Deduplicate
|
|
786
|
-
*
|
|
787
|
-
*
|
|
892
|
+
* Deduplicate the already-ranked result stream by file path. The caller owns
|
|
893
|
+
* the one ranking order; re-sorting here would silently discard exact-name and
|
|
894
|
+
* relaxed-recovery ordering in favor of an internal pre-clamp score.
|
|
788
895
|
*/
|
|
789
896
|
function deduplicateByPath(items) {
|
|
790
|
-
const sorted = [...items].sort((a, b) => (b.score ?? 0) - (a.score ?? 0) || a.filePath.localeCompare(b.filePath));
|
|
791
897
|
const seen = new Set();
|
|
792
|
-
return
|
|
898
|
+
return items.filter((item) => {
|
|
793
899
|
if (seen.has(item.filePath))
|
|
794
900
|
return false;
|
|
795
901
|
seen.add(item.filePath);
|
|
@@ -2,54 +2,54 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
|
-
* Pure FTS5 query
|
|
5
|
+
* Pure FTS5 query planning and ref-query helpers.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* The lexical planner transforms a raw user query into bounded FTS5-safe
|
|
8
|
+
* MATCH expressions. It touches no database state, so it is unit-testable
|
|
9
|
+
* with zero DB setup.
|
|
9
10
|
* `parseRefPrefixQuery` is the one non-FTS helper: it decides whether a raw
|
|
10
11
|
* query should bypass FTS entirely (SPEC-4 ref-prefix enumeration).
|
|
11
12
|
*/
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*/
|
|
21
|
-
export function sanitizeFtsQuery(query) {
|
|
22
|
-
let sanitized = query.replace(/[^a-zA-Z0-9_\s]/g, " ");
|
|
23
|
-
// Neutralize the NEAR operator (FTS5 proximity syntax)
|
|
24
|
-
sanitized = sanitized.replace(/\bNEAR\b/g, " ");
|
|
25
|
-
const tokens = sanitized.split(/\s+/).filter((t) => t.length >= 1);
|
|
26
|
-
if (tokens.length === 0)
|
|
27
|
-
return "";
|
|
28
|
-
// Use implicit AND (space-separated tokens) for precision. FTS5 treats
|
|
29
|
-
// space-separated tokens as an implicit AND, matching only rows that
|
|
30
|
-
// contain ALL terms.
|
|
31
|
-
return tokens.join(" ");
|
|
13
|
+
/** Maximum number of distinct lexical terms one query may execute. */
|
|
14
|
+
export const MAX_LEXICAL_QUERY_TOKENS = 16;
|
|
15
|
+
const UNICODE_TOKEN = /[\p{L}\p{N}]+/gu;
|
|
16
|
+
function quoteToken(token) {
|
|
17
|
+
return `"${token}"`;
|
|
18
|
+
}
|
|
19
|
+
function prefixToken(token) {
|
|
20
|
+
return [...token].length >= 3 ? `${quoteToken(token)}*` : quoteToken(token);
|
|
32
21
|
}
|
|
33
22
|
/**
|
|
34
|
-
* Build
|
|
35
|
-
* token that is 3+ characters long. Tokens shorter than 3 characters are
|
|
36
|
-
* kept as-is (no prefix expansion) to avoid overly broad matches.
|
|
23
|
+
* Build the sole lexical retrieval plan from raw user input.
|
|
37
24
|
*
|
|
38
|
-
*
|
|
25
|
+
* Tokenization follows the useful portion of SQLite FTS5's `unicode61`
|
|
26
|
+
* tokenizer (Unicode letters and numbers). Quoting every term makes FTS
|
|
27
|
+
* operators ordinary searchable words. Tokens are normalized, deduplicated
|
|
28
|
+
* case-insensitively, and capped before any SQL executes.
|
|
39
29
|
*/
|
|
40
|
-
export function
|
|
41
|
-
const tokens =
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
30
|
+
export function buildLexicalQueryPlan(query) {
|
|
31
|
+
const tokens = [];
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const normalized = query.normalize("NFKC");
|
|
34
|
+
for (const match of normalized.matchAll(UNICODE_TOKEN)) {
|
|
35
|
+
const token = match[0];
|
|
36
|
+
const key = token.toLowerCase();
|
|
37
|
+
if (seen.has(key))
|
|
38
|
+
continue;
|
|
39
|
+
seen.add(key);
|
|
40
|
+
tokens.push(token);
|
|
41
|
+
if (tokens.length === MAX_LEXICAL_QUERY_TOKENS)
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
const exact = tokens.map(quoteToken).join(" ");
|
|
45
|
+
const prefixTokens = tokens.map(prefixToken);
|
|
46
|
+
const exactPrefix = prefixTokens.some((token) => token.endsWith("*")) ? prefixTokens.join(" ") : undefined;
|
|
47
|
+
// A slash-bearing, whitespace-free input is an identifier/ref lookup, not
|
|
48
|
+
// sentence prose. Keep it conjunctive so a mistyped/bare ref never fans out
|
|
49
|
+
// across every path token through OR recovery.
|
|
50
|
+
const isRefLikeIdentifier = !/\s/u.test(query.trim()) && query.includes("/");
|
|
51
|
+
const relaxed = tokens.length > 1 && !isRefLikeIdentifier ? prefixTokens.join(" OR ") : undefined;
|
|
52
|
+
return { tokens, exact, exactPrefix, relaxed };
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
55
|
* D4 — parse a conceptId-prefix browse query.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { getUtilityScoresByIds } from "../../storage/repositories/index-utility-repository.js";
|
|
5
|
+
import { buildLexicalQueryPlan } from "./fts-query.js";
|
|
5
6
|
import { applyBeliefStateScoreCeiling, applyContributorAblation, applyScoreContributors, applyUtilityContributors, defaultRankingContributors, defaultUtilityRankingContributors, } from "./ranking-contributors.js";
|
|
6
7
|
export function normalizeFtsScores(results) {
|
|
7
8
|
const ftsScoreMap = new Map();
|
|
@@ -33,6 +34,7 @@ export function combineSearchScores(options) {
|
|
|
33
34
|
filePath: result.filePath,
|
|
34
35
|
score: combinedScore,
|
|
35
36
|
rankingMode: embedScore !== undefined ? "hybrid" : "fts",
|
|
37
|
+
lexicalMatch: result.lexicalMatch,
|
|
36
38
|
itemRef: result.itemRef,
|
|
37
39
|
bundleId: result.bundleId,
|
|
38
40
|
conceptId: result.conceptId,
|
|
@@ -63,7 +65,7 @@ export function combineSearchScores(options) {
|
|
|
63
65
|
return scored;
|
|
64
66
|
}
|
|
65
67
|
export function applyRankingRules(options) {
|
|
66
|
-
const queryTokens = options.query
|
|
68
|
+
const queryTokens = buildLexicalQueryPlan(options.query).tokens.map((token) => token.toLowerCase());
|
|
67
69
|
const queryLower = options.query.toLowerCase().trim();
|
|
68
70
|
const rankingContext = {
|
|
69
71
|
db: options.db,
|
|
@@ -103,6 +105,7 @@ export function applyRankingRules(options) {
|
|
|
103
105
|
};
|
|
104
106
|
for (const item of options.items) {
|
|
105
107
|
applyUtilityContributors(item, utilityContext, activeUtilityContributors);
|
|
108
|
+
applyRelaxedLexicalScoreCeiling(item, queryTokens);
|
|
106
109
|
// SPEC-5: demoting belief states (superseded/contradicted/archived/
|
|
107
110
|
// deprecated) cap the FINAL score. The additive belief penalty inside the
|
|
108
111
|
// multiplicative boost sum cannot overcome the FTS min-max normalization
|
|
@@ -112,3 +115,35 @@ export function applyRankingRules(options) {
|
|
|
112
115
|
}
|
|
113
116
|
return options.items;
|
|
114
117
|
}
|
|
118
|
+
const RELAXED_NON_NAME_SCORE_CEILING = 0.65;
|
|
119
|
+
/**
|
|
120
|
+
* Rank name evidence without relying on punctuation or ASCII-only splitting.
|
|
121
|
+
* The tiers are intentionally structural: an exact normalized name, all query
|
|
122
|
+
* tokens in a longer name, any query token in the name, or no name evidence.
|
|
123
|
+
*/
|
|
124
|
+
export function lexicalNameMatchTier(entry, queryTokens) {
|
|
125
|
+
if (queryTokens.length === 0)
|
|
126
|
+
return 0;
|
|
127
|
+
const nameBase = entry.name.toLowerCase().split("/").pop() ?? entry.name.toLowerCase();
|
|
128
|
+
const nameTokens = buildLexicalQueryPlan(nameBase).tokens.map((token) => token.toLowerCase());
|
|
129
|
+
const tokenMatches = (left, right) => left === right ||
|
|
130
|
+
(Math.min([...left].length, [...right].length) >= 3 && (left.startsWith(right) || right.startsWith(left)));
|
|
131
|
+
if (nameTokens.length === queryTokens.length &&
|
|
132
|
+
nameTokens.every((token, index) => tokenMatches(token, queryTokens[index]))) {
|
|
133
|
+
return 3;
|
|
134
|
+
}
|
|
135
|
+
const matched = queryTokens.filter((token) => nameTokens.some((nameToken) => tokenMatches(nameToken, token))).length;
|
|
136
|
+
if (matched === queryTokens.length)
|
|
137
|
+
return 2;
|
|
138
|
+
return matched > 0 ? 1 : 0;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* A relaxed OR query admits intentionally weak candidates. Candidates with no
|
|
142
|
+
* query token in their name remain visible for body-only recall, but cannot
|
|
143
|
+
* saturate at the same displayed score as stronger name-bearing recoveries.
|
|
144
|
+
*/
|
|
145
|
+
function applyRelaxedLexicalScoreCeiling(item, queryTokens) {
|
|
146
|
+
if (item.lexicalMatch !== "relaxed" || lexicalNameMatchTier(item.entry, queryTokens) > 0)
|
|
147
|
+
return;
|
|
148
|
+
item.score = Math.min(item.score, RELAXED_NON_NAME_SCORE_CEILING);
|
|
149
|
+
}
|
|
@@ -17,11 +17,13 @@ export function copySearchHitAttribution(from, to, outputDescription) {
|
|
|
17
17
|
const memorySurvives = memoryInference?.exposure !== "surface" ||
|
|
18
18
|
(memoryInference.surfaceDescription !== undefined && memoryInference.surfaceDescription === outputDescription);
|
|
19
19
|
const applicable = {
|
|
20
|
+
...(attribution.lexical ? { lexical: attribution.lexical } : {}),
|
|
20
21
|
...(memorySurvives && memoryInference ? { memoryInference } : {}),
|
|
21
22
|
...(attribution.graphExtraction ? { graphExtraction: attribution.graphExtraction } : {}),
|
|
22
23
|
};
|
|
23
|
-
if (applicable.memoryInference || applicable.graphExtraction)
|
|
24
|
+
if (applicable.lexical || applicable.memoryInference || applicable.graphExtraction) {
|
|
24
25
|
attachSearchHitAttribution(to, applicable);
|
|
26
|
+
}
|
|
25
27
|
}
|
|
26
28
|
export function getSearchHitAttribution(target) {
|
|
27
29
|
return target[ATTRIBUTION];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/** Structured metadata plus bounded body text supplied to embedding providers. */
|
|
5
|
+
export const SEARCH_TEXT_MAX_CHARS = 8_192;
|
|
4
6
|
/**
|
|
5
7
|
* Return per-field search text for multi-column FTS5 indexing.
|
|
6
8
|
*
|
|
@@ -9,7 +11,7 @@
|
|
|
9
11
|
* - description: entry description
|
|
10
12
|
* - tags: tags + aliases joined
|
|
11
13
|
* - hints: searchHints + examples + usage + intent fields
|
|
12
|
-
* - content:
|
|
14
|
+
* - content: bounded native/adapter body projection + TOC headings + parameters
|
|
13
15
|
* (lowest-weight catch-all)
|
|
14
16
|
*/
|
|
15
17
|
// NOTE (R5): the collapse detector's frozen canary queries are built from the
|
|
@@ -53,8 +55,6 @@ export function buildSearchFields(entry) {
|
|
|
53
55
|
hintParts.push(entry.whenToUse);
|
|
54
56
|
const hints = hintParts.join(" ").toLowerCase();
|
|
55
57
|
const contentParts = [];
|
|
56
|
-
if (entry.content)
|
|
57
|
-
contentParts.push(entry.content);
|
|
58
58
|
if (entry.toc) {
|
|
59
59
|
contentParts.push(entry.toc.map((h) => h.text).join(" "));
|
|
60
60
|
}
|
|
@@ -65,15 +65,8 @@ export function buildSearchFields(entry) {
|
|
|
65
65
|
contentParts.push(param.description);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// folds into the lowest-weight catch-all column — never name/description/
|
|
71
|
-
// tags/hints — so orientation prose is retrievable without outranking
|
|
72
|
-
// structured-field matches. The fold is unconditional on the entry field:
|
|
73
|
-
// `rebuildFts` rebuilds FTS rows from stored entry_json and must reproduce
|
|
74
|
-
// the same fields without re-reading config.
|
|
75
|
-
if (entry.bodyOpening)
|
|
76
|
-
contentParts.push(entry.bodyOpening);
|
|
68
|
+
if (entry.content)
|
|
69
|
+
contentParts.push(entry.content);
|
|
77
70
|
const content = contentParts.join(" ").toLowerCase();
|
|
78
71
|
return { name, description, tags, hints, content };
|
|
79
72
|
}
|
|
@@ -84,7 +77,23 @@ export function buildSearchFields(entry) {
|
|
|
84
77
|
*/
|
|
85
78
|
export function buildSearchText(entry) {
|
|
86
79
|
const fields = buildSearchFields(entry);
|
|
87
|
-
|
|
88
|
-
.filter((
|
|
80
|
+
const structured = [fields.name, fields.description, fields.tags, fields.hints]
|
|
81
|
+
.filter((field) => field.length > 0)
|
|
89
82
|
.join(" ");
|
|
83
|
+
if (structured.length >= SEARCH_TEXT_MAX_CHARS)
|
|
84
|
+
return truncateUnicodeSafe(structured, SEARCH_TEXT_MAX_CHARS);
|
|
85
|
+
if (!fields.content)
|
|
86
|
+
return structured;
|
|
87
|
+
const separator = structured ? " " : "";
|
|
88
|
+
const remaining = SEARCH_TEXT_MAX_CHARS - structured.length - separator.length;
|
|
89
|
+
return `${structured}${separator}${truncateUnicodeSafe(fields.content, remaining)}`;
|
|
90
|
+
}
|
|
91
|
+
function truncateUnicodeSafe(text, maxChars) {
|
|
92
|
+
if (text.length <= maxChars)
|
|
93
|
+
return text;
|
|
94
|
+
let cut = text.slice(0, maxChars);
|
|
95
|
+
const lastCode = cut.charCodeAt(cut.length - 1);
|
|
96
|
+
if (lastCode >= 0xd800 && lastCode <= 0xdbff)
|
|
97
|
+
cut = cut.slice(0, -1);
|
|
98
|
+
return cut.trimEnd();
|
|
90
99
|
}
|
|
@@ -53,7 +53,7 @@ export const derivedMemoryEnricher = {
|
|
|
53
53
|
// column now stores this same conceptId grammar (Group-C item 2 flip — the
|
|
54
54
|
// metadata producer + this consumer move together).
|
|
55
55
|
const parentRef = `memories/${hit.name}`;
|
|
56
|
-
const derived = getDerivedForParent(ctx.db, parentRef, ctx.
|
|
56
|
+
const derived = getDerivedForParent(ctx.db, parentRef, ctx.bundleId);
|
|
57
57
|
if (!derived)
|
|
58
58
|
return;
|
|
59
59
|
// Swap description / searchHints / tags from the derived child.
|