akm-cli 0.9.0-rc.0 → 0.9.0-rc.13

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 (598) hide show
  1. package/CHANGELOG.md +1283 -22
  2. package/README.md +62 -37
  3. package/SECURITY.md +46 -31
  4. package/dist/akm +162 -38
  5. package/dist/akm-migrate +44 -0
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/hints/cli-hints-full.md +268 -118
  8. package/dist/assets/hints/cli-hints-short.md +87 -24
  9. package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
  10. package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
  11. package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
  12. package/dist/assets/improve-strategies/frequent.json +15 -0
  13. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
  14. package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
  15. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
  16. package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
  17. package/dist/assets/improve-strategies/reflect-distill.json +30 -0
  18. package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
  19. package/dist/assets/prompts/consolidate-system.md +5 -5
  20. package/dist/assets/prompts/extract-session.md +2 -6
  21. package/dist/assets/prompts/memory-infer-user.md +2 -3
  22. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  23. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  24. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  25. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  26. package/dist/assets/stash-skeleton/README.md +38 -10
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
  39. package/dist/assets/tasks/core/extract.yml +3 -2
  40. package/dist/assets/tasks/core/improve.yml +2 -1
  41. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  42. package/dist/assets/tasks/core/sync.yml +1 -0
  43. package/dist/assets/tasks/core/version-check.yml +2 -1
  44. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  45. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  46. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  47. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  48. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  49. package/dist/assets/templates/html/health.html +5 -4
  50. package/dist/assets/workflows/workflow-template.md +31 -15
  51. package/dist/cli/invocation.js +279 -0
  52. package/dist/cli/parse-args.js +5 -90
  53. package/dist/cli/retired-commands.js +78 -0
  54. package/dist/cli/shared.js +158 -48
  55. package/dist/cli-node.mjs +2 -1
  56. package/dist/cli.js +747 -293
  57. package/dist/commands/agent/agent-dispatch.js +19 -18
  58. package/dist/commands/agent/agent-support.js +0 -24
  59. package/dist/commands/agent/contribute-cli.js +43 -97
  60. package/dist/commands/completions.js +80 -23
  61. package/dist/commands/config-cli.js +44 -281
  62. package/dist/commands/env/env-binding.js +99 -0
  63. package/dist/commands/env/env-cli.js +84 -224
  64. package/dist/commands/env/env.js +12 -163
  65. package/dist/commands/env/marker-path.js +6 -0
  66. package/dist/commands/env/secret-cli.js +45 -61
  67. package/dist/commands/env/secret.js +32 -62
  68. package/dist/commands/feedback-cli.js +179 -85
  69. package/dist/commands/health/accept-rate.js +58 -0
  70. package/dist/commands/health/advisories.js +7 -8
  71. package/dist/commands/health/checks.js +279 -94
  72. package/dist/commands/health/html-report.js +197 -578
  73. package/dist/commands/health/improve-metrics.js +277 -246
  74. package/dist/commands/health/llm-usage.js +19 -19
  75. package/dist/commands/health/md-report.js +16 -7
  76. package/dist/commands/health/metrics.js +67 -32
  77. package/dist/commands/health/renderers.js +47 -0
  78. package/dist/commands/health/report-view-model.js +508 -0
  79. package/dist/commands/health/stash-exposure.js +1 -1
  80. package/dist/commands/health/surfaces.js +16 -56
  81. package/dist/commands/health/task-runs.js +3 -67
  82. package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
  83. package/dist/commands/health/types-improve.js +29 -0
  84. package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
  85. package/dist/commands/health/types-result.js +7 -0
  86. package/dist/commands/health/types-runs.js +4 -0
  87. package/dist/commands/health/types-session-log.js +4 -0
  88. package/dist/commands/health/types-windows.js +4 -0
  89. package/dist/commands/health/types.js +26 -21
  90. package/dist/commands/health/windows.js +2 -3
  91. package/dist/commands/health.js +296 -167
  92. package/dist/commands/improve/anti-collapse.js +5 -5
  93. package/dist/commands/improve/autonomy-gate.js +68 -0
  94. package/dist/commands/improve/collapse-detector.js +65 -52
  95. package/dist/commands/improve/consolidate/chunking.js +9 -7
  96. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  97. package/dist/commands/improve/consolidate/merge.js +4 -0
  98. package/dist/commands/improve/consolidate.js +454 -1354
  99. package/dist/commands/improve/content-hash.js +39 -0
  100. package/dist/commands/improve/distill/content-repair.js +4 -10
  101. package/dist/commands/improve/distill/promote-memory.js +89 -64
  102. package/dist/commands/improve/distill/quality-gate.js +118 -42
  103. package/dist/commands/improve/distill-guards.js +1 -1
  104. package/dist/commands/improve/distill-promotion-policy.js +33 -888
  105. package/dist/commands/improve/distill.js +607 -363
  106. package/dist/commands/improve/eligibility.js +165 -79
  107. package/dist/commands/improve/extract-cli.js +35 -126
  108. package/dist/commands/improve/extract-prompt.js +6 -35
  109. package/dist/commands/improve/extract.js +640 -391
  110. package/dist/commands/improve/feedback-valence.js +2 -12
  111. package/dist/commands/improve/improve-cli.js +134 -135
  112. package/dist/commands/improve/improve-result-file.js +30 -50
  113. package/dist/commands/improve/improve-run-types.js +4 -0
  114. package/dist/commands/improve/improve-strategies.js +135 -0
  115. package/dist/commands/improve/improve.js +904 -701
  116. package/dist/commands/improve/locks.js +64 -111
  117. package/dist/commands/improve/loop-stages.js +1110 -923
  118. package/dist/commands/improve/memory/derived-ref.js +124 -0
  119. package/dist/commands/improve/memory/memory-belief.js +79 -7
  120. package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
  121. package/dist/commands/improve/memory/memory-improve.js +25 -37
  122. package/dist/commands/improve/outcome-loop.js +25 -88
  123. package/dist/commands/improve/preparation.js +1034 -813
  124. package/dist/commands/improve/proactive-maintenance.js +34 -9
  125. package/dist/commands/improve/proposal-envelope.js +31 -0
  126. package/dist/commands/improve/reflect.js +983 -794
  127. package/dist/commands/improve/run-context.js +119 -0
  128. package/dist/commands/improve/salience.js +24 -127
  129. package/dist/commands/improve/session-asset.js +7 -3
  130. package/dist/commands/improve/shared.js +14 -34
  131. package/dist/commands/improve/source-identity.js +28 -0
  132. package/dist/commands/improve/triage.js +20 -17
  133. package/dist/commands/lint/base-linter.js +340 -313
  134. package/dist/commands/lint/env-key-rules.js +31 -47
  135. package/dist/commands/lint/index.js +185 -30
  136. package/dist/commands/{events.js → log.js} +28 -38
  137. package/dist/commands/migrate-cli.js +54 -0
  138. package/dist/commands/migration-tool.js +55 -0
  139. package/dist/commands/observability-cli.js +70 -208
  140. package/dist/commands/proposal/diff-format.js +50 -0
  141. package/dist/commands/proposal/drain-policies.js +0 -6
  142. package/dist/commands/proposal/drain.js +91 -40
  143. package/dist/commands/proposal/proposal-cli.js +134 -132
  144. package/dist/commands/proposal/proposal-types.js +56 -0
  145. package/dist/commands/proposal/proposal.js +83 -65
  146. package/dist/commands/proposal/propose-cli.js +88 -0
  147. package/dist/commands/proposal/propose.js +105 -88
  148. package/dist/commands/proposal/repository.js +1303 -278
  149. package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
  150. package/dist/commands/proposal/validators/proposal-validators.js +61 -12
  151. package/dist/commands/proposal/validators/proposals.js +6 -8
  152. package/dist/commands/read/curate.js +78 -73
  153. package/dist/commands/read/knowledge.js +510 -13
  154. package/dist/commands/read/registry-search.js +2 -2
  155. package/dist/commands/read/remember-cli.js +84 -15
  156. package/dist/commands/read/search-cli.js +203 -96
  157. package/dist/commands/read/search.js +126 -94
  158. package/dist/commands/read/show.js +226 -250
  159. package/dist/commands/registry-cli.js +34 -60
  160. package/dist/commands/remember.js +18 -57
  161. package/dist/commands/sources/add-cli.js +104 -49
  162. package/dist/commands/sources/bundle-cli.js +166 -0
  163. package/dist/commands/sources/bundle-config-ops.js +63 -0
  164. package/dist/commands/sources/info.js +27 -15
  165. package/dist/commands/sources/init.js +30 -40
  166. package/dist/commands/sources/installed-stashes.js +469 -172
  167. package/dist/commands/sources/migration-help.js +7 -4
  168. package/dist/commands/sources/schema-repair.js +10 -9
  169. package/dist/commands/sources/self-update.js +182 -121
  170. package/dist/commands/sources/source-add.js +169 -178
  171. package/dist/commands/sources/source-clone.js +144 -41
  172. package/dist/commands/sources/source-manage.js +94 -59
  173. package/dist/commands/sources/sources-cli.js +64 -205
  174. package/dist/commands/sources/stash-cli.js +91 -54
  175. package/dist/commands/sources/stash-skeleton.js +1 -1
  176. package/dist/commands/tasks/tasks-cli.js +106 -104
  177. package/dist/commands/tasks/tasks.js +445 -262
  178. package/dist/commands/workflow-cli.js +232 -121
  179. package/dist/core/action-contributors.js +1 -1
  180. package/dist/core/activation-policy.js +49 -0
  181. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  182. package/dist/core/adapter/adapters/akm-adapter.js +528 -0
  183. package/dist/core/adapter/adapters/akm-lint.js +392 -0
  184. package/dist/core/adapter/adapters/akm-metadata.js +387 -0
  185. package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
  186. package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
  187. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  188. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  189. package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
  190. package/dist/core/adapter/adapters/index.js +80 -0
  191. package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
  192. package/dist/core/adapter/adapters/okf-adapter.js +391 -0
  193. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  194. package/dist/core/adapter/adapters/shared.js +286 -0
  195. package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
  196. package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
  197. package/dist/core/adapter/bundle-adapter.js +4 -0
  198. package/dist/core/adapter/detect-adapter.js +17 -0
  199. package/dist/core/adapter/recognize-match.js +44 -0
  200. package/dist/core/adapter/registry.js +56 -0
  201. package/dist/core/adapter/types.js +4 -0
  202. package/dist/core/asset/akm-markdown.js +30 -0
  203. package/dist/core/asset/asset-placement.js +243 -0
  204. package/dist/core/asset/asset-ref.js +110 -79
  205. package/dist/core/asset/asset-serialize.js +20 -0
  206. package/dist/core/asset/frontmatter.js +28 -12
  207. package/dist/core/asset/markdown.js +40 -51
  208. package/dist/core/asset/resolve-ref.js +274 -0
  209. package/dist/core/asset/stash-meta.js +2 -2
  210. package/dist/core/bundle-id.js +51 -0
  211. package/dist/core/common.js +281 -86
  212. package/dist/core/config/config-io.js +42 -128
  213. package/dist/core/config/config-schema.js +233 -834
  214. package/dist/core/config/config-sources.js +162 -39
  215. package/dist/core/config/config-types.js +16 -11
  216. package/dist/core/config/config-version.js +29 -0
  217. package/dist/core/config/config-walker.js +126 -37
  218. package/dist/core/config/config.js +154 -331
  219. package/dist/core/config/deep-merge.js +41 -0
  220. package/dist/core/config/engine-semantics.js +28 -0
  221. package/dist/core/config/experimental.js +21 -0
  222. package/dist/core/config/schema/embedding.js +38 -0
  223. package/dist/core/config/schema/engines.js +116 -0
  224. package/dist/core/config/schema/experimental.js +47 -0
  225. package/dist/core/config/schema/feedback.js +31 -0
  226. package/dist/core/config/schema/improve-processes.js +389 -0
  227. package/dist/core/config/schema/improve.js +94 -0
  228. package/dist/core/config/schema/index-config.js +176 -0
  229. package/dist/core/config/schema/output.js +18 -0
  230. package/dist/core/config/schema/primitives.js +94 -0
  231. package/dist/core/config/schema/search.js +30 -0
  232. package/dist/core/config/schema/setup.js +18 -0
  233. package/dist/core/config/schema/sources-bundles.js +169 -0
  234. package/dist/core/config/schema/workflow.js +29 -0
  235. package/dist/core/env-secret-ref.js +155 -20
  236. package/dist/core/errors.js +17 -15
  237. package/dist/core/events-types.js +4 -0
  238. package/dist/core/events.js +46 -128
  239. package/dist/core/extra-params.js +62 -0
  240. package/dist/core/file-change.js +17 -0
  241. package/dist/core/file-lock.js +202 -57
  242. package/dist/core/fs-txn.js +392 -0
  243. package/dist/core/git-message.js +59 -0
  244. package/dist/core/improve-result.js +167 -0
  245. package/dist/core/json-schema.js +142 -0
  246. package/dist/core/lesson-lint.js +1 -17
  247. package/dist/core/logs-db.js +1 -1
  248. package/dist/core/maintenance-barrier.js +135 -0
  249. package/dist/core/migration-operation.js +44 -0
  250. package/dist/core/mutation-target.js +78 -0
  251. package/dist/core/paths.js +22 -25
  252. package/dist/core/platform.js +10 -0
  253. package/dist/core/recognition-util.js +128 -0
  254. package/dist/core/redaction.js +392 -0
  255. package/dist/core/standards/resolve-standards-context.js +36 -65
  256. package/dist/core/standards/resolve-stash-standards.js +2 -2
  257. package/dist/core/standards/resolve-type-conventions.js +5 -5
  258. package/dist/core/state/migrations.js +242 -11
  259. package/dist/core/state-db.js +98 -10
  260. package/dist/core/structured.js +1 -1
  261. package/dist/core/subprocess.js +303 -0
  262. package/dist/core/text-truncation.js +9 -5
  263. package/dist/core/time.js +20 -0
  264. package/dist/core/type-presentation.js +130 -0
  265. package/dist/core/warn.js +0 -3
  266. package/dist/core/write-source.js +834 -118
  267. package/dist/indexer/bundle-identity-guard.js +92 -0
  268. package/dist/indexer/db/graph-db.js +1 -25
  269. package/dist/indexer/db/llm-cache.js +1 -1
  270. package/dist/indexer/ensure-index.js +30 -9
  271. package/dist/indexer/graph/graph-boost.js +9 -30
  272. package/dist/indexer/graph/graph-extraction.js +41 -27
  273. package/dist/indexer/graph/graph-types.js +4 -0
  274. package/dist/indexer/index-writer-lock.js +93 -49
  275. package/dist/indexer/index-written-assets.js +100 -53
  276. package/dist/indexer/indexer.js +746 -329
  277. package/dist/indexer/init.js +18 -25
  278. package/dist/indexer/installations.js +142 -0
  279. package/dist/indexer/passes/dir-staleness.js +18 -10
  280. package/dist/indexer/passes/memory-inference.js +25 -15
  281. package/dist/indexer/passes/metadata.js +412 -243
  282. package/dist/indexer/scan/doc-to-entry.js +160 -0
  283. package/dist/indexer/scan/drain-dir.js +134 -0
  284. package/dist/indexer/search/db-search.js +292 -108
  285. package/dist/indexer/search/fts-query.js +64 -0
  286. package/dist/indexer/search/ranking-contributors.js +145 -25
  287. package/dist/indexer/search/ranking-types.js +4 -0
  288. package/dist/indexer/search/ranking.js +28 -71
  289. package/dist/indexer/search/search-attribution.js +67 -0
  290. package/dist/indexer/search/search-fields.js +18 -3
  291. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  292. package/dist/indexer/search/search-source.js +157 -111
  293. package/dist/indexer/search/semantic-status.js +4 -1
  294. package/dist/indexer/usage/usage-events.js +10 -30
  295. package/dist/indexer/walk/file-context.js +3 -45
  296. package/dist/indexer/walk/matchers.js +42 -34
  297. package/dist/indexer/walk/path-resolver.js +11 -5
  298. package/dist/indexer/walk/walker.js +42 -14
  299. package/dist/integrations/agent/builder-shared.js +7 -0
  300. package/dist/integrations/agent/builders.js +5 -56
  301. package/dist/integrations/agent/config.js +3 -143
  302. package/dist/integrations/agent/detect.js +17 -2
  303. package/dist/integrations/agent/engine-resolution.js +231 -0
  304. package/dist/integrations/agent/index.js +1 -2
  305. package/dist/integrations/agent/model-aliases.js +16 -2
  306. package/dist/integrations/agent/profiles.js +36 -62
  307. package/dist/integrations/agent/prompts.js +46 -18
  308. package/dist/integrations/agent/runner-dispatch.js +93 -4
  309. package/dist/integrations/agent/runner.js +76 -208
  310. package/dist/integrations/agent/spawn.js +88 -196
  311. package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
  312. package/dist/integrations/harnesses/aider/index.js +48 -0
  313. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  314. package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
  315. package/dist/integrations/harnesses/amazonq/index.js +45 -0
  316. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  317. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  318. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  319. package/dist/integrations/harnesses/claude/index.js +24 -35
  320. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  321. package/dist/integrations/harnesses/claude/session-log.js +27 -75
  322. package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
  323. package/dist/integrations/harnesses/codex/index.js +52 -0
  324. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  325. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  326. package/dist/integrations/harnesses/copilot/index.js +48 -0
  327. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  328. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  329. package/dist/integrations/harnesses/gemini/index.js +48 -0
  330. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  331. package/dist/integrations/harnesses/ids.js +24 -0
  332. package/dist/integrations/harnesses/index.js +54 -34
  333. package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
  334. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  335. package/dist/integrations/harnesses/opencode/index.js +14 -32
  336. package/dist/integrations/harnesses/opencode/session-log.js +67 -125
  337. package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
  338. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  339. package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
  340. package/dist/integrations/harnesses/openhands/index.js +48 -0
  341. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  342. package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
  343. package/dist/integrations/harnesses/pi/index.js +45 -0
  344. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  345. package/dist/integrations/harnesses/shared.js +17 -0
  346. package/dist/integrations/harnesses/types.js +43 -32
  347. package/dist/integrations/lockfile.js +211 -24
  348. package/dist/integrations/session-logs/index.js +36 -39
  349. package/dist/integrations/session-logs/provider-base.js +113 -0
  350. package/dist/llm/client.js +182 -110
  351. package/dist/llm/embedders/deterministic.js +2 -2
  352. package/dist/llm/embedders/remote.js +21 -9
  353. package/dist/llm/feature-gate.js +17 -57
  354. package/dist/llm/graph-extract.js +12 -13
  355. package/dist/llm/index-passes.js +8 -42
  356. package/dist/llm/memory-infer.js +144 -1
  357. package/dist/llm/metadata-enhance.js +45 -30
  358. package/dist/llm/structured-call.js +16 -8
  359. package/dist/llm/usage-persist.js +30 -5
  360. package/dist/llm/usage-telemetry.js +59 -6
  361. package/dist/output/cli-hints.js +1 -2
  362. package/dist/output/command-registry.js +27 -0
  363. package/dist/output/context.js +22 -7
  364. package/dist/output/format-exempt.js +80 -0
  365. package/dist/output/generic-render.js +251 -0
  366. package/dist/output/html-render.js +11 -16
  367. package/dist/output/render-registry.js +57 -0
  368. package/dist/output/renderers.js +14 -279
  369. package/dist/output/shapes/curate.js +10 -1
  370. package/dist/output/shapes/events.js +12 -7
  371. package/dist/output/shapes/helpers.js +58 -84
  372. package/dist/output/shapes/passthrough.js +11 -39
  373. package/dist/output/shapes/proposal/producer.js +15 -7
  374. package/dist/output/shapes/registry.js +12 -6
  375. package/dist/output/shapes.js +0 -9
  376. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  377. package/dist/output/text/bundle-show.js +7 -0
  378. package/dist/output/text/command-format.js +562 -0
  379. package/dist/output/text/env.js +1 -3
  380. package/dist/output/text/events.js +8 -7
  381. package/dist/output/text/helpers.js +15 -1164
  382. package/dist/output/text/proposal/producer.js +4 -2
  383. package/dist/output/text/proposal-format.js +202 -0
  384. package/dist/output/text/registry-commands.js +1 -2
  385. package/dist/output/text/registry.js +12 -6
  386. package/dist/output/text/show-directives.js +117 -0
  387. package/dist/output/text/show-format.js +103 -0
  388. package/dist/output/text/sync.js +5 -0
  389. package/dist/output/text/workflow-format.js +332 -0
  390. package/dist/output/text/workflow.js +3 -2
  391. package/dist/output/text.js +10 -19
  392. package/dist/registry/factory.js +4 -6
  393. package/dist/registry/origin-resolve.js +16 -27
  394. package/dist/registry/providers/skills-sh.js +3 -3
  395. package/dist/registry/providers/static-index.js +15 -25
  396. package/dist/registry/resolve.js +43 -94
  397. package/dist/registry/semver.js +43 -0
  398. package/dist/runtime.js +81 -12
  399. package/dist/scripts/akm-migrate.js +35529 -0
  400. package/dist/setup/detect.js +5 -7
  401. package/dist/setup/detected-engines.js +136 -0
  402. package/dist/setup/engine-config.js +100 -0
  403. package/dist/setup/registry-stash-loader.js +3 -3
  404. package/dist/setup/semantic-assets.js +12 -9
  405. package/dist/setup/setup.js +444 -208
  406. package/dist/setup/steps/connection-shared.js +120 -0
  407. package/dist/setup/steps/connection.js +108 -305
  408. package/dist/setup/steps/platforms.js +13 -12
  409. package/dist/setup/steps/semantic.js +15 -3
  410. package/dist/setup/steps/sources.js +21 -15
  411. package/dist/setup/steps/stashdir.js +6 -4
  412. package/dist/setup/steps/tasks.js +236 -119
  413. package/dist/setup/steps.js +3 -2
  414. package/dist/sources/freshness.js +39 -0
  415. package/dist/sources/provider-factory.js +11 -17
  416. package/dist/sources/providers/filesystem.js +2 -3
  417. package/dist/sources/providers/git-install.js +278 -34
  418. package/dist/sources/providers/git-provider.js +54 -56
  419. package/dist/sources/providers/git-stash.js +420 -91
  420. package/dist/sources/providers/git.js +2 -2
  421. package/dist/sources/providers/npm.js +16 -19
  422. package/dist/sources/providers/provider-utils.js +47 -22
  423. package/dist/sources/providers/sync-from-ref.js +3 -9
  424. package/dist/sources/providers/website.js +2 -2
  425. package/dist/sources/resolve.js +11 -10
  426. package/dist/sources/snapshot-fetchers/types.js +4 -0
  427. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
  428. package/dist/storage/database.js +60 -4
  429. package/dist/storage/engines/sqlite-migrations.js +156 -5
  430. package/dist/storage/locations.js +1 -2
  431. package/dist/storage/repositories/canaries-repository.js +1 -1
  432. package/dist/storage/repositories/events-repository.js +51 -11
  433. package/dist/storage/repositories/improve-runs-repository.js +6 -32
  434. package/dist/storage/repositories/index-connection.js +79 -0
  435. package/dist/storage/repositories/index-db.js +4 -3
  436. package/dist/storage/repositories/index-entries-repository.js +863 -0
  437. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
  438. package/dist/storage/repositories/index-entry-types.js +4 -0
  439. package/dist/storage/repositories/index-fts-repository.js +167 -0
  440. package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
  441. package/dist/storage/repositories/index-meta-repository.js +49 -0
  442. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
  443. package/dist/storage/repositories/index-sql.js +12 -0
  444. package/dist/storage/repositories/index-utility-repository.js +356 -0
  445. package/dist/storage/repositories/index-vec-repository.js +250 -0
  446. package/dist/storage/repositories/outcome-repository.js +119 -0
  447. package/dist/storage/repositories/proposals-repository.js +317 -75
  448. package/dist/storage/repositories/registry-cache.js +1 -1
  449. package/dist/storage/repositories/salience-repository.js +172 -0
  450. package/dist/storage/repositories/task-history-repository.js +110 -3
  451. package/dist/storage/repositories/workflow-runs-repository.js +240 -19
  452. package/dist/tasks/backends/cron.js +169 -46
  453. package/dist/tasks/backends/exec-utils.js +76 -3
  454. package/dist/tasks/backends/index.js +6 -9
  455. package/dist/tasks/backends/launchd.js +292 -55
  456. package/dist/tasks/backends/schtasks.js +557 -70
  457. package/dist/tasks/backends/types.js +4 -0
  458. package/dist/tasks/command-executable.js +93 -0
  459. package/dist/tasks/embedded.js +56 -38
  460. package/dist/tasks/parser.js +156 -64
  461. package/dist/tasks/resolve-akm-bin.js +144 -51
  462. package/dist/tasks/runner.js +377 -209
  463. package/dist/tasks/schedule.js +108 -19
  464. package/dist/tasks/scheduler-invocation.js +296 -0
  465. package/dist/tasks/schema.js +1 -1
  466. package/dist/tasks/task-id.js +35 -0
  467. package/dist/tasks/validator.js +30 -16
  468. package/dist/text-import-hook.mjs +1 -1
  469. package/dist/workflows/authoring/authoring.js +104 -43
  470. package/dist/workflows/authoring/scope-key.js +1 -1
  471. package/dist/workflows/cli.js +0 -16
  472. package/dist/workflows/concurrency-policy.js +15 -0
  473. package/dist/workflows/exec/brief.js +450 -0
  474. package/dist/workflows/exec/frozen-judge.js +47 -0
  475. package/dist/workflows/exec/native-executor.js +1038 -0
  476. package/dist/workflows/exec/param-secrets.js +115 -0
  477. package/dist/workflows/exec/report.js +1460 -0
  478. package/dist/workflows/exec/run-workflow.js +602 -0
  479. package/dist/workflows/exec/scheduler.js +71 -0
  480. package/dist/workflows/exec/step-work.js +1190 -0
  481. package/dist/workflows/exec/unit-writer.js +23 -0
  482. package/dist/workflows/exec/workflow-engine-gate.js +67 -0
  483. package/dist/workflows/exec/worktree.js +171 -0
  484. package/dist/workflows/ir/compile.js +246 -0
  485. package/dist/workflows/ir/freeze.js +233 -0
  486. package/dist/workflows/ir/params.js +54 -0
  487. package/dist/workflows/ir/plan-hash.js +68 -0
  488. package/dist/workflows/ir/schema.js +540 -0
  489. package/dist/workflows/parser.js +878 -304
  490. package/dist/workflows/program/expressions.js +181 -0
  491. package/dist/workflows/program/schema.js +51 -0
  492. package/dist/workflows/renderer.js +100 -45
  493. package/dist/workflows/resource-limits.js +22 -0
  494. package/dist/workflows/runtime/agent-identity.js +59 -14
  495. package/dist/workflows/runtime/checkin.js +1 -1
  496. package/dist/workflows/runtime/plan-classifier.js +131 -0
  497. package/dist/workflows/runtime/runs.js +376 -119
  498. package/dist/workflows/runtime/unit-checkin.js +45 -0
  499. package/dist/workflows/runtime/unit-phases.js +20 -0
  500. package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
  501. package/dist/workflows/schema.js +1 -11
  502. package/dist/workflows/validate-summary.js +2 -3
  503. package/dist/workflows/validator.js +52 -30
  504. package/docs/README.md +42 -78
  505. package/docs/migration/README.md +8 -0
  506. package/docs/migration/release-notes/0.6.0.md +1 -1
  507. package/docs/migration/release-notes/0.7.0.md +9 -8
  508. package/docs/migration/release-notes/0.9.0.md +158 -14
  509. package/docs/migration/v0.7-to-v0.8.md +46 -47
  510. package/docs/migration/v0.8-to-v0.9.md +844 -0
  511. package/docs/reference/README.md +12 -0
  512. package/docs/reference/data-and-telemetry.md +333 -0
  513. package/package.json +21 -17
  514. package/schemas/akm-asset-envelope.json +93 -0
  515. package/schemas/akm-config.json +4636 -0
  516. package/schemas/akm-task.json +87 -0
  517. package/schemas/akm-workflow.json +373 -0
  518. package/dist/akm-migrate-storage +0 -38
  519. package/dist/assets/help/help-accept.md +0 -12
  520. package/dist/assets/help/help-improve.md +0 -84
  521. package/dist/assets/help/help-proposals.md +0 -17
  522. package/dist/assets/help/help-propose.md +0 -17
  523. package/dist/assets/help/help-reject.md +0 -11
  524. package/dist/assets/profiles/frequent.json +0 -13
  525. package/dist/assets/profiles/recombine-only.json +0 -21
  526. package/dist/assets/profiles/reflect-distill.json +0 -30
  527. package/dist/assets/profiles/synthesize.json +0 -15
  528. package/dist/assets/prompts/procedural-system.md +0 -44
  529. package/dist/assets/prompts/recombine-system.md +0 -40
  530. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  531. package/dist/assets/tasks/core/backup.yml +0 -4
  532. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  533. package/dist/assets/templates/html/default.html +0 -78
  534. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  535. package/dist/assets/wiki/index-template.md +0 -12
  536. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  537. package/dist/assets/wiki/log-template.md +0 -8
  538. package/dist/assets/wiki/schema-template.md +0 -61
  539. package/dist/cli/config-migrate.js +0 -150
  540. package/dist/cli/config-validate.js +0 -39
  541. package/dist/commands/graph/graph-cli.js +0 -124
  542. package/dist/commands/graph/graph.js +0 -487
  543. package/dist/commands/improve/calibration.js +0 -161
  544. package/dist/commands/improve/dedup.js +0 -482
  545. package/dist/commands/improve/extract-watch.js +0 -140
  546. package/dist/commands/improve/hot-probation.js +0 -45
  547. package/dist/commands/improve/improve-auto-accept.js +0 -276
  548. package/dist/commands/improve/improve-profiles.js +0 -168
  549. package/dist/commands/improve/procedural.js +0 -398
  550. package/dist/commands/improve/recombine.js +0 -818
  551. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  552. package/dist/commands/lint/agent-linter.js +0 -44
  553. package/dist/commands/lint/command-linter.js +0 -44
  554. package/dist/commands/lint/default-linter.js +0 -16
  555. package/dist/commands/lint/fact-linter.js +0 -39
  556. package/dist/commands/lint/knowledge-linter.js +0 -16
  557. package/dist/commands/lint/memory-linter.js +0 -61
  558. package/dist/commands/lint/registry.js +0 -41
  559. package/dist/commands/lint/skill-linter.js +0 -45
  560. package/dist/commands/lint/task-linter.js +0 -50
  561. package/dist/commands/lint/workflow-linter.js +0 -81
  562. package/dist/commands/proposal/legacy-import.js +0 -115
  563. package/dist/commands/sources/history.js +0 -196
  564. package/dist/commands/tasks/default-tasks.js +0 -186
  565. package/dist/commands/wiki-cli.js +0 -292
  566. package/dist/core/asset/asset-registry.js +0 -76
  567. package/dist/core/asset/asset-spec.js +0 -259
  568. package/dist/core/config/config-migration.js +0 -602
  569. package/dist/core/deep-merge.js +0 -38
  570. package/dist/core/eval/rank-metrics.js +0 -113
  571. package/dist/core/ripgrep/install.js +0 -163
  572. package/dist/core/ripgrep/resolve.js +0 -81
  573. package/dist/indexer/db/db.js +0 -1413
  574. package/dist/indexer/manifest.js +0 -170
  575. package/dist/indexer/passes/metadata-contributors.js +0 -31
  576. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
  577. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -49
  578. package/dist/llm/call-ai.js +0 -62
  579. package/dist/llm/memory-infer-impl.js +0 -138
  580. package/dist/output/shapes/distill.js +0 -14
  581. package/dist/output/shapes/history.js +0 -11
  582. package/dist/output/text/distill.js +0 -6
  583. package/dist/output/text/enable-disable.js +0 -8
  584. package/dist/output/text/history.js +0 -6
  585. package/dist/output/text/wiki.js +0 -16
  586. package/dist/registry/build-index.js +0 -386
  587. package/dist/scripts/migrate-storage.js +0 -19108
  588. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
  589. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  590. package/dist/setup/legacy-config.js +0 -106
  591. package/dist/storage/repositories/consolidation-repository.js +0 -38
  592. package/dist/storage/repositories/recombine-repository.js +0 -213
  593. package/dist/wiki/wiki-templates.js +0 -15
  594. package/dist/wiki/wiki.js +0 -1012
  595. package/dist/workflows/db.js +0 -215
  596. package/docs/data-and-telemetry.md +0 -226
  597. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  598. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -14,18 +14,26 @@
