akm-cli 0.9.17-alpha.1 → 0.9.17-alpha.10

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 (399) hide show
  1. package/CHANGELOG.md +1623 -0
  2. package/STABILITY.md +11 -10
  3. package/dist/akm +124 -193
  4. package/dist/akm-migrate +38 -19
  5. package/dist/assets/hints/cli-hints-full.md +6 -7
  6. package/dist/assets/improve-strategies/catchup.json +0 -3
  7. package/dist/assets/improve-strategies/consolidate.json +0 -1
  8. package/dist/assets/improve-strategies/default.json +1 -2
  9. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
  10. package/dist/assets/improve-strategies/quick.json +1 -2
  11. package/dist/assets/improve-strategies/reflect-distill.json +1 -2
  12. package/dist/assets/improve-strategies/thorough.json +0 -3
  13. package/dist/assets/prompts/consolidate-pair.md +20 -0
  14. package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
  15. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
  16. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
  17. package/dist/assets/templates/html/health.html +3 -5
  18. package/dist/cli/retired-commands.js +1 -1
  19. package/dist/cli/shared.js +6 -2
  20. package/dist/cli/unknown-flags.js +24 -1
  21. package/dist/cli.js +68 -10
  22. package/dist/commands/agent/agent-dispatch.js +1 -1
  23. package/dist/commands/command/command-execution.js +24 -62
  24. package/dist/commands/feedback-cli.js +0 -1
  25. package/dist/commands/health/accept-rate.js +2 -2
  26. package/dist/commands/health/archive-usage.js +92 -0
  27. package/dist/commands/health/checks.js +30 -75
  28. package/dist/commands/health/config-skew.js +38 -0
  29. package/dist/commands/health/data-dir-usage.js +25 -13
  30. package/dist/commands/health/egress.js +54 -0
  31. package/dist/commands/health/html-report.js +1 -42
  32. package/dist/commands/health/improve-metrics.js +136 -587
  33. package/dist/commands/health/md-report.js +1 -6
  34. package/dist/commands/health/plugin-staleness.js +53 -3
  35. package/dist/commands/health/renderers.js +12 -4
  36. package/dist/commands/health/report-view-model.js +14 -119
  37. package/dist/commands/health/types-improve.js +4 -19
  38. package/dist/commands/health/windows.js +64 -74
  39. package/dist/commands/health.js +135 -143
  40. package/dist/commands/improve/consolidate/chunking.js +25 -100
  41. package/dist/commands/improve/consolidate/continuity-check.js +137 -0
  42. package/dist/commands/improve/consolidate/pair-pass.js +791 -0
  43. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  44. package/dist/commands/improve/consolidate.js +573 -1124
  45. package/dist/commands/improve/content-hash.js +16 -24
  46. package/dist/commands/improve/distill/content-repair.js +18 -100
  47. package/dist/commands/improve/distill-guards.js +20 -81
  48. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  49. package/dist/commands/improve/distill.js +608 -1075
  50. package/dist/commands/improve/eligibility.js +126 -400
  51. package/dist/commands/improve/execution.js +8 -10
  52. package/dist/commands/improve/extract-prompt.js +1 -2
  53. package/dist/commands/improve/extract.js +487 -1046
  54. package/dist/commands/improve/feedback-valence.js +0 -25
  55. package/dist/commands/improve/improve-cli.js +54 -171
  56. package/dist/commands/improve/improve-result-file.js +10 -66
  57. package/dist/commands/improve/improve-strategies.js +35 -9
  58. package/dist/commands/improve/improve-usage-report.js +18 -64
  59. package/dist/commands/improve/improve.js +451 -1082
  60. package/dist/commands/improve/ledger.js +119 -0
  61. package/dist/commands/improve/locks.js +2 -8
  62. package/dist/commands/improve/loop-stages.js +401 -1192
  63. package/dist/commands/improve/memory/derived-ref.js +12 -77
  64. package/dist/commands/improve/memory/memory-belief.js +16 -118
  65. package/dist/commands/improve/memory/memory-improve.js +266 -14
  66. package/dist/commands/improve/outcome-loop.js +28 -156
  67. package/dist/commands/improve/planner.js +5 -15
  68. package/dist/commands/improve/preparation.js +776 -2349
  69. package/dist/commands/improve/proactive-maintenance.js +34 -101
  70. package/dist/commands/improve/reflect-noise.js +104 -280
  71. package/dist/commands/improve/reflect.js +642 -1364
  72. package/dist/commands/improve/retrieval-gate.js +127 -0
  73. package/dist/commands/improve/retrieval-scope.js +92 -0
  74. package/dist/commands/improve/salience.js +41 -240
  75. package/dist/commands/improve/session-asset.js +19 -100
  76. package/dist/commands/improve/stage.js +322 -0
  77. package/dist/commands/lint/base-linter.js +37 -15
  78. package/dist/commands/proposal/drain.js +258 -644
  79. package/dist/commands/proposal/proposal-cli.js +19 -20
  80. package/dist/commands/proposal/proposal-types.js +27 -41
  81. package/dist/commands/proposal/proposal.js +38 -8
  82. package/dist/commands/proposal/propose.js +134 -160
  83. package/dist/commands/proposal/repository.js +1099 -1475
  84. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  85. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  86. package/dist/commands/proposal/validators/proposals.js +22 -89
  87. package/dist/commands/read/curate.js +105 -462
  88. package/dist/commands/read/knowledge.js +3 -2
  89. package/dist/commands/read/search-cli.js +16 -33
  90. package/dist/commands/read/search.js +17 -23
  91. package/dist/commands/read/show.js +57 -108
  92. package/dist/commands/sources/bundle-cli.js +25 -2
  93. package/dist/commands/sources/bundle-config-ops.js +4 -0
  94. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  95. package/dist/commands/sources/info.js +127 -29
  96. package/dist/commands/sources/installed-stashes.js +197 -746
  97. package/dist/commands/sources/schema-repair.js +98 -129
  98. package/dist/commands/sources/source-add.js +62 -12
  99. package/dist/commands/sources/source-manage.js +9 -2
  100. package/dist/commands/sources/stash-cli.js +24 -4
  101. package/dist/commands/tasks/explain.js +10 -13
  102. package/dist/commands/tasks/tasks-cli.js +12 -13
  103. package/dist/commands/tasks/tasks.js +350 -936
  104. package/dist/commands/tasks/validate.js +26 -24
  105. package/dist/commands/workflow/plan.js +22 -29
  106. package/dist/commands/workflow-cli.js +4 -4
  107. package/dist/core/adapter/adapters/akm-adapter.js +2 -1
  108. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  109. package/dist/core/adapter/adapters/akm-metadata.js +42 -12
  110. package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
  111. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  112. package/dist/core/adapter/execution-source.js +17 -29
  113. package/dist/core/asset/asset-placement.js +4 -13
  114. package/dist/core/asset/resolve-ref.js +1 -1
  115. package/dist/core/bundle-id.js +42 -5
  116. package/dist/core/bundle-rename.js +285 -0
  117. package/dist/core/config/config-io.js +1 -2
  118. package/dist/core/config/config-schema.js +9 -34
  119. package/dist/core/config/config-walker.js +1 -1
  120. package/dist/core/config/config.js +184 -111
  121. package/dist/core/config/engine-semantics.js +0 -2
  122. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  123. package/dist/core/config/schema/embedding.js +20 -5
  124. package/dist/core/config/schema/engines.js +5 -0
  125. package/dist/core/config/schema/execution.js +1 -1
  126. package/dist/core/config/schema/experimental.js +1 -1
  127. package/dist/core/config/schema/improve-processes.js +37 -135
  128. package/dist/core/config/schema/improve.js +4 -42
  129. package/dist/core/config/schema/index-config.js +9 -48
  130. package/dist/core/config/schema/scheduler.js +12 -12
  131. package/dist/core/config/schema/search.js +6 -22
  132. package/dist/core/env-secret-ref.js +0 -1
  133. package/dist/core/errors.js +8 -9
  134. package/dist/core/file-change.js +13 -5
  135. package/dist/core/file-lock.js +76 -173
  136. package/dist/core/improve-result.js +26 -7
  137. package/dist/core/improve-types.js +0 -1
  138. package/dist/core/logs-db.js +2 -2
  139. package/dist/core/loopback.js +7 -12
  140. package/dist/core/non-task-input.js +20 -0
  141. package/dist/core/parse.js +13 -16
  142. package/dist/core/paths.js +0 -31
  143. package/dist/core/redaction.js +109 -2
  144. package/dist/core/run-lock.js +2 -5
  145. package/dist/core/spawn-env.js +1 -1
  146. package/dist/core/state/migrations.js +123 -61
  147. package/dist/core/state-db-scope.js +2 -4
  148. package/dist/core/state-db.js +126 -692
  149. package/dist/core/time.js +0 -20
  150. package/dist/core/type-presentation.js +1 -9
  151. package/dist/core/write-source.js +294 -1005
  152. package/dist/execution/input-contract.js +1 -1
  153. package/dist/execution/resolved-request.js +135 -689
  154. package/dist/execution/source.js +63 -257
  155. package/dist/execution/target-ref.js +1 -1
  156. package/dist/indexer/bundle-identity-guard.js +2 -2
  157. package/dist/indexer/db/llm-cache.js +2 -2
  158. package/dist/indexer/ensure-index.js +46 -87
  159. package/dist/indexer/index-rebuild-lock.js +3 -11
  160. package/dist/indexer/index-writer-lock.js +8 -17
  161. package/dist/indexer/index-written-assets.js +141 -154
  162. package/dist/indexer/indexer.js +400 -1124
  163. package/dist/indexer/links/declared-links.js +90 -0
  164. package/dist/indexer/materialize-embeddings.js +60 -397
  165. package/dist/indexer/passes/memory-inference.js +81 -90
  166. package/dist/indexer/passes/metadata.js +132 -219
  167. package/dist/indexer/read-preflight.js +0 -7
  168. package/dist/indexer/scan/doc-to-entry.js +2 -3
  169. package/dist/indexer/scan/drain-dir.js +1 -1
  170. package/dist/indexer/search/db-search.js +190 -590
  171. package/dist/indexer/search/fts-query.js +30 -41
  172. package/dist/indexer/search/ranking.js +28 -154
  173. package/dist/indexer/search/search-attribution.js +12 -32
  174. package/dist/indexer/search/search-fields.js +11 -15
  175. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  176. package/dist/indexer/search/search-source.js +1 -4
  177. package/dist/indexer/usage/usage-events.js +36 -7
  178. package/dist/indexer/walk/walker.js +3 -4
  179. package/dist/integrations/agent/engine-fallback.js +23 -40
  180. package/dist/integrations/agent/engine-resolution.js +93 -183
  181. package/dist/integrations/agent/execution.js +507 -0
  182. package/dist/integrations/agent/model-map.js +28 -156
  183. package/dist/integrations/agent/request-lowering.js +66 -141
  184. package/dist/integrations/agent/runner-dispatch.js +143 -321
  185. package/dist/integrations/agent/runner.js +54 -14
  186. package/dist/integrations/lockfile.js +53 -101
  187. package/dist/llm/client.js +8 -10
  188. package/dist/llm/embedders/deterministic.js +2 -3
  189. package/dist/llm/embedders/profile.js +71 -0
  190. package/dist/llm/embedders/remote.js +11 -17
  191. package/dist/llm/feature-gate.js +0 -8
  192. package/dist/llm/index-passes.js +3 -5
  193. package/dist/llm/memory-infer.js +1 -2
  194. package/dist/llm/structured-call.js +5 -24
  195. package/dist/output/generic-render.js +23 -11
  196. package/dist/output/html-render.js +13 -10
  197. package/dist/output/render-registry.js +3 -32
  198. package/dist/output/shapes/helpers.js +25 -38
  199. package/dist/output/shapes/passthrough.js +1 -9
  200. package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
  201. package/dist/output/text/command-format.js +69 -31
  202. package/dist/output/text/helpers.js +1 -1
  203. package/dist/output/text/migrate.js +5 -14
  204. package/dist/output/text/proposal-format.js +48 -3
  205. package/dist/output/text/show-format.js +13 -17
  206. package/dist/output/text/workflow-format.js +0 -32
  207. package/dist/output/text.js +2 -0
  208. package/dist/registry/factory.js +4 -19
  209. package/dist/registry/network.js +66 -220
  210. package/dist/registry/providers/index.js +0 -2
  211. package/dist/registry/providers/skills-sh.js +3 -14
  212. package/dist/registry/providers/static-index.js +24 -26
  213. package/dist/registry/resolve.js +55 -131
  214. package/dist/scripts/akm-migrate-node.js +42938 -92375
  215. package/dist/scripts/akm-migrate.js +42930 -92365
  216. package/dist/setup/registry-stash-loader.js +4 -13
  217. package/dist/setup/semantic-assets.js +3 -44
  218. package/dist/setup/setup.js +1 -1
  219. package/dist/setup/steps/connection.js +5 -6
  220. package/dist/setup/steps/platforms.js +2 -2
  221. package/dist/setup/steps/tasks.js +25 -15
  222. package/dist/sources/provider-factory.js +17 -18
  223. package/dist/sources/providers/filesystem.js +2 -3
  224. package/dist/sources/providers/git-install.js +7 -1
  225. package/dist/sources/providers/git-provider.js +0 -3
  226. package/dist/sources/providers/git-stash.js +83 -21
  227. package/dist/sources/providers/npm.js +2 -4
  228. package/dist/sources/providers/provider-utils.js +5 -10
  229. package/dist/sources/providers/website.js +0 -2
  230. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  231. package/dist/sources/website-url.js +2 -2
  232. package/dist/storage/database.js +9 -35
  233. package/dist/storage/repositories/improve-ledger-repository.js +209 -0
  234. package/dist/storage/repositories/index-connection.js +39 -72
  235. package/dist/storage/repositories/index-entries-repository.js +88 -129
  236. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  237. package/dist/storage/repositories/index-entry-schema.js +101 -268
  238. package/dist/storage/repositories/index-fts-repository.js +86 -256
  239. package/dist/storage/repositories/index-links-repository.js +143 -0
  240. package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
  241. package/dist/storage/repositories/index-meta-repository.js +6 -4
  242. package/dist/storage/repositories/index-schema.js +257 -325
  243. package/dist/storage/repositories/index-utility-repository.js +8 -29
  244. package/dist/storage/repositories/index-vec-repository.js +133 -414
  245. package/dist/storage/repositories/outcome-repository.js +2 -1
  246. package/dist/storage/repositories/proposals-repository.js +100 -0
  247. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  248. package/dist/storage/repositories/salience-repository.js +1 -19
  249. package/dist/storage/repositories/task-history-repository.js +26 -4
  250. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  251. package/dist/storage/sqlite-migrations.js +136 -0
  252. package/dist/storage/sqlite-pragmas.js +11 -9
  253. package/dist/storage/sqlite-transaction.js +170 -0
  254. package/dist/storage/state-db-integrity.js +34 -27
  255. package/dist/tasks/activation-config.js +134 -62
  256. package/dist/tasks/backends/cron.js +191 -302
  257. package/dist/tasks/backends/exec-utils.js +2 -5
  258. package/dist/tasks/backends/launchd.js +141 -748
  259. package/dist/tasks/backends/schtasks.js +119 -623
  260. package/dist/tasks/prepare/prepare-support.js +5 -15
  261. package/dist/tasks/prepare/prepare.js +0 -2
  262. package/dist/tasks/resolve-akm-bin.js +20 -79
  263. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  264. package/dist/tasks/run/load-task.js +1 -1
  265. package/dist/tasks/scheduler-binding.js +20 -238
  266. package/dist/tasks/scheduler-invocation.js +136 -244
  267. package/dist/tasks/scheduler-lock.js +53 -0
  268. package/dist/tasks/scheduler-sync.js +368 -679
  269. package/dist/tasks/source/parse-task-source.js +55 -9
  270. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  271. package/dist/tasks/source/task-to-v4.js +464 -88
  272. package/dist/workflows/authoring/authoring.js +3 -12
  273. package/dist/workflows/compile.js +211 -0
  274. package/dist/workflows/concurrency-policy.js +13 -74
  275. package/dist/workflows/exec/child-invocation.js +3 -17
  276. package/dist/workflows/exec/child-workflow.js +32 -141
  277. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  278. package/dist/workflows/exec/environment.js +98 -0
  279. package/dist/workflows/exec/exec-unit.js +33 -140
  280. package/dist/workflows/exec/frozen-judge.js +7 -59
  281. package/dist/workflows/exec/native-executor.js +82 -341
  282. package/dist/workflows/exec/param-secrets.js +29 -47
  283. package/dist/workflows/exec/run-workflow.js +154 -387
  284. package/dist/workflows/exec/scheduler.js +9 -36
  285. package/dist/workflows/exec/step-work.js +127 -430
  286. package/dist/workflows/exec/unit-dispatch.js +11 -63
  287. package/dist/workflows/exec/unit-writer.js +8 -52
  288. package/dist/workflows/exec/worktree.js +39 -273
  289. package/dist/workflows/freeze/child-output-references.js +4 -15
  290. package/dist/workflows/freeze/environment.js +99 -92
  291. package/dist/workflows/freeze/freeze.js +172 -0
  292. package/dist/workflows/freeze/step-values.js +19 -21
  293. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  294. package/dist/workflows/freeze/targets/command.js +10 -33
  295. package/dist/workflows/freeze/targets/script.js +5 -12
  296. package/dist/workflows/freeze/targets/shell.js +3 -6
  297. package/dist/workflows/freeze/targets/task.js +25 -80
  298. package/dist/workflows/freeze/task-bindings.js +20 -67
  299. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  300. package/dist/workflows/ir/params.js +6 -51
  301. package/dist/workflows/ir/plan-hash.js +2 -34
  302. package/dist/workflows/parser.js +140 -43
  303. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  304. package/dist/workflows/renderer.js +36 -69
  305. package/dist/workflows/resource-limits.js +12 -120
  306. package/dist/workflows/runtime/agent-identity.js +8 -40
  307. package/dist/workflows/runtime/run-outputs.js +3 -6
  308. package/dist/workflows/runtime/run-plan.js +316 -0
  309. package/dist/workflows/runtime/runs.js +48 -200
  310. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  311. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  312. package/dist/workflows/validate-summary.js +2 -7
  313. package/docs/integration/bundling-akm.md +49 -42
  314. package/docs/migration/README.md +1 -0
  315. package/docs/migration/release-notes/0.9.17.md +43 -0
  316. package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
  317. package/docs/reference/cli.md +229 -133
  318. package/docs/reference/configuration.md +71 -57
  319. package/docs/reference/data-and-telemetry.md +19 -21
  320. package/docs/reference/tasks.md +105 -39
  321. package/docs/reference/workflow-schema.md +14 -18
  322. package/docs/reference/workflows.md +6 -9
  323. package/package.json +1 -1
  324. package/schemas/akm-config.json +87 -754
  325. package/dist/assets/improve-strategies/graph-refresh.json +0 -15
  326. package/dist/assets/prompts/contradiction-judge.md +0 -33
  327. package/dist/assets/prompts/graph-extract-system.md +0 -1
  328. package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
  329. package/dist/assets/prompts/metadata-enhance-system.md +0 -1
  330. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
  331. package/dist/commands/health/advisories.js +0 -150
  332. package/dist/commands/health/metrics.js +0 -329
  333. package/dist/commands/health/surfaces.js +0 -102
  334. package/dist/commands/improve/anti-collapse.js +0 -83
  335. package/dist/commands/improve/collapse-detector.js +0 -432
  336. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  337. package/dist/commands/improve/consolidate/merge.js +0 -146
  338. package/dist/commands/improve/distill/promote-memory.js +0 -329
  339. package/dist/commands/improve/distill/quality-gate.js +0 -500
  340. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  341. package/dist/commands/improve/proposal-envelope.js +0 -31
  342. package/dist/commands/improve/run-context.js +0 -123
  343. package/dist/commands/improve/shared.js +0 -21
  344. package/dist/commands/improve/source-identity.js +0 -28
  345. package/dist/commands/improve/triage.js +0 -96
  346. package/dist/commands/proposal/drain-policies.js +0 -151
  347. package/dist/commands/sources/update-transaction.js +0 -220
  348. package/dist/core/action-contributors.js +0 -28
  349. package/dist/core/config/config-version-shim.js +0 -101
  350. package/dist/core/fs-txn.js +0 -405
  351. package/dist/core/lexical-score.js +0 -25
  352. package/dist/core/maintenance-barrier.js +0 -167
  353. package/dist/execution/executable-identity.js +0 -105
  354. package/dist/execution/guarded-source.js +0 -427
  355. package/dist/indexer/db/graph-db.js +0 -444
  356. package/dist/indexer/graph/graph-boost.js +0 -427
  357. package/dist/indexer/graph/graph-dedup.js +0 -95
  358. package/dist/indexer/graph/graph-extraction.js +0 -1182
  359. package/dist/indexer/search/name-match.js +0 -35
  360. package/dist/indexer/search/ranking-contributors.js +0 -515
  361. package/dist/indexer/search/ranking-types.js +0 -4
  362. package/dist/indexer/walk/project-context.js +0 -192
  363. package/dist/integrations/agent/execution-cascade.js +0 -566
  364. package/dist/integrations/agent/execution-definitions.js +0 -202
  365. package/dist/integrations/agent/execution-lowering.js +0 -841
  366. package/dist/integrations/agent/execution-preparation.js +0 -98
  367. package/dist/integrations/agent/inline-execution.js +0 -74
  368. package/dist/llm/graph-extract.js +0 -872
  369. package/dist/llm/metadata-enhance.js +0 -96
  370. package/dist/registry/create-provider-registry.js +0 -29
  371. package/dist/registry/pinned-request-helper.js +0 -247
  372. package/dist/registry/pinned-transport.js +0 -717
  373. package/dist/sources/providers/index.js +0 -14
  374. package/dist/storage/engines/sqlite-migrations.js +0 -271
  375. package/dist/storage/repositories/canaries-repository.js +0 -107
  376. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  377. package/dist/storage/repositories/registry-cache.js +0 -113
  378. package/dist/tasks/scheduler-sync-preview.js +0 -52
  379. package/dist/tasks/source/task-to-v3.js +0 -507
  380. package/dist/workflows/freeze/resolve-steps.js +0 -86
  381. package/dist/workflows/freeze/source-freeze.js +0 -64
  382. package/dist/workflows/ir/compile.js +0 -321
  383. package/dist/workflows/ir/environment-v4.js +0 -330
  384. package/dist/workflows/ir/freeze-v4.js +0 -153
  385. package/dist/workflows/ir/schema-v4.js +0 -745
  386. package/dist/workflows/ir/schema.js +0 -354
  387. package/dist/workflows/program/schema.js +0 -78
  388. package/dist/workflows/runtime/checkin.js +0 -57
  389. package/dist/workflows/runtime/plan-classifier.js +0 -196
  390. package/dist/workflows/runtime/unit-checkin.js +0 -45
  391. package/dist/workflows/runtime/unit-phases.js +0 -20
  392. package/dist/workflows/schema.js +0 -4
  393. package/dist/workflows/source-ir/compile.js +0 -200
  394. package/dist/workflows/source-ir/program.js +0 -50
  395. package/dist/workflows/source-ir/result.js +0 -26
  396. package/dist/workflows/source-ir/schema.js +0 -786
  397. package/dist/workflows/source-ir/triggers.js +0 -79
  398. package/dist/workflows/source-ir/uses.js +0 -40
  399. package/dist/workflows/validator.js +0 -60
