akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.4

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 (343) hide show
  1. package/CHANGELOG.md +756 -0
  2. package/dist/akm +94 -196
  3. package/dist/cli/shared.js +6 -2
  4. package/dist/cli.js +22 -9
  5. package/dist/commands/agent/agent-dispatch.js +1 -1
  6. package/dist/commands/command/command-execution.js +24 -62
  7. package/dist/commands/feedback-cli.js +0 -1
  8. package/dist/commands/health/accept-rate.js +2 -2
  9. package/dist/commands/health/checks.js +30 -75
  10. package/dist/commands/health/config-skew.js +38 -0
  11. package/dist/commands/health/egress.js +54 -0
  12. package/dist/commands/health/html-report.js +0 -38
  13. package/dist/commands/health/improve-metrics.js +123 -562
  14. package/dist/commands/health/plugin-staleness.js +53 -3
  15. package/dist/commands/health/renderers.js +12 -4
  16. package/dist/commands/health/report-view-model.js +11 -106
  17. package/dist/commands/health/types-improve.js +4 -19
  18. package/dist/commands/health/windows.js +64 -73
  19. package/dist/commands/health.js +122 -143
  20. package/dist/commands/improve/consolidate/chunking.js +25 -100
  21. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  22. package/dist/commands/improve/consolidate.js +538 -1075
  23. package/dist/commands/improve/content-hash.js +16 -24
  24. package/dist/commands/improve/distill/content-repair.js +18 -100
  25. package/dist/commands/improve/distill-guards.js +20 -81
  26. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  27. package/dist/commands/improve/distill.js +608 -1075
  28. package/dist/commands/improve/eligibility.js +126 -400
  29. package/dist/commands/improve/execution.js +3 -5
  30. package/dist/commands/improve/extract.js +487 -1046
  31. package/dist/commands/improve/feedback-valence.js +0 -25
  32. package/dist/commands/improve/improve-cli.js +29 -166
  33. package/dist/commands/improve/improve-result-file.js +10 -66
  34. package/dist/commands/improve/improve-strategies.js +12 -7
  35. package/dist/commands/improve/improve-usage-report.js +18 -64
  36. package/dist/commands/improve/improve.js +443 -1063
  37. package/dist/commands/improve/ledger.js +114 -0
  38. package/dist/commands/improve/locks.js +2 -8
  39. package/dist/commands/improve/loop-stages.js +459 -1172
  40. package/dist/commands/improve/memory/derived-ref.js +12 -77
  41. package/dist/commands/improve/memory/memory-belief.js +14 -118
  42. package/dist/commands/improve/memory/memory-improve.js +4 -3
  43. package/dist/commands/improve/outcome-loop.js +28 -156
  44. package/dist/commands/improve/planner.js +5 -10
  45. package/dist/commands/improve/preparation.js +851 -2339
  46. package/dist/commands/improve/proactive-maintenance.js +34 -101
  47. package/dist/commands/improve/reflect-noise.js +104 -280
  48. package/dist/commands/improve/reflect.js +621 -1367
  49. package/dist/commands/improve/salience.js +46 -232
  50. package/dist/commands/improve/session-asset.js +19 -100
  51. package/dist/commands/improve/stage.js +323 -0
  52. package/dist/commands/proposal/drain.js +251 -644
  53. package/dist/commands/proposal/proposal-cli.js +3 -18
  54. package/dist/commands/proposal/proposal-types.js +20 -41
  55. package/dist/commands/proposal/proposal.js +1 -2
  56. package/dist/commands/proposal/propose.js +134 -160
  57. package/dist/commands/proposal/repository.js +502 -1487
  58. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  59. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  60. package/dist/commands/proposal/validators/proposals.js +13 -89
  61. package/dist/commands/read/curate.js +63 -413
  62. package/dist/commands/read/search-cli.js +16 -33
  63. package/dist/commands/read/search.js +17 -23
  64. package/dist/commands/read/show.js +2 -13
  65. package/dist/commands/sources/bundle-cli.js +25 -2
  66. package/dist/commands/sources/bundle-config-ops.js +7 -0
  67. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  68. package/dist/commands/sources/info.js +2 -11
  69. package/dist/commands/sources/installed-stashes.js +197 -746
  70. package/dist/commands/sources/schema-repair.js +98 -129
  71. package/dist/commands/sources/source-add.js +62 -12
  72. package/dist/commands/sources/stash-cli.js +1 -1
  73. package/dist/commands/tasks/explain.js +10 -13
  74. package/dist/commands/tasks/tasks-cli.js +9 -8
  75. package/dist/commands/tasks/tasks.js +326 -930
  76. package/dist/commands/tasks/validate.js +42 -21
  77. package/dist/commands/workflow/plan.js +22 -29
  78. package/dist/commands/workflow-cli.js +4 -4
  79. package/dist/core/adapter/adapters/akm-adapter.js +0 -1
  80. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  81. package/dist/core/adapter/adapters/akm-metadata.js +11 -12
  82. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  83. package/dist/core/adapter/execution-source.js +17 -29
  84. package/dist/core/asset/resolve-ref.js +1 -1
  85. package/dist/core/bundle-id.js +42 -5
  86. package/dist/core/bundle-rename.js +291 -0
  87. package/dist/core/config/config-io.js +1 -2
  88. package/dist/core/config/config-schema.js +1 -33
  89. package/dist/core/config/config-walker.js +1 -1
  90. package/dist/core/config/config.js +163 -68
  91. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  92. package/dist/core/config/schema/embedding.js +20 -5
  93. package/dist/core/config/schema/engines.js +5 -0
  94. package/dist/core/config/schema/execution.js +1 -1
  95. package/dist/core/config/schema/experimental.js +1 -1
  96. package/dist/core/config/schema/improve-processes.js +21 -95
  97. package/dist/core/config/schema/improve.js +4 -42
  98. package/dist/core/config/schema/scheduler.js +12 -12
  99. package/dist/core/config/schema/search.js +6 -22
  100. package/dist/core/env-secret-ref.js +0 -1
  101. package/dist/core/errors.js +8 -9
  102. package/dist/core/file-lock.js +76 -173
  103. package/dist/core/logs-db.js +2 -2
  104. package/dist/core/paths.js +0 -27
  105. package/dist/core/redaction.js +109 -2
  106. package/dist/core/run-lock.js +2 -5
  107. package/dist/core/spawn-env.js +1 -1
  108. package/dist/core/state/migrations.js +108 -61
  109. package/dist/core/state-db-scope.js +2 -4
  110. package/dist/core/state-db.js +126 -692
  111. package/dist/core/type-presentation.js +1 -9
  112. package/dist/core/write-source.js +293 -1012
  113. package/dist/execution/input-contract.js +1 -1
  114. package/dist/execution/resolved-request.js +135 -689
  115. package/dist/execution/source.js +63 -257
  116. package/dist/execution/target-ref.js +1 -1
  117. package/dist/indexer/bundle-identity-guard.js +2 -2
  118. package/dist/indexer/db/graph-db.js +106 -46
  119. package/dist/indexer/ensure-index.js +44 -85
  120. package/dist/indexer/graph/graph-extraction.js +340 -562
  121. package/dist/indexer/graph/graph-related.js +130 -0
  122. package/dist/indexer/index-rebuild-lock.js +3 -11
  123. package/dist/indexer/index-writer-lock.js +8 -17
  124. package/dist/indexer/index-written-assets.js +139 -151
  125. package/dist/indexer/indexer.js +524 -846
  126. package/dist/indexer/materialize-embeddings.js +60 -397
  127. package/dist/indexer/passes/memory-inference.js +81 -90
  128. package/dist/indexer/passes/metadata.js +132 -200
  129. package/dist/indexer/read-preflight.js +0 -7
  130. package/dist/indexer/scan/doc-to-entry.js +1 -3
  131. package/dist/indexer/scan/drain-dir.js +1 -1
  132. package/dist/indexer/search/db-search.js +181 -590
  133. package/dist/indexer/search/fts-query.js +30 -41
  134. package/dist/indexer/search/ranking.js +28 -154
  135. package/dist/indexer/search/search-attribution.js +12 -32
  136. package/dist/indexer/search/search-fields.js +11 -15
  137. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  138. package/dist/indexer/search/search-source.js +1 -4
  139. package/dist/indexer/usage/usage-events.js +2 -7
  140. package/dist/integrations/agent/engine-fallback.js +23 -40
  141. package/dist/integrations/agent/engine-resolution.js +93 -183
  142. package/dist/integrations/agent/execution.js +507 -0
  143. package/dist/integrations/agent/model-map.js +28 -156
  144. package/dist/integrations/agent/request-lowering.js +66 -141
  145. package/dist/integrations/agent/runner-dispatch.js +143 -321
  146. package/dist/integrations/agent/runner.js +54 -14
  147. package/dist/integrations/lockfile.js +53 -101
  148. package/dist/llm/embedders/deterministic.js +2 -3
  149. package/dist/llm/embedders/profile.js +71 -0
  150. package/dist/llm/embedders/remote.js +10 -15
  151. package/dist/llm/graph-extract.js +3 -12
  152. package/dist/llm/index-passes.js +3 -5
  153. package/dist/llm/memory-infer.js +1 -2
  154. package/dist/llm/metadata-enhance.js +1 -2
  155. package/dist/llm/structured-call.js +5 -24
  156. package/dist/output/generic-render.js +23 -11
  157. package/dist/output/html-render.js +13 -10
  158. package/dist/output/render-registry.js +3 -32
  159. package/dist/output/shapes/helpers.js +2 -34
  160. package/dist/output/shapes/passthrough.js +1 -9
  161. package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
  162. package/dist/output/text/command-format.js +60 -23
  163. package/dist/output/text/helpers.js +1 -1
  164. package/dist/output/text/migrate.js +5 -14
  165. package/dist/output/text/proposal-format.js +1 -2
  166. package/dist/output/text/workflow-format.js +0 -32
  167. package/dist/output/text.js +2 -0
  168. package/dist/registry/factory.js +4 -19
  169. package/dist/registry/network.js +66 -220
  170. package/dist/registry/providers/index.js +0 -2
  171. package/dist/registry/providers/skills-sh.js +3 -14
  172. package/dist/registry/providers/static-index.js +24 -26
  173. package/dist/registry/resolve.js +55 -131
  174. package/dist/scripts/akm-migrate-node.js +43937 -93313
  175. package/dist/scripts/akm-migrate.js +43697 -93071
  176. package/dist/setup/registry-stash-loader.js +4 -13
  177. package/dist/setup/semantic-assets.js +3 -44
  178. package/dist/setup/setup.js +1 -1
  179. package/dist/setup/steps/tasks.js +25 -15
  180. package/dist/sources/provider-factory.js +17 -18
  181. package/dist/sources/providers/filesystem.js +2 -3
  182. package/dist/sources/providers/git-install.js +7 -1
  183. package/dist/sources/providers/git-provider.js +0 -3
  184. package/dist/sources/providers/git-stash.js +0 -17
  185. package/dist/sources/providers/npm.js +2 -4
  186. package/dist/sources/providers/provider-utils.js +5 -10
  187. package/dist/sources/providers/website.js +0 -2
  188. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  189. package/dist/sources/website-url.js +2 -2
  190. package/dist/storage/database.js +9 -35
  191. package/dist/storage/repositories/improve-ledger-repository.js +168 -0
  192. package/dist/storage/repositories/index-connection.js +34 -70
  193. package/dist/storage/repositories/index-entries-repository.js +69 -111
  194. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  195. package/dist/storage/repositories/index-entry-schema.js +83 -269
  196. package/dist/storage/repositories/index-fts-repository.js +86 -256
  197. package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
  198. package/dist/storage/repositories/index-meta-repository.js +6 -4
  199. package/dist/storage/repositories/index-schema.js +192 -220
  200. package/dist/storage/repositories/index-utility-repository.js +8 -29
  201. package/dist/storage/repositories/index-vec-repository.js +133 -414
  202. package/dist/storage/repositories/outcome-repository.js +2 -1
  203. package/dist/storage/repositories/proposals-repository.js +35 -0
  204. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  205. package/dist/storage/repositories/task-history-repository.js +26 -4
  206. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  207. package/dist/storage/sqlite-migrations.js +136 -0
  208. package/dist/storage/sqlite-pragmas.js +11 -9
  209. package/dist/storage/sqlite-transaction.js +170 -0
  210. package/dist/storage/state-db-integrity.js +34 -27
  211. package/dist/tasks/activation-config.js +134 -62
  212. package/dist/tasks/backends/cron.js +129 -277
  213. package/dist/tasks/backends/exec-utils.js +2 -5
  214. package/dist/tasks/backends/launchd.js +125 -745
  215. package/dist/tasks/backends/schtasks.js +101 -620
  216. package/dist/tasks/prepare/prepare-support.js +5 -15
  217. package/dist/tasks/prepare/prepare.js +0 -2
  218. package/dist/tasks/resolve-akm-bin.js +20 -79
  219. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  220. package/dist/tasks/scheduler-binding.js +18 -238
  221. package/dist/tasks/scheduler-invocation.js +52 -52
  222. package/dist/tasks/scheduler-lock.js +53 -0
  223. package/dist/tasks/scheduler-sync.js +363 -679
  224. package/dist/tasks/source/parse-task-source.js +160 -10
  225. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  226. package/dist/tasks/source/task-to-v4.js +2 -2
  227. package/dist/workflows/authoring/authoring.js +3 -12
  228. package/dist/workflows/compile.js +211 -0
  229. package/dist/workflows/concurrency-policy.js +13 -74
  230. package/dist/workflows/exec/child-invocation.js +3 -17
  231. package/dist/workflows/exec/child-workflow.js +32 -141
  232. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  233. package/dist/workflows/exec/environment.js +98 -0
  234. package/dist/workflows/exec/exec-unit.js +33 -140
  235. package/dist/workflows/exec/frozen-judge.js +7 -59
  236. package/dist/workflows/exec/native-executor.js +82 -341
  237. package/dist/workflows/exec/param-secrets.js +29 -47
  238. package/dist/workflows/exec/run-workflow.js +154 -387
  239. package/dist/workflows/exec/scheduler.js +9 -36
  240. package/dist/workflows/exec/step-work.js +127 -430
  241. package/dist/workflows/exec/unit-dispatch.js +11 -63
  242. package/dist/workflows/exec/unit-writer.js +8 -52
  243. package/dist/workflows/exec/worktree.js +39 -273
  244. package/dist/workflows/freeze/child-output-references.js +4 -15
  245. package/dist/workflows/freeze/environment.js +99 -92
  246. package/dist/workflows/freeze/freeze.js +172 -0
  247. package/dist/workflows/freeze/step-values.js +19 -21
  248. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  249. package/dist/workflows/freeze/targets/command.js +10 -33
  250. package/dist/workflows/freeze/targets/script.js +5 -12
  251. package/dist/workflows/freeze/targets/shell.js +3 -6
  252. package/dist/workflows/freeze/targets/task.js +25 -80
  253. package/dist/workflows/freeze/task-bindings.js +20 -67
  254. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  255. package/dist/workflows/ir/params.js +6 -51
  256. package/dist/workflows/ir/plan-hash.js +2 -34
  257. package/dist/workflows/parser.js +140 -43
  258. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  259. package/dist/workflows/renderer.js +36 -69
  260. package/dist/workflows/resource-limits.js +12 -120
  261. package/dist/workflows/runtime/agent-identity.js +8 -40
  262. package/dist/workflows/runtime/run-outputs.js +3 -6
  263. package/dist/workflows/runtime/run-plan.js +316 -0
  264. package/dist/workflows/runtime/runs.js +48 -200
  265. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  266. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  267. package/dist/workflows/validate-summary.js +2 -7
  268. package/docs/integration/bundling-akm.md +49 -42
  269. package/docs/migration/README.md +1 -0
  270. package/docs/migration/release-notes/0.9.17.md +41 -0
  271. package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
  272. package/docs/reference/cli.md +182 -125
  273. package/docs/reference/configuration.md +49 -56
  274. package/docs/reference/data-and-telemetry.md +19 -20
  275. package/docs/reference/tasks.md +86 -38
  276. package/docs/reference/workflow-schema.md +14 -18
  277. package/docs/reference/workflows.md +6 -9
  278. package/package.json +1 -1
  279. package/schemas/akm-config.json +87 -406
  280. package/dist/commands/health/advisories.js +0 -150
  281. package/dist/commands/health/metrics.js +0 -329
  282. package/dist/commands/health/surfaces.js +0 -102
  283. package/dist/commands/improve/anti-collapse.js +0 -83
  284. package/dist/commands/improve/collapse-detector.js +0 -432
  285. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  286. package/dist/commands/improve/consolidate/merge.js +0 -146
  287. package/dist/commands/improve/distill/promote-memory.js +0 -329
  288. package/dist/commands/improve/distill/quality-gate.js +0 -500
  289. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  290. package/dist/commands/improve/proposal-envelope.js +0 -31
  291. package/dist/commands/improve/run-context.js +0 -123
  292. package/dist/commands/improve/shared.js +0 -21
  293. package/dist/commands/improve/source-identity.js +0 -28
  294. package/dist/commands/improve/triage.js +0 -96
  295. package/dist/commands/proposal/drain-policies.js +0 -151
  296. package/dist/commands/sources/update-transaction.js +0 -220
  297. package/dist/core/action-contributors.js +0 -28
  298. package/dist/core/config/config-version-shim.js +0 -101
  299. package/dist/core/config/retired-experimental-keys-shim.js +0 -62
  300. package/dist/core/fs-txn.js +0 -405
  301. package/dist/core/lexical-score.js +0 -25
  302. package/dist/core/maintenance-barrier.js +0 -167
  303. package/dist/execution/executable-identity.js +0 -105
  304. package/dist/execution/guarded-source.js +0 -427
  305. package/dist/indexer/graph/graph-boost.js +0 -427
  306. package/dist/indexer/graph/graph-dedup.js +0 -95
  307. package/dist/indexer/search/name-match.js +0 -35
  308. package/dist/indexer/search/ranking-contributors.js +0 -515
  309. package/dist/indexer/walk/project-context.js +0 -192
  310. package/dist/integrations/agent/execution-cascade.js +0 -566
  311. package/dist/integrations/agent/execution-definitions.js +0 -202
  312. package/dist/integrations/agent/execution-lowering.js +0 -841
  313. package/dist/integrations/agent/execution-preparation.js +0 -98
  314. package/dist/integrations/agent/inline-execution.js +0 -74
  315. package/dist/registry/create-provider-registry.js +0 -29
  316. package/dist/registry/pinned-request-helper.js +0 -247
  317. package/dist/registry/pinned-transport.js +0 -717
  318. package/dist/sources/providers/index.js +0 -14
  319. package/dist/storage/engines/sqlite-migrations.js +0 -271
  320. package/dist/storage/repositories/canaries-repository.js +0 -107
  321. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  322. package/dist/storage/repositories/registry-cache.js +0 -113
  323. package/dist/tasks/scheduler-sync-preview.js +0 -52
  324. package/dist/workflows/freeze/resolve-steps.js +0 -86
  325. package/dist/workflows/freeze/source-freeze.js +0 -64
  326. package/dist/workflows/ir/compile.js +0 -321
  327. package/dist/workflows/ir/environment-v4.js +0 -330
  328. package/dist/workflows/ir/freeze-v4.js +0 -153
  329. package/dist/workflows/ir/schema-v4.js +0 -745
  330. package/dist/workflows/ir/schema.js +0 -354
  331. package/dist/workflows/program/schema.js +0 -78
  332. package/dist/workflows/runtime/checkin.js +0 -57
  333. package/dist/workflows/runtime/plan-classifier.js +0 -196
  334. package/dist/workflows/runtime/unit-checkin.js +0 -45
  335. package/dist/workflows/runtime/unit-phases.js +0 -20
  336. package/dist/workflows/schema.js +0 -4
  337. package/dist/workflows/source-ir/compile.js +0 -200
  338. package/dist/workflows/source-ir/program.js +0 -50
  339. package/dist/workflows/source-ir/result.js +0 -26
  340. package/dist/workflows/source-ir/schema.js +0 -786
  341. package/dist/workflows/source-ir/triggers.js +0 -79
  342. package/dist/workflows/source-ir/uses.js +0 -40
  343. package/dist/workflows/validator.js +0 -60