14
14
  * implementation, not a "local vs. remote" distinction.
15
15
  */
16
16
  import fs from "node:fs";
17
+ import path from "node:path";
17
18
  import { buildActionFromContributors, defaultActionContributors } from "../../core/action-contributors.js";
18
- import { makeAssetRef } from "../../core/asset/asset-ref.js";
19
- import { defaultRendererRegistry } from "../../core/asset/asset-registry.js";
19
+ import { stashDirFor } from "../../core/asset/asset-placement.js";
20
+ import { displayRef } from "../../core/asset/resolve-ref.js";
20
21
  import { getDbPath } from "../../core/paths.js";
22
+ import { defaultRendererRegistry } from "../../core/type-presentation.js";
21
23
  import { warn } from "../../core/warn.js";
24
+ import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
25
+ import { getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntryById, getEntryCount, getPositiveFeedbackCountsByIds, } from "../../storage/repositories/index-entries-repository.js";
26
+ import { searchFts } from "../../storage/repositories/index-fts-repository.js";
27
+ import { getMeta } from "../../storage/repositories/index-meta-repository.js";
28
+ import { searchVec } from "../../storage/repositories/index-vec-repository.js";
22
29
  import { getCurrentWorkflowScopeKey } from "../../workflows/authoring/scope-key.js";