@@ -4,82 +4,103 @@
4
4
  /**
5
5
  * `index.db` FTS5 search + materialization repository.
6
6
  *
7
- * Owns the `entries_fts` full-text query path, per-entry projections, and the
8
- * explicit full recovery rebuild.
7
+ * Owns the `entries_fts` full-text query path, per-entry projections (the FTS
8
+ * row and the `entry_fragments` safe Markdown `akm show` resolves fragment
9
+ * selectors from), and the explicit full recovery rebuild.
10
+ *
11
+ * `entries_fts` is contentless (`content=''`, see index-entry-schema.ts): the
12
+ * text lives once in `entries`, and an FTS row is addressed only by its rowid
13
+ * (`entries.id`). The `COALESCE(f.entry_id, f.rowid)` join below also reads
14
+ * the content-bearing table older releases wrote (where the UNINDEXED column
15
+ * is populated), so a read-only open of a not-yet-migrated index still
16
+ * answers correctly.
9
17
  */
10
18
  import { splitMarkdownFragments } from "../../core/asset/markdown-fragments.js";
11
- import { stableFtsScore } from "../../core/lexical-score.js";
12
19
  import { warn } from "../../core/warn.js";
13
- import { buildLexicalQueryPlan } from "../../indexer/search/fts-query.js";
20
+ import { ftsOrMatch, ftsQueryTokens } from "../../indexer/search/fts-query.js";
14
21
  import { buildSearchFields } from "../../indexer/search/search-fields.js";