@@ -1,50 +1,39 @@
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
- const UNICODE_TOKEN = /[\p{L}\p{N}]+/gu;
5
- function quoteToken(token) {
6
- return `"${token}"`;
7
- }
8
- function prefixToken(token) {
9
- return [...token].length >= 3 ? `${quoteToken(token)}*` : quoteToken(token);
10
- }
11
4
  /**
12
- * Build the sole lexical retrieval plan from raw user input.
5
+ * Pure FTS5 query building and ref-query helpers.
13
6
  *
14
- * Tokenization follows the useful portion of SQLite FTS5's `unicode61`
15
- * tokenizer (Unicode letters and numbers). Quoting every term makes FTS
16
- * operators ordinary searchable words. Tokens are normalized and deduplicated
17
- * case-insensitively.
18
- *
19
- * There is deliberately NO cap on token count. `MAX_LEXICAL_QUERY_TOKENS = 16`
20
- * used to truncate here, silently: a query past 16 unique tokens searched only
21
- * its first 16, dropping the tail — which for natural-language input is
22
- * usually where the discriminating words are. It was unexplained in both the
23
- * code and the commit that introduced it, unreachable from any flag, config
24
- * key, or env var, and the user was never told their query had been altered.
25
- * A wrong answer delivered silently is worse than a slow one.
7
+ * The lexical channel matches ANY query word: BM25 over an OR of the query's
8
+ * non-stopword tokens. Requiring every word first and relaxing only when that
9
+ * found nothing cost 0.108 nDCG@10 on the retrieval suite
10
+ * (`akm/eval/retrieval/reports/baseline-2026-09-27.md`): one conjunctive match
11
+ * in a long document suppressed every better OR candidate.
12
+ * `parseRefPrefixQuery` is the one non-FTS helper: it decides whether a raw
13
+ * query should bypass FTS entirely (SPEC-4 ref-prefix enumeration).
26
14
  */
