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.
Files changed (350) hide show
  1. package/CHANGELOG.md +103 -28
  2. package/README.md +3 -1
  3. package/SECURITY.md +1 -1
  4. package/STABILITY.md +1 -1
  5. package/dist/akm +2 -2
  6. package/dist/akm-migrate +2 -2
  7. package/dist/assets/hints/cli-hints-full.md +14 -9
  8. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
  9. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  10. package/dist/assets/models.json +35 -0
  11. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
  12. package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
  13. package/dist/assets/tasks/core/extract.yml +6 -5
  14. package/dist/assets/tasks/core/improve.yml +6 -5
  15. package/dist/assets/tasks/core/index-refresh.yml +6 -5
  16. package/dist/assets/tasks/core/sync.yml +6 -5
  17. package/dist/assets/tasks/core/version-check.yml +6 -5
  18. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
  19. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
  20. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
  21. package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
  22. package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
  23. package/dist/cli/confirm.js +2 -2
  24. package/dist/cli/parse-args.js +3 -24
  25. package/dist/cli/retired-commands.js +1 -1
  26. package/dist/cli/shared.js +2 -2
  27. package/dist/cli.js +11 -9
  28. package/dist/commands/agent/agent-dispatch.js +55 -89
  29. package/dist/commands/agent/contribute-cli.js +12 -45
  30. package/dist/commands/command/builtin-action.js +32 -0
  31. package/dist/commands/command/command-cli.js +99 -0
  32. package/dist/commands/command/command-execution.js +308 -0
  33. package/dist/commands/command/execution-source-loader.js +176 -0
  34. package/dist/commands/command/portable-template.js +60 -0
  35. package/dist/commands/config-cli.js +10 -4
  36. package/dist/commands/env/env.js +4 -2
  37. package/dist/commands/feedback-cli.js +1 -1
  38. package/dist/commands/health/checks.js +241 -29
  39. package/dist/commands/health/html-report.js +0 -14
  40. package/dist/commands/health/report-view-model.js +0 -1
  41. package/dist/commands/health/surfaces.js +6 -7
  42. package/dist/commands/health/types.js +0 -2
  43. package/dist/commands/health.js +63 -18
  44. package/dist/commands/improve/collapse-detector.js +5 -6
  45. package/dist/commands/improve/consolidate.js +251 -214
  46. package/dist/commands/improve/distill/promote-memory.js +71 -34
  47. package/dist/commands/improve/distill/quality-gate.js +17 -5
  48. package/dist/commands/improve/distill.js +232 -155
  49. package/dist/commands/improve/eligibility.js +112 -79
  50. package/dist/commands/improve/execution.js +57 -0
  51. package/dist/commands/improve/extract-cli.js +5 -5
  52. package/dist/commands/improve/extract-prompt.js +64 -22
  53. package/dist/commands/improve/extract.js +608 -360
  54. package/dist/commands/improve/improve-strategies.js +43 -14
  55. package/dist/commands/improve/improve.js +249 -29
  56. package/dist/commands/improve/loop-stages.js +11 -17
  57. package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
  58. package/dist/commands/improve/outcome-loop.js +22 -38
  59. package/dist/commands/improve/planner.js +134 -0
  60. package/dist/commands/improve/preparation.js +730 -409
  61. package/dist/commands/improve/reflect.js +386 -223
  62. package/dist/commands/improve/run-context.js +3 -4
  63. package/dist/commands/improve/salience.js +6 -58
  64. package/dist/commands/improve/session-asset.js +12 -12
  65. package/dist/commands/lint/index.js +101 -29
  66. package/dist/commands/migrate-cli.js +11 -69
  67. package/dist/commands/migration-tool.js +6 -9
  68. package/dist/commands/models-cli.js +27 -0
  69. package/dist/commands/proposal/drain.js +258 -186
  70. package/dist/commands/proposal/proposal-cli.js +32 -10
  71. package/dist/commands/proposal/proposal.js +2 -5
  72. package/dist/commands/proposal/propose.js +192 -172
  73. package/dist/commands/proposal/repository.js +54 -91
  74. package/dist/commands/proposal/validators/proposal-validators.js +9 -7
  75. package/dist/commands/read/curate.js +53 -22
  76. package/dist/commands/read/registry-search.js +25 -9
  77. package/dist/commands/read/remember-cli.js +14 -2
  78. package/dist/commands/read/search.js +10 -4
  79. package/dist/commands/read/show.js +139 -153
  80. package/dist/commands/registry-cli.js +16 -7
  81. package/dist/commands/remember.js +33 -18
  82. package/dist/commands/sources/add-cli.js +19 -178
  83. package/dist/commands/sources/bundle-cli.js +15 -3
  84. package/dist/commands/sources/dangerous-env-audit.js +135 -0
  85. package/dist/commands/sources/info.js +2 -1
  86. package/dist/commands/sources/installed-stashes.js +901 -177
  87. package/dist/commands/sources/schema-repair.js +174 -95
  88. package/dist/commands/sources/self-update.js +30 -74
  89. package/dist/commands/sources/source-add.js +3 -5
  90. package/dist/commands/sources/sources-cli.js +2 -15
  91. package/dist/commands/sources/update-transaction.js +220 -0
  92. package/dist/commands/tasks/tasks-cli.js +3 -3
  93. package/dist/commands/tasks/tasks.js +736 -317
  94. package/dist/commands/workflow-cli.js +2 -2
  95. package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
  96. package/dist/core/adapter/adapters/akm-adapter.js +85 -35
  97. package/dist/core/adapter/adapters/akm-lint.js +54 -39
  98. package/dist/core/adapter/adapters/akm-metadata.js +45 -45
  99. package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
  100. package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
  101. package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
  102. package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
  103. package/dist/core/adapter/adapters/index.js +0 -9
  104. package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
  105. package/dist/core/adapter/adapters/okf-adapter.js +4 -0
  106. package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
  107. package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
  108. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
  109. package/dist/core/adapter/execution-source.js +308 -0
  110. package/dist/core/adapter/recognize-match.js +36 -13
  111. package/dist/core/adapter/registry.js +0 -9
  112. package/dist/core/asset/stash-meta.js +94 -4
  113. package/dist/core/common.js +6 -11
  114. package/dist/core/config/config-io.js +3 -3
  115. package/dist/core/config/config-schema.js +18 -40
  116. package/dist/core/config/config-sources.js +11 -21
  117. package/dist/core/config/config-walker.js +31 -13
  118. package/dist/core/config/config.js +23 -26
  119. package/dist/core/config/schema/engines.js +8 -7
  120. package/dist/core/config/schema/improve-processes.js +29 -5
  121. package/dist/core/config/schema/index-config.js +0 -27
  122. package/dist/core/config/schema/primitives.js +1 -23
  123. package/dist/core/config/schema/sources-bundles.js +13 -16
  124. package/dist/core/errors.js +2 -0
  125. package/dist/core/events.js +68 -32
  126. package/dist/core/extra-params.js +1 -0
  127. package/dist/core/improve-result.js +315 -0
  128. package/dist/core/lesson-lint.js +0 -6
  129. package/dist/core/maintenance-barrier.js +4 -4
  130. package/dist/core/network-policy.js +152 -0
  131. package/dist/core/paths.js +1 -1
  132. package/dist/core/recognition-util.js +4 -4
  133. package/dist/core/registry-url.js +456 -0
  134. package/dist/core/state/migrations.js +161 -47
  135. package/dist/core/state-db.js +453 -80
  136. package/dist/core/system-error.js +32 -0
  137. package/dist/core/time.js +2 -12
  138. package/dist/core/write-source.js +0 -18
  139. package/dist/execution/directory-identity.js +52 -0
  140. package/dist/execution/executable-identity.js +107 -0
  141. package/dist/execution/guarded-source.js +398 -0
  142. package/dist/execution/json.js +95 -0
  143. package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
  144. package/dist/execution/record.js +55 -0
  145. package/dist/execution/resolved-request.js +730 -0
  146. package/dist/execution/source.js +320 -0
  147. package/dist/indexer/bundle-identity-guard.js +5 -4
  148. package/dist/indexer/db/graph-db.js +33 -0
  149. package/dist/indexer/graph/graph-boost.js +3 -4
  150. package/dist/indexer/graph/graph-extraction.js +562 -373
  151. package/dist/indexer/index-written-assets.js +78 -39
  152. package/dist/indexer/indexer.js +471 -432
  153. package/dist/indexer/installations.js +6 -0
  154. package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
  155. package/dist/indexer/materialize-embeddings.js +155 -0
  156. package/dist/indexer/passes/memory-inference.js +227 -174
  157. package/dist/indexer/passes/metadata.js +263 -118
  158. package/dist/indexer/scan/doc-to-entry.js +7 -10
  159. package/dist/indexer/scan/drain-dir.js +51 -23
  160. package/dist/indexer/search/db-search.js +156 -50
  161. package/dist/indexer/search/fts-query.js +40 -40
  162. package/dist/indexer/search/ranking.js +36 -1
  163. package/dist/indexer/search/search-attribution.js +3 -1
  164. package/dist/indexer/search/search-fields.js +23 -14
  165. package/dist/indexer/search/search-hit-enrichers.js +1 -1
  166. package/dist/indexer/search/search-source.js +7 -16
  167. package/dist/indexer/search/semantic-status.js +10 -1
  168. package/dist/indexer/usage/show-usage.js +105 -0
  169. package/dist/indexer/usage/usage-events.js +7 -2
  170. package/dist/indexer/walk/matchers.js +40 -10
  171. package/dist/indexer/walk/path-resolver.js +5 -2
  172. package/dist/indexer/walk/walker.js +20 -2
  173. package/dist/integrations/agent/builder-shared.js +3 -6
  174. package/dist/integrations/agent/conversation-fallback.js +16 -0
  175. package/dist/integrations/agent/engine-resolution.js +87 -87
  176. package/dist/integrations/agent/execution-cascade.js +566 -0
  177. package/dist/integrations/agent/execution-definitions.js +211 -0
  178. package/dist/integrations/agent/execution-lowering.js +811 -0
  179. package/dist/integrations/agent/execution-preparation.js +67 -0
  180. package/dist/integrations/agent/index.js +0 -2
  181. package/dist/integrations/agent/inline-execution.js +74 -0
  182. package/dist/integrations/agent/model-map.js +515 -0
  183. package/dist/integrations/agent/persona-fallback.js +30 -0
  184. package/dist/integrations/agent/request-lowering.js +186 -0
  185. package/dist/integrations/agent/runner-dispatch.js +230 -37
  186. package/dist/integrations/agent/runner.js +12 -83
  187. package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
  188. package/dist/integrations/harnesses/aider/index.js +0 -1
  189. package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
  190. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  191. package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
  192. package/dist/integrations/harnesses/claude/index.js +1 -5
  193. package/dist/integrations/harnesses/claude/session-log.js +3 -33
  194. package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
  195. package/dist/integrations/harnesses/codex/index.js +0 -1
  196. package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
  197. package/dist/integrations/harnesses/copilot/index.js +0 -1
  198. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
  199. package/dist/integrations/harnesses/gemini/index.js +0 -1
  200. package/dist/integrations/harnesses/index.js +4 -44
  201. package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
  202. package/dist/integrations/harnesses/opencode/index.js +0 -2
  203. package/dist/integrations/harnesses/opencode/session-log.js +14 -204
  204. package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
  205. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
  206. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
  207. package/dist/integrations/harnesses/openhands/index.js +0 -1
  208. package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
  209. package/dist/integrations/harnesses/pi/index.js +0 -1
  210. package/dist/integrations/harnesses/shared.js +0 -1
  211. package/dist/integrations/harnesses/types.js +1 -3
  212. package/dist/integrations/lockfile.js +82 -79
  213. package/dist/integrations/session-logs/index.js +6 -17
  214. package/dist/integrations/session-logs/provider-base.js +1 -29
  215. package/dist/llm/client.js +10 -5
  216. package/dist/llm/embedder.js +6 -7
  217. package/dist/llm/embedders/local.js +37 -88
  218. package/dist/llm/embedders/types.js +1 -1
  219. package/dist/llm/graph-extract.js +75 -50
  220. package/dist/llm/index-passes.js +43 -5
  221. package/dist/llm/memory-infer.js +8 -6
  222. package/dist/llm/metadata-enhance.js +5 -3
  223. package/dist/llm/structured-call.js +122 -25
  224. package/dist/output/format-exempt.js +1 -1
  225. package/dist/output/render-registry.js +0 -16
  226. package/dist/output/renderers.js +12 -7
  227. package/dist/output/shapes/curate.js +1 -0
  228. package/dist/output/shapes/helpers.js +10 -2
  229. package/dist/output/shapes/passthrough.js +2 -0
  230. package/dist/output/text/command-format.js +31 -33
  231. package/dist/output/text/health-format.js +1 -29
  232. package/dist/output/text/migrate.js +6 -56
  233. package/dist/output/text/proposal-format.js +16 -1
  234. package/dist/output/text/workflow-format.js +16 -0
  235. package/dist/registry/network.js +279 -0
  236. package/dist/registry/pinned-request-helper.js +247 -0
  237. package/dist/registry/pinned-transport.js +717 -0
  238. package/dist/registry/providers/skills-sh.js +18 -6
  239. package/dist/registry/providers/static-index.js +20 -7
  240. package/dist/registry/resolve.js +53 -28
  241. package/dist/scripts/akm-migrate-node.js +19334 -52269
  242. package/dist/scripts/akm-migrate.js +19270 -51612
  243. package/dist/setup/registry-stash-loader.js +64 -20
  244. package/dist/setup/semantic-assets.js +9 -34
  245. package/dist/setup/setup.js +12 -30
  246. package/dist/setup/source-identity.js +17 -0
  247. package/dist/setup/steps/sources.js +36 -15
  248. package/dist/setup/steps/tasks.js +39 -11
  249. package/dist/sources/providers/git-provider.js +3 -3
  250. package/dist/sources/providers/npm.js +2 -2
  251. package/dist/sources/providers/provider-utils.js +4 -3
  252. package/dist/sources/providers/website.js +11 -7
  253. package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
  254. package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
  255. package/dist/sources/website-url.js +73 -0
  256. package/dist/storage/engines/sqlite-migrations.js +81 -26
  257. package/dist/storage/managed-db.js +27 -24
  258. package/dist/storage/repositories/events-repository.js +3 -0
  259. package/dist/storage/repositories/index-connection.js +42 -10
  260. package/dist/storage/repositories/index-entries-repository.js +203 -229
  261. package/dist/storage/repositories/index-entry-mapper.js +8 -12
  262. package/dist/storage/repositories/index-entry-schema.js +255 -0
  263. package/dist/storage/repositories/index-fts-repository.js +64 -71
  264. package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
  265. package/dist/storage/repositories/index-meta-repository.js +0 -11
  266. package/dist/storage/repositories/index-schema.js +74 -350
  267. package/dist/storage/repositories/index-utility-repository.js +12 -17
  268. package/dist/storage/repositories/index-vec-repository.js +56 -7
  269. package/dist/storage/repositories/proposals-repository.js +4 -127
  270. package/dist/storage/repositories/registry-cache.js +2 -1
  271. package/dist/storage/repositories/task-history-repository.js +20 -40
  272. package/dist/storage/repositories/workflow-runs-repository.js +228 -129
  273. package/dist/storage/sqlite-read-snapshot.js +148 -0
  274. package/dist/tasks/backends/cron.js +170 -42
  275. package/dist/tasks/backends/index.js +1 -1
  276. package/dist/tasks/backends/launchd.js +787 -202
  277. package/dist/tasks/backends/schtasks.js +282 -83
  278. package/dist/tasks/embedded.js +7 -7
  279. package/dist/tasks/frozen-script.js +50 -0
  280. package/dist/tasks/resolve-akm-bin.js +5 -1
  281. package/dist/tasks/runner.js +239 -251
  282. package/dist/tasks/runtime-v3.js +281 -0
  283. package/dist/tasks/scheduler-binding.js +272 -0
  284. package/dist/tasks/scheduler-invocation.js +57 -43
  285. package/dist/tasks/scheduler-sync.js +654 -0
  286. package/dist/tasks/source-v3.js +752 -0
  287. package/dist/tasks/standalone-script-entry.js +5 -0
  288. package/dist/tasks/task-id.js +29 -0
  289. package/dist/workflows/authoring/authoring.js +15 -32
  290. package/dist/workflows/exec/dispatch-redaction.js +14 -8
  291. package/dist/workflows/exec/exec-unit.js +7 -28
  292. package/dist/workflows/exec/frozen-judge.js +57 -89
  293. package/dist/workflows/exec/lowering-notices.js +23 -0
  294. package/dist/workflows/exec/native-executor.js +301 -458
  295. package/dist/workflows/exec/param-secrets.js +4 -3
  296. package/dist/workflows/exec/run-workflow.js +26 -32
  297. package/dist/workflows/exec/step-work.js +105 -109
  298. package/dist/workflows/exec/unit-dispatch.js +103 -27
  299. package/dist/workflows/exec/unit-writer.js +3 -3
  300. package/dist/workflows/exec/worktree.js +2 -2
  301. package/dist/workflows/ir/compile.js +86 -72
  302. package/dist/workflows/ir/environment-v4.js +328 -0
  303. package/dist/workflows/ir/freeze-v4.js +122 -0
  304. package/dist/workflows/ir/plan-hash.js +13 -7
  305. package/dist/workflows/ir/schema-v4.js +525 -0
  306. package/dist/workflows/ir/schema.js +25 -284
  307. package/dist/workflows/ir/source-freeze-v4.js +506 -0
  308. package/dist/workflows/parser.js +27 -24
  309. package/dist/workflows/program/schema.js +1 -2
  310. package/dist/workflows/renderer.js +42 -29
  311. package/dist/workflows/resource-limits.js +4 -5
  312. package/dist/workflows/runtime/agent-identity.js +11 -13
  313. package/dist/workflows/runtime/plan-classifier.js +8 -8
  314. package/dist/workflows/runtime/runs.js +27 -43
  315. package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
  316. package/dist/workflows/source-files.js +373 -0
  317. package/dist/workflows/source-ir/compile.js +196 -0
  318. package/dist/workflows/source-ir/github-yaml.js +577 -0
  319. package/dist/workflows/source-ir/ordering.js +38 -0
  320. package/dist/workflows/source-ir/program.js +50 -0
  321. package/dist/workflows/source-ir/result.js +26 -0
  322. package/dist/workflows/source-ir/schema.js +772 -0
  323. package/dist/workflows/source-ir/semantics.js +242 -0
  324. package/dist/workflows/source-ir/uses.js +14 -0
  325. package/docs/README.md +2 -0
  326. package/docs/migration/README.md +3 -1
  327. package/docs/migration/release-notes/0.9.2.md +55 -0
  328. package/docs/migration/release-notes/README.md +5 -0
  329. package/docs/migration/v0.8-to-v0.9.md +76 -1077
  330. package/docs/migration/v0.9.0-troubleshooting.md +104 -516
  331. package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
  332. package/docs/reference/README.md +1 -0
  333. package/docs/reference/cli.md +230 -98
  334. package/docs/reference/configuration.md +159 -36
  335. package/docs/reference/data-and-telemetry.md +19 -1
  336. package/docs/reference/supported-formats.md +23 -3
  337. package/docs/reference/tasks.md +182 -0
  338. package/docs/reference/workflow-schema.md +91 -40
  339. package/docs/reference/workflows.md +33 -6
  340. package/package.json +10 -6
  341. package/schemas/akm-config.json +372 -224
  342. package/schemas/akm-task.json +324 -80
  343. package/schemas/akm-workflow.json +6 -9
  344. package/dist/core/migration-operation.js +0 -75
  345. package/dist/integrations/agent/model-aliases.js +0 -74
  346. package/dist/tasks/parser.js +0 -380
  347. package/dist/tasks/schema.js +0 -123
  348. package/dist/tasks/validator.js +0 -80
  349. package/dist/workflows/ir/freeze.js +0 -320
  350. 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 { warn } from "../../core/warn.js";
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, sanitizeFtsQuery } from "./fts-query.js";
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, usedSemantic } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources, includeExcludedTypes, disableProjectContext, disableScopedUtility);
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: usedSemantic ? "semantic" : "keyword",
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 && sanitizeFtsQuery(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
- usedSemantic: false,
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
- usedSemantic: false,
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 tEmbed0 = Date.now();
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
- // Deterministic tiebreaker on equal scores.
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, usedSemantic };
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
- * indexes description/tags/searchHints/aliases, not raw body prose), while the
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
- warn("Vector search failed, skipping:", error instanceof Error ? error.message : String(error));
645
- return null;
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
- if (input.attributionSource)
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 scored results by file path, keeping only the highest-scored
786
- * entry per unique path. Sorts by score descending internally to ensure the
787
- * precondition is always met regardless of caller.
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 sorted.filter((item) => {
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-string helpers, extracted from indexer/db/db.ts.
5
+ * Pure FTS5 query planning and ref-query helpers.
6
6
  *
7
- * These transform a raw user query into an FTS5-safe MATCH expression. They
8
- * touch no database state, so they are unit-testable with zero DB setup.
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
- * Sanitize a raw user query into an FTS5-safe implicit-AND expression.
14
- *
15
- * Allows only characters safe in FTS5 queries: letters, digits, underscores,
16
- * and whitespace. Everything else (hyphens, dots, quotes, parens, asterisks,
17
- * colons, carets, @, !, etc.) is replaced with a space so that compound
18
- * identifiers like "code-review" or "k8s.setup" become AND-joined tokens
19
- * ("code review", "k8s setup") rather than triggering FTS5 syntax errors.
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 a prefix query from an FTS5 query string by appending `*` to each
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
- * Returns null if no tokens qualify for prefix expansion.
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 buildPrefixQuery(ftsQuery) {
41
- const tokens = ftsQuery.split(/\s+/).filter(Boolean);
42
- let hasPrefix = false;
43
- const prefixTokens = tokens.map((t) => {
44
- if (t.length >= 3) {
45
- hasPrefix = true;
46
- return `${t}*`;
47
- }
48
- return t;
49
- });
50
- if (!hasPrefix)
51
- return null;
52
- return prefixTokens.join(" ");
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.toLowerCase().split(/\s+/).filter(Boolean);
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: TOC headings + parameters + the config-gated body opening
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
- // Stash-organization conventions (SPEC-8): the self-situating body opening
69
- // (captured by the metadata pass only when `index.indexBodyOpening` is on)
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
- return [fields.name, fields.description, fields.tags, fields.hints, fields.content]
88
- .filter((s) => s.length > 0)
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.stashDir);
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.