15
22
  import { SQLITE_CHUNK_SIZE } from "./index-sql.js";
16
- const INSERT_FTS_SQL = "INSERT INTO entries_fts (entry_id, name, description, tags, hints, content) VALUES (?, ?, ?, ?, ?, ?)";
17
- const INSERT_FRAGMENT_SQL = "INSERT INTO entry_fragments_fts (entry_id, fragment_id, fragment_ordinal, content) VALUES (?, ?, ?, ?)";
23
+ // `entries_fts.rowid = entries.id`, so a per-entry delete is a rowid lookup.
24
+ const INSERT_FTS_SQL = "INSERT INTO entries_fts (rowid, entry_id, name, description, tags, hints, content) VALUES (?, ?, ?, ?, ?, ?, ?)";
18
25
  const ftsMutationStatementsByDb = new WeakMap();
19
26
  function getFtsMutationStatements(db) {
20
27
  const existing = ftsMutationStatementsByDb.get(db);
21
28
  if (existing)
22
29
  return existing;
23
30
  const statements = {
24
- deleteOne: db.prepare("DELETE FROM entries_fts WHERE entry_id = ?"),
31
+ deleteOne: db.prepare("DELETE FROM entries_fts WHERE rowid = ?"),
25
32
  insert: db.prepare(INSERT_FTS_SQL),
26
- deleteFragments: db.prepare("DELETE FROM entry_fragments_fts WHERE entry_id = ?"),
27
33
  upsertFragmentSource: db.prepare("INSERT INTO entry_fragments (entry_id, safe_markdown) VALUES (?, ?) ON CONFLICT(entry_id) DO UPDATE SET safe_markdown = excluded.safe_markdown"),
28
34
  deleteFragmentSource: db.prepare("DELETE FROM entry_fragments WHERE entry_id = ?"),
29
- insertFragment: db.prepare(INSERT_FRAGMENT_SQL),
30
35
  };
31
36
  ftsMutationStatementsByDb.set(db, statements);
32
37
  return statements;
33
38
  }