23
- import { closeDatabase, getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntryById, getEntryCount, getMeta, getPositiveFeedbackCountsByIds, openExistingDatabase, sanitizeFtsQuery, searchFts, searchVec, } from "../db/db.js";
24
30
  import { ensureIndex } from "../ensure-index.js";
25
- import { collectGraphRelatedHit, computeGraphBoost, loadGraphBoostContext, } from "../graph/graph-boost.js";
31
+ import { collectGraphRelatedHit, loadGraphBoostContext } from "../graph/graph-boost.js";
26
32
  import { isProposedQuality } from "../passes/metadata.js";
27
33
  import { resolveProjectContext } from "../walk/project-context.js";
34
+ import { parseRefPrefixQuery, parseRetiredTypePrefixQuery, sanitizeFtsQuery } from "./fts-query.js";
28
35
  import { applyRankingRules, combineSearchScores, normalizeFtsScores } from "./ranking.js";
36
+ import { attachSearchHitAttribution, copySearchHitAttribution, getSearchHitAttribution } from "./search-attribution.js";
29
37
  import { enrichSearchHit } from "./search-hit-enrichers.js";
30
38
  import { buildEditHint, findSourceForPath, isEditable } from "./search-source.js";
31
39
  import { deriveSemanticProviderFingerprint, getEffectiveSemanticStatus, isSemanticRuntimeReady, readSemanticStatus, } from "./semantic-status.js";