27
- export function buildLexicalQueryPlan(query) {
28
- const tokens = [];
29
- const seen = new Set();
30
- const normalized = query.normalize("NFKC");
31
- for (const match of normalized.matchAll(UNICODE_TOKEN)) {
32
- const token = match[0];
33
- const key = token.toLowerCase();
34
- if (seen.has(key))
35
- continue;
36
- seen.add(key);
37
- tokens.push(token);
38
- }
39
- const exact = tokens.map(quoteToken).join(" ");
40
- const prefixTokens = tokens.map(prefixToken);
41
- const exactPrefix = prefixTokens.some((token) => token.endsWith("*")) ? prefixTokens.join(" ") : undefined;
42
- // A slash-bearing, whitespace-free input is an identifier/ref lookup, not
43
- // sentence prose. Keep it conjunctive so a mistyped/bare ref never fans out
44
- // across every path token through OR recovery.
45
- const isRefLikeIdentifier = !/\s/u.test(query.trim()) && query.includes("/");
46
- const relaxed = tokens.length > 1 && !isRefLikeIdentifier ? prefixTokens.join(" OR ") : undefined;
47
- return { tokens, exact, exactPrefix, relaxed };
15
+ /** English function words dropped from the lexical query (the retrieval lab's list). */
16
+ const STOPWORDS = new Set(`a about above after again against all am an and any are as at be because been before being below between both but
17
+ by can could did do does doing down during each few for from further had has have having he her here hers herself
18
+ him himself his how i if in into is it its itself just me more most my myself no nor not now of off on once only or
19
+ other our ours out over own same she should so some such than that the their theirs them then there these they this
20
+ those through to too under until up very was we were what when where which while who whom why will with would you
21
+ your yours yourself`.split(/\s+/));
22
+ const UNICODE_TOKEN = /[\p{L}\p{N}]+/gu;
23
+ /**
24
+ * The query's lexical tokens: Unicode letters and numbers (the useful part of
25
+ * FTS5's `unicode61` tokenizer), NFKC-normalized, lowercased and deduplicated,
26
+ * with stopwords removed. A query made only of stopwords keeps all of them, so
27
+ * "how to" still searches for something.
28
+ */
29
+ export function ftsQueryTokens(query) {
30
+ const tokens = [...new Set(query.normalize("NFKC").toLowerCase().match(UNICODE_TOKEN) ?? [])];
31
+ const content = tokens.filter((token) => !STOPWORDS.has(token));
32
+ return content.length > 0 ? content : tokens;
33
+ }
34
+ /** FTS5 MATCH expression matching any token. Quoting makes FTS operators ordinary words. */
35
+ export function ftsOrMatch(tokens) {
36
+ return tokens.map((token) => `"${token}"`).join(" OR ");
48
37
  }
49
38
  /**
50
39
  * D4 — parse a conceptId-prefix browse query.
@@ -1,163 +1,37 @@
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
- import { stableFtsScore } from "../../core/lexical-score.js";
5
- import { getUtilityScoresByIds } from "../../storage/repositories/index-utility-repository.js";
6
- import { buildLexicalQueryPlan } from "./fts-query.js";
7
- import { lexicalNameTokens, structuralNameTokenMatch } from "./name-match.js";
8
- import { applyBeliefStateScoreCeiling, applyScoreContributors, applyUtilityContributors, defaultRankingContributors, defaultUtilityRankingContributors, } from "./ranking-contributors.js";
9
4
  /**
10
- * Lower bounds keep a lexical hit competitive with a vector-only neighbour;
11
- * the upper bound deliberately leaves room for the ranking contributors that
12
- * run after retrieval (notably the bounded graph boost). This is a
13
- * calibration for the one search pipeline, not a claim that BM25 is
14
- * comparable across different queries or FTS tables.
15
- */
16
- /**
17
- * Convert FTS5's negative BM25 value into the lexical contribution used by
18
- * this pipeline. The transform is fixed and monotone: it depends only on a
19
- * row's own BM25 value, so appending weaker candidates cannot rewrite an
20
- * existing row's score. FTS5 commonly emits relevance near 1e-6 for broad
21
- * queries, so first put relevance on a log scale around that observed value.
22
- * The shape constant intentionally makes the curve approach its ceiling
23
- * slowly: rare-term scores retain separation instead of all reading as 0.8.
5
+ * Reciprocal rank fusion (Cormack, Clarke and Büttcher, SIGIR 2009) of the
6
+ * lexical and vector candidate lists — the whole of search ranking.
24
7
  *
25
- * FTS5 produces finite non-positive values in normal operation. Keeping the
26
- * defensive cases here finite makes this boundary safe if a driver or fixture
27
- * hands us an invalid value: `-Infinity` is the strongest possible match,
28
- * while NaN, +Infinity, and positive scores contribute no lexical evidence.
29
- */
30
- export function normalizeFtsScores(results) {
31
- const ftsScoreMap = new Map();
32
- for (const result of results) {
33
- ftsScoreMap.set(result.id, { score: result.lexicalScore ?? stableFtsScore(result.bm25Score), result });
34
- }
35
- return ftsScoreMap;
36
- }
37
- export function combineSearchScores(options) {
38
- const FTS_WEIGHT = 0.7;
39
- const VEC_WEIGHT = 0.3;
40
- const excludeTypeSet = options.excludeTypes && options.excludeTypes.length > 0 ? new Set(options.excludeTypes) : null;
41
- const scored = [];
42
- const seenIds = new Set();
43
- for (const [id, { score: ftsScore, result }] of options.ftsScoreMap) {
44
- seenIds.add(id);
45
- const embedScore = options.embedScoreMap.get(id);
46
- const combinedScore = embedScore !== undefined ? ftsScore * FTS_WEIGHT + embedScore * VEC_WEIGHT : ftsScore;
47
- scored.push({
48
- id,
49
- entry: result.entry,
50
- filePath: result.filePath,
51
- score: combinedScore,
52
- rankingMode: embedScore !== undefined ? "hybrid" : "fts",
53
- lexicalMatch: result.lexicalMatch,
54
- itemRef: result.itemRef,
55
- bundleId: result.bundleId,
56
- conceptId: result.conceptId,
57
- fragmentId: result.fragmentId,
58
- });
59
- }
60
- for (const [id, cosine] of options.embedScoreMap) {
61
- if (seenIds.has(id))
62
- continue;
63
- const found = options.getEntryById(id);
64
- if (!found)
65
- continue;
66
- if (options.typeFilter && found.entry.type !== options.typeFilter)
67
- continue;
68
- // #627 — drop vector-only neighbors whose type is excluded on the default path.
69
- if (excludeTypeSet?.has(found.entry.type))
70
- continue;
71
- scored.push({
72
- id,
73
- entry: found.entry,
74
- filePath: found.filePath,
75
- score: cosine * VEC_WEIGHT,
76
- rankingMode: "semantic",
77
- itemRef: found.itemRef,
78
- bundleId: found.bundleId,
79
- conceptId: found.conceptId,
80
- });
81
- }
82
- return scored;
83
- }
84
- export function applyRankingRules(options) {
85
- const queryTokens = buildLexicalQueryPlan(options.query).tokens.map((token) => token.toLowerCase());
86
- const queryLower = options.query.toLowerCase().trim();
87
- const rankingContext = {
88
- db: options.db,
89
- query: options.query,
90
- queryLower,
91
- queryTokens,
92
- graphContext: options.graphContext,
93
- projectContext: options.projectContext,
94
- };
95
- for (const item of options.items) {
96
- applyScoreContributors(item, rankingContext, defaultRankingContributors);
97
- }
98
- const { global: utilScoresMap, scoped: scopedUtilScoresMap } = getUtilityScoresByIds(options.db, options.items.map((item) => item.id), options.scopeKey);
99
- // R2 / #692 — salience-ranking is not in defaultUtilityRankingContributors
100
- // (see ranking-contributors.ts), so this is never consumed by the default
101
- // ranking path below; it exists only for a caller that explicitly builds a
102
- // contributor list including salienceRankingContributor. undefined/null
103
- // both normalize to "no data" — there is no state.db fallback load.
104
- const salienceRankScores = options.salienceRankScores ?? new Map();
105
- const utilityContext = {
106
- ...rankingContext,
107
- utilityScores: utilScoresMap,
108
- scopedUtilityScores: scopedUtilScoresMap,
109
- utilityDecayConfig: options.utilityDecayConfig,
110
- positiveFeedbackCounts: options.positiveFeedbackCounts,
111
- salienceRankScores,
112
- };
113
- for (const item of options.items) {
114
- applyUtilityContributors(item, utilityContext, defaultUtilityRankingContributors);
115
- applyRelaxedLexicalScoreCeiling(item, queryTokens);
116
- // SPEC-5: demoting belief states (superseded/contradicted/archived/
117
- // deprecated) cap the FINAL score. The additive belief penalty inside the
118
- // multiplicative boost sum can still overwhelm an additive belief penalty,
119
- // so without the ceiling a superseded incumbent can outrank its correction.
120
- applyBeliefStateScoreCeiling(item);
121
- }
122
- return options.items;
123
- }
124
- const RELAXED_NON_NAME_SCORE_CEILING = 0.65;
125
- /**
126
- * Rank name evidence without relying on punctuation or ASCII-only splitting.
127
- * The tiers are intentionally structural: an exact normalized name, all query
128
- * tokens in a longer name, any query token in the name, or no name evidence.
8
+ * Fusing ranks rather than scores needs no calibration between BM25 and cosine
9
+ * similarity, so it holds for every embedding model. On the retrieval suite,
10
+ * BM25 fused with document vectors this way scored nDCG@10 0.568 against 0.349
11
+ * for the additive boost stack it replaced
12
+ * (`akm/eval/retrieval/reports/baseline-2026-09-27.md`).
129
13
  */
130
- export function lexicalNameMatchTier(entry, queryTokens) {
131
- if (queryTokens.length === 0)
132
- return 0;
133
- const nameBase = entry.name.toLowerCase().split("/").pop() ?? entry.name.toLowerCase();
134
- const nameTokens = lexicalNameTokens(nameBase);
135
- if (nameTokens.length === queryTokens.length &&
136
- nameTokens.every((token, index) => structuralNameTokenMatch(token, queryTokens[index]))) {
137
- return 3;
138
- }
139
- const matched = queryTokens.filter((token) => nameTokens.some((nameToken) => structuralNameTokenMatch(nameToken, token))).length;
140
- if (matched === queryTokens.length)
141
- return 2;
142
- return matched > 0 ? 1 : 0;
143
- }
14
+ import { compareCodePoints } from "../../core/common.js";
15
+ /** The RRF constant: a candidate at rank r in a channel adds 1 / (RRF_K + r). */
16
+ export const RRF_K = 60;
144
17
  /**
145
- * A relaxed OR query admits intentionally weak candidates. Candidates with no
146
- * query token in their name remain visible for body-only recall, but cannot
147
- * share the same bounded displayed score as stronger name-bearing recoveries.
148
- * The raw ceiling is 0.65; the public score projection is applied later, so
149
- * callers never literally receive `0.65` just because this ceiling bound.
150
- *
151
- * Preserve the pre-ceiling relevance separately from `preCeilingScore`, which
152
- * belongs to belief-state demotion and may be written afterwards. A relaxed,
153
- * belief-demoted candidate otherwise loses both its body relevance and its
154
- * ordering signal when the second ceiling overwrites the first.
18
+ * Fuse ranked channels with equal weights. Candidates are ordered by fused
19
+ * score, then by `itemRef`, so equal scores always come back in the same order.
155
20
  */
156
- function applyRelaxedLexicalScoreCeiling(item, queryTokens) {
157
- if (item.lexicalMatch !== "relaxed" || lexicalNameMatchTier(item.entry, queryTokens) > 0)
158
- return;
159
- if (item.score > RELAXED_NON_NAME_SCORE_CEILING) {
160
- item.preRelaxedCeilingScore = item.score;
161
- item.score = RELAXED_NON_NAME_SCORE_CEILING;
162
- }
21
+ export function reciprocalRankFusion(channels) {
22
+ const fused = new Map();
23
+ channels.forEach((channel, channelIndex) => {
24
+ channel.forEach((candidate, position) => {
25
+ let entry = fused.get(candidate.id);
26
+ if (entry === undefined) {
27
+ entry = { id: candidate.id, itemRef: candidate.itemRef, score: 0, ranks: channels.map(() => undefined) };
28
+ fused.set(candidate.id, entry);
29
+ }
30
+ if (entry.ranks[channelIndex] !== undefined)
31
+ return;
32
+ entry.ranks[channelIndex] = position + 1;
33
+ entry.score += 1 / (RRF_K + position + 1);
34
+ });
35
+ });
36
+ return [...fused.values()].sort((a, b) => b.score - a.score || compareCodePoints(a.itemRef, b.itemRef));
163
37
  }
@@ -10,52 +10,32 @@ export function attachSearchHitAttribution(target, attribution) {
10
10
  };
11
11
  }
12
12
  export function copySearchHitAttribution(from, to, outputDescription) {
13
- const attribution = from[ATTRIBUTION];
14
- if (!attribution)
13
+ const memoryInference = from[ATTRIBUTION]?.memoryInference;
14
+ if (!memoryInference)
15
15
  return;
16
- const memoryInference = attribution.memoryInference;
17
- const memorySurvives = memoryInference?.exposure !== "surface" ||
16
+ const memorySurvives = memoryInference.exposure !== "surface" ||
18
17
  (memoryInference.surfaceDescription !== undefined && memoryInference.surfaceDescription === outputDescription);
19
- const applicable = {
20
- ...(attribution.lexical ? { lexical: attribution.lexical } : {}),
21
- ...(memorySurvives && memoryInference ? { memoryInference } : {}),
22
- ...(attribution.graphExtraction ? { graphExtraction: attribution.graphExtraction } : {}),
23
- };
24
- if (applicable.lexical || applicable.memoryInference || applicable.graphExtraction) {
25
- attachSearchHitAttribution(to, applicable);
26
- }
18
+ if (memorySurvives)
19
+ attachSearchHitAttribution(to, { memoryInference });
27
20
  }
28
21
  export function getSearchHitAttribution(target) {
29
22
  return target[ATTRIBUTION];
30
23
  }
31
24
  export function buildUsageEventAttribution(attribution, entryRef, projection = "full") {
32
- if (!attribution)
25
+ const memoryInference = attribution?.memoryInference;
26
+ if (!memoryInference)
33
27
  return undefined;
34
- const memoryInference = attribution.memoryInference;
35
- const childRef = memoryInference?.exposure === "direct" ? entryRef : memoryInference?.childRef;
36
- const graphExtraction = attribution.graphExtraction;
37
- const graphApplies = graphExtraction !== undefined && Number.isFinite(graphExtraction.boost) && graphExtraction.boost > 0;
38
- const surfaceFields = memoryInference?.surfaceFields ?? [];
39
- const surfaceVisible = memoryInference?.exposure !== "surface" ||
28
+ const childRef = memoryInference.exposure === "direct" ? entryRef : memoryInference.childRef;
29
+ const surfaceFields = memoryInference.surfaceFields ?? [];
30
+ const surfaceVisible = memoryInference.exposure !== "surface" ||
40
31
  (projection === "full"
41
32
  ? surfaceFields.length > 0
42
33
  : projection === "normal" || projection === "agent"
43
34
  ? surfaceFields.includes("description")
44
35
  : false);
45
- const memoryApplies = memoryInference !== undefined && childRef?.includes("//") === true && surfaceVisible;
46
- if (!memoryApplies && !graphApplies)
36
+ if (childRef?.includes("//") !== true || !surfaceVisible)
47
37
  return undefined;
48
- return {
49
- ...(memoryApplies
50
- ? {
51
- memoryInference: {
52
- exposure: memoryInference.exposure,
53
- childRef,
54
- },
55
- }
56
- : {}),
57
- ...(graphApplies ? { graphExtraction } : {}),
58
- };
38
+ return { memoryInference: { exposure: memoryInference.exposure, childRef } };
59
39
  }
60
40
  export function usageEventAttributionMetadata(attribution, entryRef, projection = "full") {
61
41
  const applicable = buildUsageEventAttribution(attribution, entryRef, projection);
@@ -4,6 +4,9 @@
4
4
  /**
5
5
  * Return per-field search text for multi-column FTS5 indexing.
6
6
  *
7
+ * Text keeps its case: FTS5's `unicode61` tokenizer folds case itself, and
8
+ * `buildSearchText` feeds the embedder, where case carries meaning.
9
+ *
7
10
  * Fields:
8
11
  * - name: entry name with hyphens/underscores replaced by spaces
9
12
  * - description: entry description
@@ -12,22 +15,15 @@
12
15
  * - content: bounded native/adapter body projection + TOC headings + parameters
13
16
  * (lowest-weight catch-all)
14
17
  */
15
- // NOTE (R5): the collapse detector's frozen canary queries are built from the
16
- // same surface this function indexes (name tokens / tags / description) and
17
- // scored via FTS against it. Changing what buildSearchFields includes shifts
18
- // the detector's recall baseline for ALL existing canary sets — coordinate
19
- // with src/commands/improve/collapse-detector.ts (buildCanaryQuery) and expect
20
- // operators to re-mint via `bun scripts/refresh-canary-set.ts --refresh` after
21
- // such a change.
22
18
  export function buildSearchFields(entry) {
23
- const name = entry.name.replace(/[-_]/g, " ").toLowerCase();
24
- const description = (entry.description ?? "").toLowerCase();
19
+ const name = entry.name.replace(/[-_]/g, " ");
20
+ const description = entry.description ?? "";
25
21
  const tagParts = [];
26
22
  if (entry.tags)
27
23
  tagParts.push(entry.tags.join(" "));
28
24
  if (entry.aliases)
29
25
  tagParts.push(entry.aliases.join(" "));
30
- const tags = tagParts.join(" ").toLowerCase();
26
+ const tags = tagParts.join(" ");
31
27
  const hintParts = [];
32
28
  if (entry.hints)
33
29
  hintParts.push(entry.hints.join(" "));
@@ -51,7 +47,7 @@ export function buildSearchFields(entry) {
51
47
  hintParts.push(entry.pageKind);
52
48
  if (entry.whenToUse)
53
49
  hintParts.push(entry.whenToUse);
54
- const hints = hintParts.join(" ").toLowerCase();
50
+ const hints = hintParts.join(" ");
55
51
  const contentParts = [];
56
52
  if (entry.toc) {
57
53
  contentParts.push(entry.toc.map((h) => h.text).join(" "));
@@ -65,13 +61,13 @@ export function buildSearchFields(entry) {
65
61
  }
66
62
  if (entry.content)
67
63
  contentParts.push(entry.content);
68
- const content = contentParts.join(" ").toLowerCase();
64
+ const content = contentParts.join(" ");
69
65
  return { name, description, tags, hints, content };
70
66
  }
71
67
  /**
72
- * Build a single concatenated search text string for an entry.
73
- * Used for the `search_text` column in the entries table.
74
- * and for generating embedding text.
68
+ * Build a single concatenated search text string for an entry: the text its
69
+ * vector is embedded from, derived from the stored document when the entry is
70
+ * embedded (`entries.embed_hash` holds its hash).
75
71
  */
76
72
  export function buildSearchText(entry) {
77
73
  const fields = buildSearchFields(entry);
@@ -2,100 +2,69 @@
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 { displayRef } from "../../core/asset/resolve-ref.js";
5
+ import { presentationFor } from "../../core/type-presentation.js";
5
6
  import { getDerivedForParent, getItemRefById } from "../../storage/repositories/index-entries-repository.js";
6
7
  import { getRenderer } from "../walk/file-context.js";
7
8
  import { attachSearchHitAttribution } from "./search-attribution.js";
8
- const rendererSearchHitEnricher = {
9
- name: "renderer-search-hit-enricher",
10
- appliesTo(ctx) {
11
- return ctx.rendererRegistry.rendererNameFor(ctx.type) !== undefined;
12
- },
13
- async enrich(hit, ctx) {
14
- const rendererName = ctx.rendererRegistry.rendererNameFor(ctx.type);
15
- if (!rendererName)
16
- return;
9
+ /**
10
+ * Let the hit's type renderer enrich it, then — for a memory, when a DB is
11
+ * open — surface its derived child ({@link surfaceDerivedMemory}).
12
+ */
13
+ export async function enrichSearchHit(hit, ctx) {
14
+ const rendererName = presentationFor(ctx.type).renderer;
15
+ if (rendererName) {
17
16
  const renderer = await getRenderer(rendererName);
18
17
  renderer?.enrichSearchHit?.(hit, ctx.stashDir);
19
- },
20
- };
18
+ }
19
+ if (ctx.type === "memory" && ctx.db)
20
+ surfaceDerivedMemory(hit, ctx.db, ctx.bundleId);
21
+ }
21
22
  /**
22
- * Phase 5A / Advantage D5 — derived-memory enricher.
23
- *
24
- * When a parent memory has a `.derived` child indexed (the LLM-distilled
25
- * lesson surface), this enricher rewrites the parent hit to surface the
26
- * derived child's description / searchHints / tags AND sets `expandTo` to
27
- * the derived child's ref so callers can fetch it via `akm show <ref>`.
23
+ * Phase 5A / Advantage D5 — when a parent memory has a `.derived` child
24
+ * indexed (the LLM-distilled lesson surface), rewrite the parent hit to
25
+ * surface the derived child's description / tags AND set `expandTo` to the
26
+ * derived child's ref so callers can fetch it via `akm show <ref>`.
28
27
  *
29
28
  * The parent ref is preserved on the hit — only the surface text is
30
29
  * swapped, so links and provenance still point at the canonical parent.
31
- *
32
- * Skipped for:
33
- * - non-memory hits
34
- * - memory hits that are themselves derived children (name ends with
35
- * `.derived`) — we never recurse parent→child→grandchild
36
- * - contexts without an open DB connection
37
30
  */
38
- export const derivedMemoryEnricher = {
39
- name: "derived-memory-enricher",
40
- appliesTo(ctx) {
41
- return ctx.type === "memory" && ctx.db !== undefined;
42
- },
43
- enrich(hit, ctx) {
44
- if (!ctx.db)
45
- return;
46
- // Never recurse: a `.derived` hit is itself the child surface; leaving
47
- // it untouched also avoids `<parent>.derived.derived` chains.
48
- if (hit.name.toLowerCase().endsWith(".derived"))
49
- return;
50
- // Parent ref shape: the 0.9.0 `memories/<name>` conceptId. Re-build from the
51
- // entry's name so we don't depend on whatever wiki/registry prefix `hit.ref`
52
- // carries. INTERNAL lookup key into `getDerivedForParent`: the `derived_from`
53
- // column now stores this same conceptId grammar (Group-C item 2 flip — the
54
- // metadata producer + this consumer move together).
55
- const parentRef = `memories/${hit.name}`;
56
- const derived = getDerivedForParent(ctx.db, parentRef, ctx.bundleId);
57
- if (!derived)
58
- return;
59
- // Swap description / searchHints / tags from the derived child.
60
- // The parent ref itself is preserved — only the surface text is swapped.
61
- const surfaceFields = [];
62
- let surfaceDescription;
63
- if (typeof derived.entry.description === "string" && derived.entry.description.length > 0) {
64
- hit.description = derived.entry.description;
65
- surfaceDescription = derived.entry.description;
66
- surfaceFields.push("description");
67
- }
68
- if (Array.isArray(derived.entry.searchHints) && derived.entry.searchHints.length > 0) {
69
- // We don't have a `searchHints` field on SourceSearchHit today — it's
70
- // only used inside ranking. The plan says to swap when present; we
71
- // record it onto the hit only if a future renderer surfaces it. For
72
- // now, treat as advisory (no-op when SearchHit lacks the field).
73
- }
74
- if (Array.isArray(derived.entry.tags) && derived.entry.tags.length > 0) {
75
- hit.tags = derived.entry.tags;
76
- surfaceFields.push("tags");
77
- }
78
- // F4b output-spelling flip: `expandTo` is a user-facing `akm show <ref>`
79
- // target, so emit the 0.9.0 short conceptId grammar (`memories/<name>`).
80
- hit.expandTo = displayRef({ type: "memory", name: derived.entry.name });
81
- const childRef = getItemRefById(ctx.db, derived.id);
82
- if (childRef && surfaceFields.length > 0) {
83
- attachSearchHitAttribution(hit, {
84
- memoryInference: {
85
- exposure: "surface",
86
- childRef,
87
- surfaceFields,
88
- ...(surfaceDescription ? { surfaceDescription } : {}),
89
- },
90
- });
91
- }
92
- },
93
- };
94
- export const defaultSearchHitEnrichers = [rendererSearchHitEnricher, derivedMemoryEnricher];
95
- export async function enrichSearchHit(hit, ctx, enrichers = defaultSearchHitEnrichers) {
96
- for (const enricher of enrichers) {
97
- if (!enricher.appliesTo(ctx))
98
- continue;
99
- await enricher.enrich(hit, ctx);
31
+ function surfaceDerivedMemory(hit, db, bundleId) {
32
+ // Never recurse: a `.derived` hit is itself the child surface; leaving
33
+ // it untouched also avoids `<parent>.derived.derived` chains.
34
+ if (hit.name.toLowerCase().endsWith(".derived"))
35
+ return;
36
+ // Parent ref shape: the 0.9.0 `memories/<name>` conceptId. Re-build from the
37
+ // entry's name so we don't depend on whatever wiki/registry prefix `hit.ref`
38
+ // carries. INTERNAL lookup key into `getDerivedForParent`: the `derived_from`
39
+ // column stores this same conceptId grammar.
40
+ const derived = getDerivedForParent(db, `memories/${hit.name}`, bundleId);
41
+ if (!derived)
42
+ return;
43
+ // Swap description / tags from the derived child (SourceSearchHit carries no
44
+ // `searchHints`).
45
+ const surfaceFields = [];
46
+ let surfaceDescription;
47
+ if (typeof derived.entry.description === "string" && derived.entry.description.length > 0) {
48
+ hit.description = derived.entry.description;
49
+ surfaceDescription = derived.entry.description;
50
+ surfaceFields.push("description");
51
+ }
52
+ if (Array.isArray(derived.entry.tags) && derived.entry.tags.length > 0) {
53
+ hit.tags = derived.entry.tags;
54
+ surfaceFields.push("tags");
55
+ }
56
+ // `expandTo` is a user-facing `akm show <ref>` target, so emit the 0.9.0
57
+ // short conceptId grammar (`memories/<name>`).
58
+ hit.expandTo = displayRef({ type: "memory", name: derived.entry.name });
59
+ const childRef = getItemRefById(db, derived.id);
60
+ if (childRef && surfaceFields.length > 0) {
61
+ attachSearchHitAttribution(hit, {
62
+ memoryInference: {
63
+ exposure: "surface",
64
+ childRef,
65
+ surfaceFields,
66
+ ...(surfaceDescription ? { surfaceDescription } : {}),
67
+ },
68
+ });
100
69
  }
101
70
  }
@@ -7,14 +7,11 @@ import { isWithin, resolveStashDir } from "../../core/common.js";
7
7
  import { bundleComponentConfig, bundleKeyForContentRoot, bundlesToSourceEntries, getSources, isBundleEnabled, loadConfig, } from "../../core/config/config.js";
8
8
  import { ConfigError } from "../../core/errors.js";
9
9
  import { getUnresolvedSourcesDir } from "../../core/paths.js";
10
+ import { warn } from "../../core/warn.js";
10
11
  import { resolveGitContentRoot, resolveWritable } from "../../core/write-source.js";
11
12
  import { lockContentRootFor } from "../../integrations/lockfile.js";
12
13
  import { resolveSourceProviderFactory } from "../../sources/provider-factory.js";
13
14
  import { ensureWebsiteMirror } from "../../sources/snapshot-fetchers/website-ingest.js";
14
- // Eager side-effect imports so all built-in source providers self-register
15
- // before resolveEntryContentDir() runs.
16
- import "../../sources/providers/index.js";
17
- import { warn } from "../../core/warn.js";
18
15
  // ── Resolution ──────────────────────────────────────────────────────────────
19
16
  /**
20
17
  * Build the ordered list of stash sources, walking every configured stash
@@ -94,17 +94,12 @@ export function countUsageEventsByType(db, eventType) {
94
94
  /**
95
95
  * Delete usage events older than the given number of days.
96
96
  */
97
- export function purgeOldUsageEvents(db, retentionDays, options) {
97
+ export function purgeOldUsageEvents(db, retentionDays) {
98
98
  if (!Number.isFinite(retentionDays) || retentionDays <= 0)
99
99
  return;
100
100
  try {
101
- const stateSchema = options?.stateSchema;
102
- if (stateSchema !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(stateSchema)) {
103
- throw new Error("Invalid attached state schema name.");
104
- }
105
- const usageEvents = stateSchema === undefined ? "usage_events" : `"${stateSchema}".usage_events`;
106
101
  const cutoff = new Date(Date.now() - retentionDays * 86_400_000).toISOString();
107
- db.prepare(`DELETE FROM ${usageEvents} WHERE created_at < ?`).run(cutoff);
102
+ db.prepare("DELETE FROM usage_events WHERE created_at < ?").run(cutoff);
108
103
  }
109
104
  catch {
110
105
  /* Table may not exist yet */