34
- /** Replace one entry's derived FTS projection inside the caller's transaction. */
39
+ /**
40
+ * Replace one entry's derived FTS row, and its fragment source when the scan
41
+ * read Markdown, inside the caller's transaction.
42
+ */
35
43
  export function replaceFtsEntry(db, entryId, entry, fragmentContent) {
36
44
  const fields = buildSearchFields(entry);
37
45
  const statements = getFtsMutationStatements(db);
38
46
  statements.deleteOne.run(entryId);
39
- statements.insert.run(entryId, fields.name, fields.description, fields.tags, fields.hints, fields.content);
47
+ statements.insert.run(entryId, entryId, fields.name, fields.description, fields.tags, fields.hints, fields.content);
40
48
  if (fragmentContent === undefined) {
41
49
  // Metadata-only re-upserts and re-keys deserialize the public document
42
50
  // without the internal substrate. Leave the persisted source untouched.
43
51
  // A scan that did read Markdown always supplies a value below.
44
52
  return;
45
53
  }
46
- statements.deleteFragments.run(entryId);
47
- statements.deleteFragmentSource.run(entryId);
48
- if (!fragmentContent)
49
- return;
50
- statements.upsertFragmentSource.run(entryId, fragmentContent);
51
- for (const fragment of splitMarkdownFragments(fragmentContent)) {
52
- statements.insertFragment.run(entryId, fragment.fragmentId, fragment.ordinal, fragment.text.toLowerCase());
53
- }
54
+ if (fragmentContent)
55
+ statements.upsertFragmentSource.run(entryId, fragmentContent);
56
+ else
57
+ statements.deleteFragmentSource.run(entryId);
54
58
  }
55
- /** Delete derived FTS projections for canonical entries that are being removed. */
59
+ /** Delete the FTS rows and fragment sources of canonical entries that are being removed. */
56
60
  export function deleteFtsEntries(db, entryIds) {
57
61
  for (let i = 0; i < entryIds.length; i += SQLITE_CHUNK_SIZE) {
58
62
  const chunk = entryIds.slice(i, i + SQLITE_CHUNK_SIZE);
59
63
  const placeholders = chunk.map(() => "?").join(",");
60
- db.prepare(`DELETE FROM entries_fts WHERE entry_id IN (${placeholders})`).run(...chunk);
61
- db.prepare(`DELETE FROM entry_fragments_fts WHERE entry_id IN (${placeholders})`).run(...chunk);
64
+ db.prepare(`DELETE FROM entries_fts WHERE rowid IN (${placeholders})`).run(...chunk);
62
65
  db.prepare(`DELETE FROM entry_fragments WHERE entry_id IN (${placeholders})`).run(...chunk);
63
66
  }
64
67
  }