@@ -37,6 +45,9 @@ import { deriveSemanticProviderFingerprint, getEffectiveSemanticStatus, isSemant
37
45
  * actionable without re-introducing read-triggered reindexing.
38
46
  */
39
47
  const STALE_INDEX_HINT_MS = 7 * 24 * 60 * 60 * 1000;
48
+ function hasIndexedProvenance(entry) {
49
+ return Boolean(entry.itemRef && entry.bundleId && entry.conceptId);
50
+ }
40
51
  function buildStaleIndexHint(db) {
41
52
  try {
42
53
  const builtAt = getMeta(db, "builtAt");
@@ -52,17 +63,22 @@ function buildStaleIndexHint(db) {
52
63
  return undefined;
53
64
  }
54
65
  }
66
+ function indexedProvenance(entry) {
67
+ return { itemRef: entry.itemRef, bundleId: entry.bundleId, conceptId: entry.conceptId };
68
+ }
55
69
  export function buildLocalAction(type, ref, registry = defaultRendererRegistry) {
56
70
  return buildActionFromContributors({ type, ref }, defaultActionContributors(registry)) ?? `akm show ${ref}`;
57
71
  }
58
- function resolveSearchHitRef(entry, refName, source) {
59
- if (source?.wikiName) {
60
- return makeAssetRef(entry.type, entry.name);
61
- }
62
- return makeAssetRef(entry.type, refName, source?.registryId);
72
+ function resolveSearchHitRef(entry, provenance, defaultBundleId) {
73
+ return displayRef({
74
+ type: entry.type,
75
+ name: entry.name,
76
+ conceptId: provenance.conceptId,
77
+ bundleId: provenance.bundleId,
78
+ }, defaultBundleId);
63
79
  }
64
80
  function resolveSearchHitOrigin(source) {
65
- return source?.wikiName ? null : (source?.registryId ?? null);
81
+ return source?.registryId ?? null;
66
82
  }
67
83
  /**
68
84
  * Phase 2A / Rec 5: gate for the per-search `getPositiveFeedbackCountsByIds`
@@ -116,7 +132,26 @@ export async function searchLocal(input) {
116
132
  }
117
133
  }
118
134
  if (config.semanticSearchMode === "auto" && semanticStatus === "blocked") {
119
- warnings.push("Semantic search is currently blocked. Using keyword search until the semantic backend is healthy again.");
135
+ if (!config.embedding?.endpoint || !config.embedding?.model) {
136
+ // F7/A2: same predicate as the `pending` branch above (#480) — a
137
+ // `blocked` status can outlive the provider config that produced it
138
+ // (e.g. the embedding config was later unset). This is not a fault;
139
+ // there is simply nothing configured to use.
140
+ warnings.push("Semantic search is enabled (semanticSearchMode='auto') but no embedding provider is configured. " +
141
+ 'Either: (a) `akm config set embedding \'{"endpoint":"...","model":"..."}\'`, or ' +
142
+ "(b) `akm config set semanticSearchMode off` to use keyword-only search.");
143
+ }
144
+ else {
145
+ // F7/A2: surface the ACTUAL diagnostic instead of one fixed generic
146
+ // string. `rawStatus.reason`/`rawStatus.message` record why the
147
+ // semantic backend failed (auth, network, a stuck local-model
148
+ // download, …) — read at `readSemanticStatus()` above and, before
149
+ // this fix, discarded here in favor of a message that never varied.
150
+ const detail = rawStatus?.message ?? (rawStatus?.reason ? `reason: ${rawStatus.reason}` : undefined);
151
+ warnings.push(`Semantic search is blocked${detail ? ` (${detail})` : ""}. Using keyword search until the semantic ` +
152
+ "backend is healthy again. Run 'akm index --full' to retry, or " +
153
+ "`akm config set semanticSearchMode off` to silence this warning.");
154
+ }
120
155
  }
121
156
  // Bootstrap-only: builds the index inline when it cannot serve this stash.
122
157
  // Content freshness is the writers' job (indexWrittenAssets + full runs);
@@ -145,16 +180,16 @@ export async function searchLocal(input) {
145
180
  const staleHint = buildStaleIndexHint(db);
146
181
  if (staleHint)
147
182
  warnings.push(staleHint);
148
- const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources, includeExcludedTypes, disableProjectContext, disableScopedUtility);
183
+ const { hits, embedMs, rankMs, usedSemantic } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources, includeExcludedTypes, disableProjectContext, disableScopedUtility);
149
184
  return {
150
185
  hits,
151
- tip: hits.length === 0
152
- ? "No matching stash assets were found. Try a different query or run 'akm index' to rebuild."
153
- : undefined,
186
+ tip: hits.length === 0 ? emptyResultTip(query) : undefined,
154
187
  warnings: warnings.length > 0 ? warnings : undefined,
155
188
  embedMs,
156
189
  rankMs,
157
- mode: embedMs !== undefined && embedMs > 0 ? "semantic" : "keyword",
190
+ // Report the mode the search ACTUALLY used, carried explicitly from the
191
+ // vector scorer — not inferred from elapsed embedding milliseconds.
192
+ mode: usedSemantic ? "semantic" : "keyword",
158
193
  };
159
194
  }
160
195
  finally {
@@ -169,58 +204,56 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
169
204
  // `includeExcludedTypes`. When the config key is ABSENT a built-in default of
170
205
  // ['session'] is applied; an explicit empty list disables exclusion.
171
206
  const defaultExcludes = searchType === "any" && !includeExcludedTypes ? (config.search?.defaultExcludeTypes ?? ["session"]) : [];
207
+ // D4 — conceptId-prefix queries (`memories/projecta/`, `bundle//`,
208
+ // `bundle//skills/`) translate to a deterministic enumeration narrowed by
209
+ // conceptId, instead of degenerating into the AND-token FTS query their
210
+ // sanitized form would produce ("memories projecta" — noise). The branch
211
+ // fires only on the untyped path: an explicit `--type` flag expresses
212
+ // stronger intent and wins. The PREFIX is itself explicit intent, so
213
+ // `defaultExcludeTypes` does not apply — `sessions/` enumerates sessions
214
+ // exactly like `--type session` does, and `bundle//` means the whole bundle.
215
+ const refPrefix = searchType === "any" ? parseRefPrefixQuery(query) : null;
216
+ // Shared args for the two browse paths below; browse never runs semantic
217
+ // ranking, so both return usedSemantic: false.
218
+ const browseArgs = {
219
+ db,
220
+ query,
221
+ limit,
222
+ stashDir,
223
+ allSourceDirs,
224
+ sources,
225
+ config,
226
+ rendererRegistry,
227
+ filters,
228
+ includeProposed,
229
+ beliefFilter,
230
+ restrictToSources,
231
+ };
232
+ if (refPrefix) {
233
+ // Browse path (conceptId-prefix enumeration).
234
+ return {
235
+ ...(await enumerateEntries({
236
+ ...browseArgs,
237
+ excludeTypes: [],
238
+ conceptIdPrefix: refPrefix.conceptIdPrefix,
239
+ ...(refPrefix.bundle !== undefined ? { bundle: refPrefix.bundle } : {}),
240
+ })),
241
+ usedSemantic: false,
242
+ };
243
+ }
172
244
  // Empty queries — including ones that sanitize down to no searchable FTS
173
245
  // tokens such as "." — should enumerate matching entries instead of
174
246
  // returning an empty result set from FTS.
175
247
  if (!hasSearchableTokens) {
176
- const typeFilter = searchType === "any" ? undefined : searchType;
177
- const allEntries = getAllEntries(db, typeFilter, defaultExcludes);
178
- // Deduplicate by file path — multiple entries can share the same file
179
- const seenFilePaths = new Set();
180
- const uniqueEntries = allEntries.filter((ie) => {
181
- if (seenFilePaths.has(ie.filePath))
182
- return false;
183
- seenFilePaths.add(ie.filePath);
184
- return true;
185
- });
186
- // Source filter: when the caller narrowed `sources` via `--source <name>`,
187
- // drop entries whose filePath does not live under any of the requested
188
- // sources. The FTS index spans every configured source, so without this
189
- // filter a narrowed --source request would still leak results.
190
- const sourceFiltered = restrictToSources
191
- ? uniqueEntries.filter((ie) => findSourceForPath(ie.filePath, sources) !== undefined)
192
- : uniqueEntries;
193
- // Scope filter: drop entries whose stored scope does not satisfy every
194
- // supplied scope key. Filtering happens BEFORE the limit slice so a
195
- // restrictive filter still returns up to `limit` results.
196
- const scopeFiltered = filters
197
- ? sourceFiltered.filter((ie) => entryMatchesScope(ie.entry.scope, filters))
198
- : sourceFiltered;
199
- // Proposed-quality filter (v1 spec §4.2): exclude entries with
200
- // `quality: "proposed"` unless the caller explicitly opts in.
201
- const qualityFiltered = includeProposed
202
- ? scopeFiltered
203
- : scopeFiltered.filter((ie) => !isProposedQuality(ie.entry.quality));
204
- // 03-R3: derived twins inherit their base's demoting belief state here too,
205
- // so the belief FILTER (and the reported hit state) stays consistent on the
206
- // enumerate/browse path — not only on the FTS-scored path below.
207
- inheritDerivedTwinBeliefStates(db, qualityFiltered);
208
- const beliefFiltered = qualityFiltered.filter((ie) => matchBeliefFilter(ie.entry.beliefState, beliefFilter));
209
- const selected = beliefFiltered.slice(0, limit);
210
- const hits = await Promise.all(selected.map((ie) => buildDbHit({
211
- entry: ie.entry,
212
- path: ie.filePath,
213
- score: 1,
214
- query,
215
- rankingMode: "fts",
216
- defaultStashDir: stashDir,
217
- allSourceDirs,
218
- sources,
219
- config,
220
- rendererRegistry,
221
- db,
222
- })));
223
- return { hits };
248
+ // Browse path (empty/unsearchable query).
249
+ return {
250
+ ...(await enumerateEntries({
251
+ ...browseArgs,
252
+ typeFilter: searchType === "any" ? undefined : searchType,
253
+ excludeTypes: defaultExcludes,
254
+ })),
255
+ usedSemantic: false,
256
+ };
224
257
  }
225
258
  // Start the async embedding request without awaiting, then run FTS
226
259
  // synchronously while the HTTP/local embedding request is in-flight.
@@ -230,6 +263,12 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
230
263
  const ftsResults = searchFts(db, query, limit * 3, typeFilter, defaultExcludes);
231
264
  const embeddingScores = await embeddingPromise;
232
265
  const embedMs = Date.now() - tEmbed0;
266
+ // The vector scorer returns a (possibly empty) Map when the embedding + vector
267
+ // search actually executed, or null when semantic was not runnable (disabled,
268
+ // no embeddings, or the embed call threw). This is the AUTHORITATIVE "semantic
269
+ // mode was used" signal — carried out to telemetry instead of guessing from
270
+ // elapsed milliseconds (which timed the concurrent FTS work too).
271
+ const usedSemantic = embeddingScores !== null;
233
272
  const tRank0 = Date.now();
234
273
  // ── Score normalization ──────────────────────────────────────────────
235
274
  // Normalized BM25 + cosine similarity with weighted addition
@@ -253,11 +292,10 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
253
292
  // not leak into default ('any') results. defaultExcludes is already []
254
293
  // unless this is the untyped path without includeExcludedTypes.
255
294
  excludeTypes: defaultExcludes,
256
- });
295
+ }).filter(hasIndexedProvenance);
257
296
  // ── Scoring Phase ──────────────────────────────────────────────────────
258
297
  // Apply boosts as multiplicative factors (all boosts in a single phase
259
298
  // so that sort order and displayed scores are always consistent).
260
- //
261
299
  // Ranking philosophy: the goal is to surface the MOST USEFUL result for the
262
300
  // user's intent. An exact name match is the strongest signal. Actionable
263
301
  // asset types (skills, commands, agents) are more useful than passive
@@ -280,7 +318,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
280
318
  })();
281
319
  // Resolve project-context tokens from the current working directory once
282
320
  // per search invocation. Returns null when running from home dir / /tmp,
283
- // or when the caller has set AKM_DISABLE_PROJECT_CONTEXT=1.
321
+ // or when the caller passed `--no-project-context` (disableProjectContext).
284
322
  const projectContext = disableProjectContext ? null : resolveProjectContext(process.cwd());
285
323
  // Phase 2A / Rec 5: resolve forgetting-curve config and skip the feedback
286
324
  // count query when the boost cannot make a difference (default ≤ 1.0 means
@@ -295,10 +333,11 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
295
333
  // query here would be pure overhead. The boost > 1.0 sub-gate then skips the
296
334
  // query when the configured boost is a no-op (1.5^count when boost==1 is 1).
297
335
  const positiveFeedbackCounts = shouldQueryPositiveFeedbackCounts(utilityDecayRaw)
298
- ? getPositiveFeedbackCountsByIds(db, scored.map((item) => item.id))
336
+ ? getPositiveFeedbackCountsByIds(scored.map((item) => item.id))
299
337
  : undefined;
300
338
  // Resolve per-project scope key for scoped utility scoring.
301
- // AKM_DISABLE_SCOPED_UTILITY=1 opts out (e.g. for registry searches or tests).
339
+ // `disableScopedUtility` (wired from `akm search --no-project-context`)
340
+ // opts out (e.g. for registry searches or tests).
302
341
  let scopeKey;
303
342
  try {
304
343
  scopeKey = disableScopedUtility ? undefined : getCurrentWorkflowScopeKey();
@@ -323,8 +362,14 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
323
362
  // Drop semantic-only hits (cosine-only, no FTS match) whose score falls
324
363
  // below the configured floor. FTS hits and hybrid hits are always kept.
325
364
  // Default floor: 0.2. Set search.minScore = 0 in config to disable.
365
+ // Judged on the PRE-ceiling score when a demoting belief state clamped the
366
+ // item (`preCeilingScore`): the belief ceilings can sit below this floor
367
+ // (archived 0.15 < 0.2), and a demotion must rank the hit last, not
368
+ // silently remove a result that would otherwise have listed.
326
369
  const minScore = config.search?.minScore ?? 0.2;
327
- const preFilter = minScore > 0 ? scored.filter((item) => item.rankingMode !== "semantic" || item.score >= minScore) : scored;
370
+ const preFilter = minScore > 0
371
+ ? scored.filter((item) => item.rankingMode !== "semantic" || (item.preCeilingScore ?? item.score) >= minScore)
372
+ : scored;
328
373
  // Deterministic tiebreaker on equal scores.
329
374
  //
330
375
  // CRITICAL: sort on the SAME clamped+rounded value the user sees (see the
@@ -341,34 +386,26 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
341
386
  const displayScore = (s) => Math.round(Math.min(1, Math.max(0, s)) * 10000) / 10000;
342
387
  preFilter.sort((a, b) => displayScore(b.score) - displayScore(a.score) || a.entry.name.localeCompare(b.entry.name));
343
388
  // Deduplicate by file path — keep only the highest-scored entry per file.
344
- // Multiple .stash.json entries can map to the same file (e.g. entries without
345
- // a filename field all collapse to files[0]). Showing the same path/ref
346
- // multiple times clutters results.
347
389
  const deduped = deduplicateByPath(preFilter);
348
- // Source filter: when the caller narrowed `sources` via `--source <name>`,
349
- // drop hits whose filePath does not live under any of the requested
350
- // sources. The FTS/vector index spans every configured source, so without
351
- // this filter a narrowed --source request would still leak results from
352
- // other sources that happened to match the query text.
353
- const sourceFiltered = restrictToSources
354
- ? deduped.filter((item) => findSourceForPath(item.filePath, sources) !== undefined)
355
- : deduped;
356
- // Scope filter: drop hits whose stored scope does not satisfy every supplied
357
- // key. Applied AFTER ranking — filtering narrows the result set without
358
- // touching the single FTS5+boosts scoring pipeline.
359
- const scopeFiltered = filters
360
- ? sourceFiltered.filter((item) => entryMatchesScope(item.entry.scope, filters))
361
- : sourceFiltered;
362
- // Proposed-quality filter (v1 spec §4.2): exclude entries with
363
- // `quality: "proposed"` unless the caller passed `--include-proposed`.
364
- // Applied AFTER ranking for the same reason as scope filtering.
365
- const qualityFiltered = includeProposed
366
- ? scopeFiltered
367
- : scopeFiltered.filter((item) => !isProposedQuality(item.entry.quality));
368
- const beliefFiltered = qualityFiltered.filter((item) => matchBeliefFilter(item.entry.beliefState, beliefFilter));
390
+ // Source → scope → proposed-quality → derived-twin belief inheritance →
391
+ // belief: the post-candidate filter chain shared with enumerateEntries (see
392
+ // applyEntryFilters). Applied AFTER ranking so filtering narrows the result
393
+ // set without touching the single FTS5+boosts scoring pipeline. The twin
394
+ // inheritance inside the chain re-runs here as an idempotent no-op — it
395
+ // already ran on the full candidate pool before ranking (:460) to feed the
396
+ // belief-state ranker.
397
+ const beliefFiltered = applyEntryFilters(deduped, {
398
+ db,
399
+ sources,
400
+ restrictToSources,
401
+ filters,
402
+ includeProposed,
403
+ beliefFilter,
404
+ });
369
405
  const rankMs = Date.now() - tRank0;
370
406
  const selected = beliefFiltered.slice(0, limit);
371
- const hits = await Promise.all(selected.map(({ entry, filePath, score, rankingMode, utilityBoosted }) => {
407
+ const hits = await Promise.all(selected.map((ranked) => {
408
+ const { entry, filePath, score, rankingMode, utilityBoosted } = ranked;
372
409
  // CLAUDE.md locks SearchHit.score in [0,1]. The boost loop above can
373
410
  // exceed 1.0 (this was a pre-existing breach that #207's graph boost
374
411
  // — up to ~1.05 additive contribution — made detectable); clamp here
@@ -377,7 +414,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
377
414
  return buildDbHit({
378
415
  entry,
379
416
  path: filePath,
380
- // Round to 4 decimal places
417
+ ...indexedProvenance(ranked),
381
418
  score: Math.round(finalScore * 10000) / 10000,
382
419
  query,
383
420
  rankingMode,
@@ -387,11 +424,148 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
387
424
  config,
388
425
  utilityBoosted,
389
426
  graphContext,
427
+ attributionSource: ranked,
390
428
  rendererRegistry,
391
429
  db,
392
430
  });
393
431
  }));
394
- return { embedMs, rankMs, hits };
432
+ return { embedMs, rankMs, hits, usedSemantic };
433
+ }
434
+ /**
435
+ * The no-hits tip. A query in the retired `<type>:` / `<type>:<prefix>/` browse
436
+ * grammar gets the conceptId spelling that replaces it: without this it comes
437
+ * back empty and silent, which is the failure D4 removed the grammar to avoid.
438
+ */
439
+ function emptyResultTip(query) {
440
+ const generic = "No matching stash assets were found. Try a different query or run 'akm index' to rebuild.";
441
+ const retired = parseRetiredTypePrefixQuery(query);
442
+ if (!retired)
443
+ return generic;
444
+ const root = stashDirFor(retired.type);
445
+ if (!root)
446
+ return generic;
447
+ return `No matching stash assets were found. The '<type>:' browse grammar was removed in 0.9.0 — use the conceptId spelling: 'akm search "${root}/${retired.rest}"'.`;
448
+ }
449
+ // ── Enumeration (browse) path ────────────────────────────────────────────────
450
+ /**
451
+ * Enumerate index entries without FTS scoring — the browse path shared by
452
+ * empty/unsearchable queries and D4 conceptId-prefix queries (`memories/`,
453
+ * `bundle//`, `bundle//skills/`). Applies the same post-ranking filters as the scored
454
+ * path (source narrowing, scope, proposed-quality, belief) before the limit
455
+ * slice. Hits carry the fixed browse score 1 in type-then-name order — this is
456
+ * a deterministic listing, not a relevance ranking.
457
+ */
458
+ async function enumerateEntries(opts) {
459
+ const { db, query, sources, config, rendererRegistry, filters, beliefFilter } = opts;
460
+ const allEntries = getAllEntries(db, opts.typeFilter, opts.excludeTypes).filter(hasIndexedProvenance);
461
+ // Explicit listing order: type, then name, then filePath. The underlying
462
+ // SELECT carries no ORDER BY, so its row order tracks the query plan and the
463
+ // index-insertion (file-walk) order — both machine-dependent. A browse
464
+ // listing must not change order across hosts or SQLite versions.
465
+ allEntries.sort((a, b) => a.entry.type.localeCompare(b.entry.type) ||
466
+ a.entry.name.localeCompare(b.entry.name) ||
467
+ a.filePath.localeCompare(b.filePath));
468
+ // D4: narrow to the requested bundle and subtree. Matching is against the
469
+ // conceptId — the spelling every emitted `ref` carries — so a ref copied out
470
+ // of search output round-trips back in. `startsWith` on the full
471
+ // slash-retaining prefix is exact: "memories/projecta/" cannot match a
472
+ // sibling "memories/projectalpha/…" scope.
473
+ const bundle = opts.bundle?.toLowerCase();
474
+ const bundleFiltered = bundle === undefined ? allEntries : allEntries.filter((ie) => ie.bundleId.toLowerCase() === bundle);
475
+ const conceptIdPrefix = opts.conceptIdPrefix?.toLowerCase() ?? "";
476
+ const prefixFiltered = conceptIdPrefix.length > 0
477
+ ? bundleFiltered.filter((ie) => ie.conceptId.toLowerCase().startsWith(conceptIdPrefix))
478
+ : bundleFiltered;
479
+ // Deduplicate by file path — multiple entries can share the same file
480
+ const seenFilePaths = new Set();
481
+ const uniqueEntries = prefixFiltered.filter((ie) => {
482
+ if (seenFilePaths.has(ie.filePath))
483
+ return false;
484
+ seenFilePaths.add(ie.filePath);
485
+ return true;
486
+ });
487
+ // Source → scope → proposed-quality → derived-twin belief inheritance →
488
+ // belief: the post-candidate filter chain shared with searchDatabase's
489
+ // scored path (see applyEntryFilters). Filtering happens BEFORE the limit
490
+ // slice so a restrictive filter still returns up to `limit` results. On this
491
+ // path the twin inheritance is the ONLY place it runs (there is no ranking
492
+ // pass), keeping the belief filter and reported hit state consistent with
493
+ // the scored path.
494
+ const beliefFiltered = applyEntryFilters(uniqueEntries, {
495
+ db,
496
+ sources,
497
+ restrictToSources: opts.restrictToSources,
498
+ filters,
499
+ includeProposed: opts.includeProposed,
500
+ beliefFilter,
501
+ });
502
+ const selected = beliefFiltered.slice(0, opts.limit);
503
+ const hits = await Promise.all(selected.map((ie) => buildDbHit({
504
+ entry: ie.entry,
505
+ path: ie.filePath,
506
+ itemRef: ie.itemRef,
507
+ bundleId: ie.bundleId,
508
+ conceptId: ie.conceptId,
509
+ score: 1,
510
+ query,
511
+ rankingMode: "fts",
512
+ defaultStashDir: opts.stashDir,
513
+ allSourceDirs: opts.allSourceDirs,
514
+ sources,
515
+ config,
516
+ rendererRegistry,
517
+ db,
518
+ })));
519
+ return { hits };
520
+ }
521
+ /**
522
+ * Post-candidate filter chain shared by BOTH search paths — the scored path
523
+ * (`searchDatabase`) and the browse path (`enumerateEntries`). Applies, in this
524
+ * exact order: source-narrowing → scope → proposed-quality → derived-twin
525
+ * belief inheritance → belief filter. Extracting the chain removes the two
526
+ * paths' formerly-duplicated filter sequences so the predicates, their order,
527
+ * and the twin-inheritance placement can never drift apart (plan §4.3).
528
+ *
529
+ * What this does NOT unify — and deliberately leaves divergent — is CANDIDATE-
530
+ * POOL construction, which is inherent search-vs-browse semantics: the scored
531
+ * path's pool is `searchFts`/vector matches for the query's own tokens (FTS
532
+ * indexes description/tags/searchHints/aliases, not raw body prose), while the
533
+ * enumerate path's pool is `getAllEntries` for the type, independent of query
534
+ * text. A derived twin sharing no indexed token with the query is therefore an
535
+ * enumerate-path candidate but never a scored-path candidate — see
536
+ * tests/fixtures/goldens/filter-behavior/scored-vs-enumerate.json, which pins
537
+ * that (retained) divergence.
538
+ *
539
+ * `inheritDerivedTwinBeliefStates` is idempotent, so running it here is safe on
540
+ * the scored path, which must ALSO call it before ranking (the belief-state
541
+ * ranker demotes inherited states): by the time this chain runs, those twins
542
+ * already carry a state and the call here is a no-op for them. The enumerate
543
+ * path never ranks, so this is the only place it inherits.
544
+ */
545
+ function applyEntryFilters(items, opts) {
546
+ const { filters } = opts;
547
+ // Source filter: when the caller narrowed `sources` via `--from <name>`,
548
+ // drop entries whose filePath does not live under any requested source. The
549
+ // FTS/enumerate index spans every configured source, so without this filter a
550
+ // narrowed --from request would still leak results from other sources.
551
+ const sourceFiltered = opts.restrictToSources
552
+ ? items.filter((item) => findSourceForPath(item.filePath, opts.sources) !== undefined)
553
+ : items;
554
+ // Scope filter: drop entries whose stored scope does not satisfy every
555
+ // supplied key.
556
+ const scopeFiltered = filters
557
+ ? sourceFiltered.filter((item) => entryMatchesScope(item.entry.scope, filters))
558
+ : sourceFiltered;
559
+ // Proposed-quality filter (v1 spec §4.2): exclude `quality: "proposed"`
560
+ // entries unless the caller opts in.
561
+ const qualityFiltered = opts.includeProposed
562
+ ? scopeFiltered
563
+ : scopeFiltered.filter((item) => !isProposedQuality(item.entry.quality));
564
+ // 03-R3: derived twins inherit their base's demoting belief state BEFORE the
565
+ // belief filter, so the filter (and the reported hit state) stays consistent
566
+ // across both paths.
567
+ inheritDerivedTwinBeliefStates(opts.db, qualityFiltered);
568
+ return qualityFiltered.filter((item) => matchBeliefFilter(item.entry.beliefState, opts.beliefFilter));
395
569
  }
396
570
  /**
397
571
  * 03-R3: let each `.derived` twin inherit its base memory's demoting belief
@@ -466,7 +640,8 @@ async function tryVecScores(db, query, k, config) {
466
640
  // ── Hit building ────────────────────────────────────────────────────────────
467
641
  export async function buildDbHit(input) {
468
642
  const rendererRegistry = input.rendererRegistry ?? defaultRendererRegistry;
469
- const entryStashDir = findSourceForPath(input.path, input.sources)?.path ?? input.defaultStashDir;
643
+ const absolutePath = path.resolve(input.path);
644
+ const entryStashDir = findSourceForPath(absolutePath, input.sources)?.path ?? input.defaultStashDir;
470
645
  // Quality and confidence boosts are now applied in the main scoring
471
646
  // phase (searchDatabase). buildDbHit receives the already-final score and
472
647
  // passes it through without further multiplication. We still compute the
@@ -477,23 +652,25 @@ export async function buildDbHit(input) {
477
652
  const confidenceBoost = typeof input.entry.confidence === "number" ? Math.min(0.05, Math.max(0, input.entry.confidence) * 0.05) : 0;
478
653
  // Round to 4 decimal places, no boost multiplication
479
654
  const score = Math.round(input.score * 10000) / 10000;
480
- const graphBoost = input.graphContext ? computeGraphBoost(input.graphContext, input.path) : 0;
655
+ const graphBoost = getSearchHitAttribution(input.attributionSource ?? {})?.graphExtraction?.boost ?? 0;
481
656
  const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost, input.utilityBoosted, graphBoost);
482
- const graphHit = input.graphContext ? collectGraphRelatedHit(input.graphContext, input.path) : null;
483
- const source = findSourceForPath(input.path, input.sources);
484
- const ref = resolveSearchHitRef(input.entry, input.entry.name, source);
485
- const editable = isEditable(input.path, input.config);
657
+ const graphHit = input.graphContext ? collectGraphRelatedHit(input.graphContext, absolutePath) : null;
658
+ const source = findSourceForPath(absolutePath, input.sources);
659
+ const defaultBundleId = input.config?.defaultBundle ??
660
+ (source && path.resolve(source.path) === path.resolve(input.defaultStashDir)
661
+ ? (input.bundleId ?? undefined)
662
+ : undefined);
663
+ const ref = resolveSearchHitRef(input.entry, input, defaultBundleId);
664
+ const editable = isEditable(absolutePath, input.config, input.sources);
486
665
  const estimatedTokens = typeof input.entry.fileSize === "number" ? Math.round(input.entry.fileSize / 4) : undefined;
487
666
  const hit = {
488
667
  type: input.entry.type,
489
668
  name: input.entry.name,
490
- path: input.path,
669
+ path: absolutePath,
491
670
  ref,
492
671
  origin: resolveSearchHitOrigin(source),
493
672
  editable,
494
- ...(!editable
495
- ? { editHint: buildEditHint(input.path, input.entry.type, input.entry.name, source?.registryId) }
496
- : {}),
673
+ ...(!editable ? { editHint: buildEditHint(ref) } : {}),
497
674
  description: input.entry.description,
498
675
  tags: input.entry.tags,
499
676
  size: deriveSize(input.entry.fileSize),
@@ -508,6 +685,13 @@ export async function buildDbHit(input) {
508
685
  ...(input.entry.currentBeliefRefs ? { currentBeliefRefs: input.entry.currentBeliefRefs } : {}),
509
686
  ...(graphHit ? { graph: { entities: graphHit.entities, relations: graphHit.relations } } : {}),
510
687
  };
688
+ if (input.attributionSource)
689
+ copySearchHitAttribution(input.attributionSource, hit);
690
+ if (input.entry.derivedFrom) {
691
+ attachSearchHitAttribution(hit, {
692
+ memoryInference: { exposure: "direct" },
693
+ });
694
+ }
511
695
  await enrichSearchHit(hit, {
512
696
  type: input.entry.type,
513
697
  stashDir: entryStashDir,
@@ -605,7 +789,7 @@ function deduplicateByPath(items) {
605
789
  });
606
790
  }
607
791
  /**
608
- * Exact-match scope filter check. Legacy entries without a `scope` object only
792
+ * Exact-match scope filter check. Entries without a `scope` object only
609
793
  * match when no filter is supplied — which is what the caller guards on
610
794
  * before invoking this helper.
611
795
  */
@@ -6,6 +6,8 @@
6
6
  *
7
7
  * These transform a raw user query into an FTS5-safe MATCH expression. They
8
8
  * touch no database state, so they are unit-testable with zero DB setup.
9
+ * `parseRefPrefixQuery` is the one non-FTS helper: it decides whether a raw
10
+ * query should bypass FTS entirely (SPEC-4 ref-prefix enumeration).
9
11
  */
10
12
  /**
11
13
  * Sanitize a raw user query into an FTS5-safe implicit-AND expression.
@@ -49,3 +51,65 @@ export function buildPrefixQuery(ftsQuery) {
49
51
  return null;
50
52
  return prefixTokens.join(" ");
51
53
  }
54
+ /**
55
+ * D4 — parse a conceptId-prefix browse query.
56
+ *
57
+ * Decides whether a raw query is a subtree-enumeration request rather than an
58
+ * ordinary keyword search. Matching is deliberately conservative: the trimmed
59
+ * query must be EXACTLY
60
+ *
61
+ * - `<conceptId prefix>/` → that subtree in any bundle,
62
+ * - `<bundle>//` → one bundle entirely,
63
+ * - `<bundle>//<conceptId prefix>/` → that subtree of that bundle.
64
+ *
65
+ * The trailing slash is REQUIRED — and is RETAINED in `conceptIdPrefix` — so a
66
+ * plain `conceptId.startsWith(conceptIdPrefix)` check gives exact `/`-boundary
67
+ * subtree semantics (`"projecta/"` cannot match a sibling `projectalpha/…`
68
+ * scope). Bare refs like `memories/a/b` therefore stay ordinary searches
69
+ * (resolving one ref is `akm show` territory), and any interior whitespace
70
+ * disqualifies (prose mentioning a ref is still prose).
71
+ *
72
+ * The prefix matches the conceptId — the same string every emitted `ref`
73
+ * carries — so a ref copied out of search output round-trips back in as a
74
+ * prefix. Nothing here consults a type list: enumeration covers every
75
+ * adapter's items uniformly, which the retired `<type>:` grammar could not do.
76
+ *
77
+ * Returns `null` when the query is not a browse request.
78
+ */
79
+ export function parseRefPrefixQuery(query) {
80
+ const trimmed = query.trim();
81
+ if (trimmed.length === 0 || /\s/.test(trimmed))
82
+ return null;
83
+ const separator = trimmed.indexOf("//");
84
+ if (separator < 0) {
85
+ return trimmed.endsWith("/") ? { conceptIdPrefix: trimmed } : null;
86
+ }
87
+ const bundle = trimmed.slice(0, separator);
88
+ if (bundle.length === 0)
89
+ return null;
90
+ const rest = trimmed.slice(separator + 2);
91
+ if (rest === "")
92
+ return { bundle, conceptIdPrefix: "" };
93
+ if (rest.endsWith("/") && !rest.includes("//"))
94
+ return { bundle, conceptIdPrefix: rest };
95
+ return null;
96
+ }
97
+ /**
98
+ * Recognize the retired `<type>:` / `<type>:<prefix>/` browse grammar so the
99
+ * caller can name the replacement spelling rather than letting the query
100
+ * degrade silently into a keyword search — the exact silent failure D4 removes.
101
+ * Shape recognition only; mapping the type to its conceptId root belongs to the
102
+ * caller, which keeps this module dependency-free.
103
+ */
104
+ export function parseRetiredTypePrefixQuery(query) {
105
+ const trimmed = query.trim();
106
+ if (trimmed.length === 0 || /\s/.test(trimmed) || trimmed.includes("//"))
107
+ return null;
108
+ const colon = trimmed.indexOf(":");
109
+ if (colon <= 0)
110
+ return null;
111
+ const rest = trimmed.slice(colon + 1);
112
+ if (rest !== "" && !rest.endsWith("/"))
113
+ return null;
114
+ return { type: trimmed.slice(0, colon), rest };
115
+ }