65
- export function searchFts(db, query, limit, entryType, excludeTypes) {
66
- const plan = buildLexicalQueryPlan(query);
67
- if (!plan.exact)
68
+ /**
69
+ * BM25 per-column weights for `entries_fts(entry_id, name, description, tags,
70
+ * hints, content)`. `entry_id` is UNINDEXED and carries no weight.
71
+ */
72
+ const BM25_COLUMN_WEIGHTS = "0, 1.0, 1.0, 1.0, 1.0, 1.0";
73
+ /**
74
+ * The top `limit` entries matching ANY of the query's tokens
75
+ * (`ftsQueryTokens`), best whole-document BM25 first; equal scores are
76
+ * ordered by `item_ref` so the ranking is deterministic. Fragments play no
77
+ * part: whole-document BM25 beat the fragment population by 0.059 nDCG@10 on
78
+ * the retrieval suite.
79
+ *
80
+ * `entryType` narrows to one type; otherwise `excludeTypes` (#627, the
81
+ * default-hidden types such as `session`) are left out.
82
+ */
83
+ export function searchFts(db, query, limit, entryType, excludeTypes = []) {
84
+ const tokens = ftsQueryTokens(query);
85
+ if (limit <= 0 || tokens.length === 0)
68
86
  return [];
69
- // Try the exact AND query first
70
- const exactResults = runFtsQuery(db, plan.exact, "exact", limit, entryType, excludeTypes);
71
- if (exactResults.length > 0)
72
- return exactResults;
73
- if (plan.exactPrefix) {
74
- const prefixResults = runFtsQuery(db, plan.exactPrefix, "prefix", limit, entryType, excludeTypes);
75
- if (prefixResults.length > 0)
76
- return prefixResults;
77
- }
78
- // One measured relaxation only after both conjunctive forms miss. This is
79
- // still the same FTS table, BM25 weights, candidate collection, and
80
- // downstream ranker — merely an OR candidate query for sentence-shaped
81
- // input whose filler terms prevented a strict hit.
82
- return plan.relaxed ? runFtsQuery(db, plan.relaxed, "relaxed", limit, entryType, excludeTypes) : [];
87
+ const typed = entryType !== undefined && entryType !== "any";
88
+ const filter = typed
89
+ ? "AND e.type = ?"
90
+ : excludeTypes.length > 0
91
+ ? `AND e.type NOT IN (${excludeTypes.map(() => "?").join(", ")})`
92
+ : "";
93
+ const params = [ftsOrMatch(tokens), ...(typed ? [entryType] : excludeTypes), limit];
94
+ // Contentless FTS rows address their entry by rowid; the COALESCE also reads
95
+ // the content-bearing layout older releases wrote.
96
+ return db
97
+ .prepare(`SELECT e.id AS id, e.item_ref AS itemRef
98
+ FROM entries_fts f
99
+ JOIN entries e ON e.id = COALESCE(f.entry_id, f.rowid)
100
+ WHERE entries_fts MATCH ? ${filter}
101
+ ORDER BY bm25(entries_fts, ${BM25_COLUMN_WEIGHTS}), e.item_ref
102
+ LIMIT ?`)
103
+ .all(...params);
83
104
  }
84
105
  /**
85
106
  * Resolve an opaque fragment selector from the indexed safe projection, not
@@ -87,40 +108,14 @@ export function searchFts(db, query, limit, entryType, excludeTypes) {
87
108
  * file edit; the next index refresh atomically publishes the new revision.
88
109
  */
89
110
  export function getIndexedMarkdownFragment(db, itemRef, fragmentId) {
90
- return getIndexedMarkdownFragments(db, [{ itemRef, fragmentId }])[0];
91
- }
92
- /**
93
- * Batch the selected-hit projection read. Search commonly enriches several
94
- * fragment hits at once; reading all indexed-safe parents in chunks avoids an
95
- * N-query loop, while grouping selectors by parent ensures each safe revision
96
- * is split at most once.
97
- */
98
- export function getIndexedMarkdownFragments(db, selections) {
99
- if (selections.length === 0)
100
- return [];
101
- const itemRefs = [...new Set(selections.map((selection) => selection.itemRef))];
102
- const sourceByRef = new Map();
103
- for (let offset = 0; offset < itemRefs.length; offset += SQLITE_CHUNK_SIZE) {
104
- const chunk = itemRefs.slice(offset, offset + SQLITE_CHUNK_SIZE);
105
- const placeholders = chunk.map(() => "?").join(",");
106
- const rows = db
107
- .prepare(`SELECT e.item_ref, s.safe_markdown FROM entry_fragments s JOIN entries e ON e.id = s.entry_id WHERE e.item_ref IN (${placeholders})`)
108
- .all(...chunk);
109
- for (const row of rows)
110
- sourceByRef.set(row.item_ref, row.safe_markdown);
111
- }
112
- const fragmentsByRef = new Map();
113
- for (const [itemRef, safeMarkdown] of sourceByRef) {
114
- fragmentsByRef.set(itemRef, splitMarkdownFragments(safeMarkdown));
115
- }
116
- return selections.map((selection) => {
117
- const safeMarkdown = sourceByRef.get(selection.itemRef);
118
- const fragments = fragmentsByRef.get(selection.itemRef);
119
- if (safeMarkdown === undefined || !fragments)
120
- return undefined;
121
- const fragment = fragments.find((candidate) => candidate.fragmentId === selection.fragmentId || candidate.headingSlug === selection.fragmentId);
122
- return fragment ? materializeIndexedMarkdownFragment(fragment, fragments, safeMarkdown.length) : undefined;
123
- });
111
+ const row = db
112
+ .prepare("SELECT s.safe_markdown FROM entry_fragments s JOIN entries e ON e.id = s.entry_id WHERE e.item_ref = ?")
113
+ .get(itemRef);
114
+ if (row === undefined)
115
+ return undefined;
116
+ const fragments = splitMarkdownFragments(row.safe_markdown);
117
+ const fragment = fragments.find((candidate) => candidate.fragmentId === fragmentId || candidate.headingSlug === fragmentId);
118
+ return fragment ? materializeIndexedMarkdownFragment(fragment, fragments, row.safe_markdown.length) : undefined;
124
119
  }
125
120
  function materializeIndexedMarkdownFragment(fragment, fragments, parentChars) {
126
121
  return {
@@ -136,170 +131,8 @@ function materializeIndexedMarkdownFragment(fragment, fragments, parentChars) {
136
131
  fragments,
137
132
  };
138
133
  }
139
- function runFtsQuery(db, ftsQuery, lexicalMatch, limit, entryType, excludeTypes) {
140
- // Preserve the repository's ordinary limit contract for direct callers.
141
- // The boundary-expansion rule applies only to a positive candidate pool.
142
- if (limit <= 0)
143
- return [];
144
- // #627 — exclude-type clause. Only applies on the untyped ('any') path; an
145
- // explicit include filter (entryType) already narrows to a single type, so
146
- // exclusion is redundant there. An empty list skips the clause entirely
147
- // (never emit `NOT IN ()`, which is a SQL error / always-false).
148
- const excludes = excludeTypes && excludeTypes.length > 0 ? excludeTypes : [];
149
- const candidateBoundaryOffset = Math.max(0, limit - 1);
150
- // The typed and untyped paths differ only by one `type` WHERE clause
151
- // equality vs. an optional NOT IN exclusion) and their parameter order.
152
- // Join on integer entry_id directly (no CAST; we store integer). bm25()
153
- // per-column weights:
154
- // entry_id(0), name(10), description(5), tags(3), hints(2), content(1).
155
- let filterClause;
156
- let params;
157
- if (entryType && entryType !== "any") {
158
- filterClause = "AND e.type = ?";
159
- params = [ftsQuery, entryType, candidateBoundaryOffset];
160
- }
161
- else {
162
- filterClause = excludes.length > 0 ? `AND e.type NOT IN (${excludes.map(() => "?").join(", ")})` : "";
163
- // Param order: MATCH, then the NOT IN values, then the zero-based
164
- // candidate-boundary offset.
165
- params = [ftsQuery, ...excludes, candidateBoundaryOffset];
166
- }
167
- const sql = `
168
- -- Do not make a SQL-only relevance decision inside a tied BM25 boundary:
169
- -- the TypeScript ranker adds exact-name, type, and other contributors
170
- -- afterwards. Materialize BM25 once, locate the Nth score, and admit
171
- -- every row tied with it. This deliberately makes the result set
172
- -- data-bound for a pathological all-tied query; that is the only way to
173
- -- avoid silently dropping a legitimate later ranking winner.
174
- WITH scored AS MATERIALIZED (
175
- -- Keep this materialized set deliberately narrow. document_json can be
176
- -- large, and only rows admitted through the BM25 boundary need it.
177
- SELECT e.id, bm25(entries_fts, 0, 10.0, 5.0, 3.0, 2.0, 1.0) AS bm25Score
178
- FROM entries_fts f
179
- JOIN entries e ON e.id = f.entry_id
180
- WHERE entries_fts MATCH ?
181
- ${filterClause}
182
- ), boundary AS (
183
- SELECT bm25Score
184
- FROM scored
185
- ORDER BY bm25Score
186
- LIMIT 1 OFFSET ?
187
- )
188
- SELECT e.id, e.file_path AS filePath, e.document_json AS documentJson, e.search_text AS searchText,
189
- e.item_ref AS itemRef, e.bundle_id AS bundleId, e.concept_id AS conceptId, e.adapter_id AS adapterId,
190
- scored.bm25Score
191
- FROM scored
192
- JOIN entries e ON e.id = scored.id
193
- WHERE NOT EXISTS (SELECT 1 FROM boundary)
194
- OR scored.bm25Score <= (SELECT bm25Score FROM boundary)
195
- ORDER BY scored.bm25Score, e.id ASC
196
- `;
197
- const rows = db.prepare(sql).all(...params);
198
- const results = materializeRows(rows, lexicalMatch);
199
- // Fragments are a separate, intentionally calibrated evidence population:
200
- // parent FTS remains the sole implementation of metadata/body conjunction.
201
- // A selector is emitted only for one fragment that independently satisfies
202
- // this query. Raw BM25 values are never claimed comparable across tables;
203
- // each is passed through #933's stable mapping before merge.
204
- const fragmentResults = hasFragmentFts(db)
205
- ? runFragmentQuery(db, ftsQuery, lexicalMatch, limit, entryType, excludes)
206
- : [];
207
- return mergeParentAndFragmentResults(results, fragmentResults);
208
- }
209
- function hasFragmentFts(db) {
210
- return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'entry_fragments_fts'").get());
211
- }
212
- function materializeRows(rows, lexicalMatch) {
213
- const results = [];
214
- for (const row of rows) {
215
- let entry;
216
- try {
217
- entry = JSON.parse(row.documentJson);
218
- }
219
- catch {
220
- warn(`[db] searchFts: skipping entry id=${row.id} — corrupt document_json`);
221
- continue;
222
- }
223
- results.push({
224
- id: row.id,
225
- filePath: row.filePath,
226
- entry,
227
- searchText: row.searchText,
228
- bm25Score: row.bm25Score,
229
- itemRef: row.itemRef,
230
- bundleId: row.bundleId,
231
- conceptId: row.conceptId,
232
- adapterId: row.adapterId,
233
- lexicalMatch,
234
- });
235
- }
236
- return results;
237
- }
238
- function runFragmentQuery(db, ftsQuery, lexicalMatch, limit, entryType, excludes) {
239
- const filter = entryType && entryType !== "any"
240
- ? "AND e.type = ?"
241
- : excludes.length
242
- ? `AND e.type NOT IN (${excludes.map(() => "?").join(",")})`
243
- : "";
244
- const filterParams = entryType && entryType !== "any" ? [entryType] : excludes;
245
- const candidateBoundaryOffset = Math.max(0, limit - 1);
246
- // Select the winning child per parent inside SQLite before finding the
247
- // candidate boundary. A document with many matching fragments therefore
248
- // occupies one parent slot, while a boundary tie retains every parent for
249
- // the TypeScript ranker to decide with its non-BM25 contributors. This has
250
- // one FTS query and no OFFSET walk; the returned boundary is intentionally
251
- // data-bound for a pathological all-tied query, just like parent FTS.
252
- const sql = `
253
- WITH matches AS MATERIALIZED (
254
- -- Keep repeated child rows as narrow as parent FTS's scored CTE. The
255
- -- document projection can be large; hydrate it only after the one-child
256
- -- per-parent collapse and BM25 boundary filtering below.
257
- SELECT e.id, f.fragment_id AS fragmentId, f.fragment_ordinal AS fragmentOrdinal,
258
- bm25(entry_fragments_fts) AS bm25Score
259
- FROM entry_fragments_fts f JOIN entries e ON e.id = f.entry_id
260
- WHERE entry_fragments_fts MATCH ? ${filter}
261
- ), ranked AS MATERIALIZED (
262
- SELECT *, ROW_NUMBER() OVER (PARTITION BY id ORDER BY bm25Score ASC, fragmentOrdinal ASC, fragmentId ASC) AS parentRank
263
- FROM matches
264
- ), parents AS MATERIALIZED (
265
- SELECT * FROM ranked WHERE parentRank = 1
266
- ), boundary AS (
267
- SELECT bm25Score FROM parents ORDER BY bm25Score ASC LIMIT 1 OFFSET ?
268
- )
269
- SELECT e.id, e.file_path AS filePath, e.document_json AS documentJson, e.search_text AS searchText,
270
- e.item_ref AS itemRef, e.bundle_id AS bundleId, e.concept_id AS conceptId, e.adapter_id AS adapterId,
271
- parents.fragmentId, parents.bm25Score
272
- FROM parents JOIN entries e ON e.id = parents.id
273
- WHERE NOT EXISTS (SELECT 1 FROM boundary)
274
- OR parents.bm25Score <= (SELECT bm25Score FROM boundary)
275
- ORDER BY parents.bm25Score ASC, parents.id ASC`;
276
- const rows = db.prepare(sql).all(ftsQuery, ...filterParams, candidateBoundaryOffset);
277
- const results = [];
278
- for (const row of rows) {
279
- const [result] = materializeRows([row], lexicalMatch);
280
- if (result) {
281
- results.push({
282
- ...result,
283
- fragmentId: row.fragmentId,
284
- lexicalScore: stableFtsScore(result.bm25Score, "fragment"),
285
- });
286
- }
287
- }
288
- return results;
289
- }
290
- function mergeParentAndFragmentResults(parents, fragments) {
291
- const winners = new Map();
292
- for (const parent of parents)
293
- winners.set(parent.id, { ...parent, lexicalScore: stableFtsScore(parent.bm25Score) });
294
- for (const fragment of fragments) {
295
- const existing = winners.get(fragment.id);
296
- if (!existing || (fragment.lexicalScore ?? 0) > (existing.lexicalScore ?? 0))
297
- winners.set(fragment.id, fragment);
298
- }
299
- return [...winners.values()].sort((left, right) => (right.lexicalScore ?? 0) - (left.lexicalScore ?? 0) || left.id - right.id);
300
- }
301
134
  /**
302
- * Explicitly rebuild the complete FTS5 projection from canonical entries.
135
+ * Explicitly rebuild `entries_fts` from canonical entries.
303
136
  * Ordinary entry mutations do not call this: `upsertEntry` and the delete
304
137
  * operations publish their FTS state in the same transaction as `entries`.
305
138
  * This remains a recovery/schema-verification primitive for regenerable
@@ -311,29 +144,26 @@ function mergeParentAndFragmentResults(parents, fragments) {
311
144
  export function rebuildFts(db) {
312
145
  db.transaction(() => {
313
146
  db.exec("DELETE FROM entries_fts");
314
- db.exec("DELETE FROM entry_fragments_fts");
315
- const rows = db
316
- .prepare("SELECT e.id, e.document_json, f.safe_markdown FROM entries e LEFT JOIN entry_fragments f ON f.entry_id = e.id")
317
- .all();
147
+ // Keyset pages, so a large index is never held in memory at once.
148
+ const page = db.prepare("SELECT id, document_json FROM entries WHERE id > ? ORDER BY id LIMIT 500");
318
149
  const insertStmt = db.prepare(INSERT_FTS_SQL);
319
- const fragmentStmt = db.prepare(INSERT_FRAGMENT_SQL);
320
150
  let skipped = 0;
321
- for (const row of rows) {
322
- let entry;
323
- let fields;
324
- try {
325
- entry = JSON.parse(row.document_json);
326
- fields = buildSearchFields(entry);
327
- }
328
- catch {
329
- skipped++;
330
- continue;
331
- }
332
- insertStmt.run(row.id, fields.name, fields.description, fields.tags, fields.hints, fields.content);
333
- if (row.safe_markdown) {
334
- for (const fragment of splitMarkdownFragments(row.safe_markdown)) {
335
- fragmentStmt.run(row.id, fragment.fragmentId, fragment.ordinal, fragment.text.toLowerCase());
151
+ let afterId = -1;
152
+ for (;;) {
153
+ const rows = page.all(afterId);
154
+ if (rows.length === 0)
155
+ break;
156
+ afterId = rows[rows.length - 1].id;
157
+ for (const row of rows) {
158
+ let fields;
159
+ try {
160
+ fields = buildSearchFields(JSON.parse(row.document_json));
161
+ }
162
+ catch {
163
+ skipped++;
164
+ continue;
336
165
  }
166
+ insertStmt.run(row.id, row.id, fields.name, fields.description, fields.tags, fields.hints, fields.content);
337
167
  }
338
168
  }
339
169
  if (skipped > 0) {
@@ -0,0 +1,143 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * `index.db` declared-links repository (#935): owns every SQL statement
6
+ * against `asset_links`.
7
+ *
8
+ * A row belongs to the entry that declares the link and is written, replaced
9
+ * and deleted with that entry (`upsertEntry`, `deleteRelatedRows`). A target
10
+ * is stored as `dst_bundle` (NULL for a short ref, meaning the declaring
11
+ * entry's own bundle) plus `dst_concept`; whether it exists is a join on
12
+ * `entries.item_ref` at read time, so a target indexed later resolves with
13
+ * no rewrite of its citer and a bundle rename carries short refs along.
14
+ *
15
+ * A memory target whose own file is gone resolves to its `.derived` child, the
16
+ * reachability rule lint applies (#882): consolidation keeps the distilled
17
+ * child after the parent is pruned.
18
+ */
19
+ import { declaredLinks } from "../../indexer/links/declared-links.js";
20
+ import { tableExists } from "./index-entry-schema.js";
21
+ import { SQLITE_CHUNK_SIZE } from "./index-sql.js";
22
+ const statementsByDb = new WeakMap();
23
+ function statements(db) {
24
+ const existing = statementsByDb.get(db);
25
+ if (existing)
26
+ return existing;
27
+ const created = {
28
+ deleteForEntry: db.prepare("DELETE FROM asset_links WHERE entry_id = ?"),
29
+ insert: db.prepare("INSERT INTO asset_links (entry_id, ord, kind, raw, dst_bundle, dst_concept) VALUES (?, ?, ?, ?, ?, ?)"),
30
+ };
31
+ statementsByDb.set(db, created);
32
+ return created;
33
+ }
34
+ /** Replace one entry's declared links with the ones its document names, inside the caller's transaction. */
35
+ export function replaceEntryLinks(db, entryId, document, owner) {
36
+ const { deleteForEntry, insert } = statements(db);
37
+ deleteForEntry.run(entryId);
38
+ declaredLinks(document, owner).forEach((link, ord) => {
39
+ insert.run(entryId, ord, link.kind, link.raw, link.bundle ?? null, link.conceptId);
40
+ });
41
+ }
42
+ /** Delete the declared links of entries that are being removed. */
43
+ export function deleteEntryLinks(db, entryIds) {
44
+ for (let i = 0; i < entryIds.length; i += SQLITE_CHUNK_SIZE) {
45
+ const chunk = entryIds.slice(i, i + SQLITE_CHUNK_SIZE);
46
+ db.prepare(`DELETE FROM asset_links WHERE entry_id IN (${chunk.map(() => "?").join(",")})`).run(...chunk);
47
+ }
48
+ }
49
+ /**
50
+ * Derive every entry's links from its stored `document_json`, replacing
51
+ * whatever the table held — the in-place migration to layout 26, which needs
52
+ * no file read. An entry whose JSON does not parse keeps no links until it is
53
+ * next indexed.
54
+ */
55
+ export function rebuildAllEntryLinks(db) {
56
+ db.exec("DELETE FROM asset_links");
57
+ const page = db.prepare("SELECT id, bundle_id, concept_id, document_json FROM entries WHERE id > ? ORDER BY id LIMIT 500");
58
+ let afterId = -1;
59
+ for (;;) {
60
+ const rows = page.all(afterId);
61
+ if (rows.length === 0)
62
+ break;
63
+ afterId = rows[rows.length - 1].id;
64
+ for (const row of rows) {
65
+ let document;
66
+ try {
67
+ document = JSON.parse(row.document_json);
68
+ }
69
+ catch {
70
+ continue;
71
+ }
72
+ replaceEntryLinks(db, row.id, document, { bundleId: row.bundle_id, conceptId: row.concept_id });
73
+ }
74
+ }
75
+ }
76
+ /**
77
+ * The id of the entry a link row resolves to, or NULL: the exact target, else
78
+ * (for a memory) its `.derived` child, never the declaring entry itself.
79
+ * Expects `l` (the link) and `o` (its owner) in scope.
80
+ */
81
+ const TARGET_ID_SQL = `COALESCE(
82
+ (SELECT id FROM entries WHERE item_ref = COALESCE(l.dst_bundle, o.bundle_id) || '//' || l.dst_concept),
83
+ (SELECT id FROM entries
84
+ WHERE item_ref = COALESCE(l.dst_bundle, o.bundle_id) || '//' || l.dst_concept || '.derived'
85
+ AND substr(l.dst_concept, 1, 9) = 'memories/' AND id <> o.id))`;
86
+ /**
87
+ * The declared links of the entry `itemRef`, both ways: `outgoing` in stored
88
+ * order (unresolved targets carry only `raw`), `incoming` from every other
89
+ * entry that names it, ordered by kind then source. An index that predates
90
+ * the table (an older layout served as-is) has none.
91
+ */
92
+ export function readEntryLinks(db, itemRef) {
93
+ if (!tableExists(db, "asset_links"))
94
+ return { outgoing: [], incoming: [] };
95
+ const owner = db.prepare("SELECT id, bundle_id, concept_id FROM entries WHERE item_ref = ?").get(itemRef);
96
+ if (!owner)
97
+ return { outgoing: [], incoming: [] };
98
+ const outgoing = db
99
+ .prepare(`SELECT l.kind AS kind, l.raw AS raw, t.bundle_id AS bundleId, t.concept_id AS conceptId, t.type AS type
100
+ FROM asset_links l
101
+ JOIN entries o ON o.id = l.entry_id
102
+ LEFT JOIN entries t ON t.id = ${TARGET_ID_SQL}
103
+ WHERE l.entry_id = ?
104
+ ORDER BY l.ord`)
105
+ .all(owner.id);
106
+ // A `.derived` memory also receives the links that name its parent when the
107
+ // parent is not indexed (the resolution rule above).
108
+ const parentConcept = owner.concept_id.startsWith("memories/") && owner.concept_id.endsWith(".derived")
109
+ ? owner.concept_id.slice(0, -".derived".length)
110
+ : owner.concept_id;
111
+ const incoming = db
112
+ .prepare(`SELECT l.kind AS kind, o.bundle_id AS bundleId, o.concept_id AS conceptId, o.type AS type
113
+ FROM asset_links l
114
+ JOIN entries o ON o.id = l.entry_id
115
+ WHERE l.dst_concept IN (?, ?) AND COALESCE(l.dst_bundle, o.bundle_id) = ? AND o.id <> ?
116
+ AND ${TARGET_ID_SQL} = ?
117
+ ORDER BY l.kind, o.item_ref`)
118
+ .all(owner.concept_id, parentConcept, owner.bundle_id, owner.id, owner.id);
119
+ return {
120
+ outgoing: outgoing.map((row) => row.conceptId === null
121
+ ? { kind: row.kind, raw: row.raw }
122
+ : {
123
+ kind: row.kind,
124
+ bundleId: row.bundleId ?? undefined,
125
+ conceptId: row.conceptId,
126
+ type: row.type ?? undefined,
127
+ }),
128
+ incoming,
129
+ };
130
+ }
131
+ /** Stored links per kind with how many name a target that is not indexed; empty when the index has none. */
132
+ export function countLinksByKind(db) {
133
+ if (!tableExists(db, "asset_links"))
134
+ return {};
135
+ const rows = db
136
+ .prepare(`SELECT l.kind AS kind, COUNT(*) AS total, SUM(${TARGET_ID_SQL} IS NULL) AS unresolved
137
+ FROM asset_links l
138
+ JOIN entries o ON o.id = l.entry_id
139
+ GROUP BY l.kind
140
+ ORDER BY l.kind`)
141
+ .all();
142
+ return Object.fromEntries(rows.map((row) => [row.kind, { total: row.total, unresolved: row.unresolved }]));
143
+ }
@@ -4,8 +4,9 @@
4
4
  /**
5
5
  * `index.db` LLM enrichment-cache repository.
6
6
  *
7
- * Owns the raw SQL for `llm_enrichment_cache` — the body-hash-keyed cache that
8
- * lets `akm index --enrich` skip the LLM call when a file's body is unchanged.
7
+ * Owns the raw SQL for `llm_enrichment_cache` — the body-hash-keyed cache the
8
+ * graph-extraction and memory-inference passes use to skip an LLM call when
9
+ * a file's body is unchanged.
9
10
  */
10
11
  import { sha256Hex } from "../../runtime.js";
11
12
  import { SQLITE_CHUNK_SIZE } from "./index-sql.js";
@@ -17,7 +18,7 @@ import { SQLITE_CHUNK_SIZE } from "./index-sql.js";
17
18
  * cached). In both cases the caller should invoke the LLM and write a new
18
19
  * cache entry.
19
20
  */
20
- export function getLlmCacheEntry(db, assetRef, currentBodyHash, cacheVariant = "") {
21
+ export function getLlmCacheEntry(db, assetRef, currentBodyHash, cacheVariant) {
21
22
  const row = db
22
23
  .prepare("SELECT asset_ref, cache_variant, body_hash, result_json, updated_at FROM llm_enrichment_cache WHERE asset_ref = ? AND cache_variant = ?")
23
24
  .get(assetRef, cacheVariant);
@@ -43,7 +44,7 @@ export function getLlmCacheEntry(db, assetRef, currentBodyHash, cacheVariant = "
43
44
  * compare `entry.bodyHash` against the current body hash themselves. This lets
44
45
  * the batch path issue one DB query per chunk instead of one per file.
45
46
  */
46
- export function getLlmCacheEntriesByRefs(db, refs, cacheVariant = "") {
47
+ export function getLlmCacheEntriesByRefs(db, refs, cacheVariant) {
47
48
  const result = new Map();
48
49
  if (refs.length === 0)
49
50
  return result;
@@ -69,7 +70,7 @@ export function getLlmCacheEntriesByRefs(db, refs, cacheVariant = "") {
69
70
  /**
70
71
  * Insert or update a cached LLM result for the given asset_ref.
71
72
  */
72
- export function upsertLlmCacheEntry(db, assetRef, bodyHash, resultJson, cacheVariant = "") {
73
+ export function upsertLlmCacheEntry(db, assetRef, bodyHash, resultJson, cacheVariant) {
73
74
  db.prepare(`INSERT INTO llm_enrichment_cache (asset_ref, cache_variant, body_hash, result_json, updated_at)
74
75
  VALUES (?, ?, ?, ?, ?)
75
76
  ON CONFLICT(asset_ref, cache_variant) DO UPDATE SET
@@ -82,15 +83,12 @@ export function upsertLlmCacheEntry(db, assetRef, bodyHash, resultJson, cacheVar
82
83
  * `entries` table. Should be called during the cleanup phase of each index
83
84
  * run to prevent the cache from growing unboundedly as assets are removed.
84
85
  *
85
- * Graph/memory cache refs are absolute file paths, while metadata-enrichment
86
- * refs use canonical `item_ref`; preserve a cache row that matches either
87
- * current identity.
86
+ * Cache refs are absolute file paths (memory inference).
88
87
  */
89
88
  export function clearStaleCacheEntries(db) {
90
89
  db.exec(`
91
90
  DELETE FROM llm_enrichment_cache
92
91
  WHERE asset_ref NOT IN (SELECT file_path FROM entries)
93
- AND asset_ref NOT IN (SELECT item_ref FROM entries)
94
92
  `);
95
93
  }
96
94
  /**
@@ -23,7 +23,7 @@ export function deleteMeta(db, key) {
23
23
  // ── Per-directory index state ───────────────────────────────────────────────
24
24
  export function getIndexDirState(db, dirPath) {
25
25
  const row = db
26
- .prepare("SELECT dir_path, file_set_hash, file_mtime_max_ms, reason, updated_at, row_count FROM index_dir_state WHERE dir_path = ?")
26
+ .prepare("SELECT dir_path, file_set_hash, file_mtime_max_ms, reason, updated_at, row_count, index_variant FROM index_dir_state WHERE dir_path = ?")
27
27
  .get(dirPath);
28
28
  if (!row)
29
29
  return undefined;
@@ -34,17 +34,19 @@ export function getIndexDirState(db, dirPath) {
34
34
  reason: row.reason,
35
35
  updatedAt: row.updated_at,
36
36
  rowCount: row.row_count ?? undefined,
37
+ indexVariant: row.index_variant ?? undefined,
37
38
  };
38
39
  }
39
40
  export function upsertIndexDirState(db, state) {
40
- db.prepare(`INSERT INTO index_dir_state (dir_path, file_set_hash, file_mtime_max_ms, reason, updated_at, row_count)
41
- VALUES (?, ?, ?, ?, ?, ?)
41
+ db.prepare(`INSERT INTO index_dir_state (dir_path, file_set_hash, file_mtime_max_ms, reason, updated_at, row_count, index_variant)
42
+ VALUES (?, ?, ?, ?, ?, ?, ?)
42
43
  ON CONFLICT(dir_path) DO UPDATE SET
43
44
  file_set_hash = excluded.file_set_hash,
44
45
  file_mtime_max_ms = excluded.file_mtime_max_ms,
45
46
  reason = excluded.reason,
46
47
  updated_at = excluded.updated_at,
47
- row_count = excluded.row_count`).run(state.dirPath, state.fileSetHash, state.fileMtimeMaxMs, state.reason, new Date().toISOString(), state.rowCount ?? null);
48
+ row_count = excluded.row_count,
49
+ index_variant = excluded.index_variant`).run(state.dirPath, state.fileSetHash, state.fileMtimeMaxMs, state.reason, new Date().toISOString(), state.rowCount ?? null, state.indexVariant ?? null);
48
50
  }
49
51
  export function deleteIndexDirState(db, dirPath) {
50
52
  db.prepare("DELETE FROM index_dir_state WHERE dir_path = ?").run(dirPath);