akm-cli 0.9.16 → 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 (403) hide show
  1. package/CHANGELOG.md +2101 -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/consolidate-system.md +4 -11
  15. package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
  17. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
  18. package/dist/assets/templates/html/health.html +3 -5
  19. package/dist/cli/retired-commands.js +1 -1
  20. package/dist/cli/shared.js +6 -2
  21. package/dist/cli/unknown-flags.js +24 -1
  22. package/dist/cli.js +68 -10
  23. package/dist/commands/agent/agent-dispatch.js +1 -1
  24. package/dist/commands/command/command-execution.js +24 -62
  25. package/dist/commands/feedback-cli.js +0 -1
  26. package/dist/commands/health/accept-rate.js +6 -0
  27. package/dist/commands/health/archive-usage.js +92 -0
  28. package/dist/commands/health/checks.js +83 -74
  29. package/dist/commands/health/config-skew.js +38 -0
  30. package/dist/commands/health/data-dir-usage.js +25 -13
  31. package/dist/commands/health/egress.js +54 -0
  32. package/dist/commands/health/html-report.js +1 -42
  33. package/dist/commands/health/improve-metrics.js +136 -591
  34. package/dist/commands/health/md-report.js +1 -6
  35. package/dist/commands/health/plugin-staleness.js +53 -3
  36. package/dist/commands/health/renderers.js +12 -4
  37. package/dist/commands/health/report-view-model.js +14 -120
  38. package/dist/commands/health/types-improve.js +4 -19
  39. package/dist/commands/health/windows.js +64 -74
  40. package/dist/commands/health.js +145 -143
  41. package/dist/commands/improve/consolidate/chunking.js +26 -117
  42. package/dist/commands/improve/consolidate/continuity-check.js +137 -0
  43. package/dist/commands/improve/consolidate/pair-pass.js +791 -0
  44. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  45. package/dist/commands/improve/consolidate.js +589 -1127
  46. package/dist/commands/improve/content-hash.js +16 -24
  47. package/dist/commands/improve/distill/content-repair.js +18 -100
  48. package/dist/commands/improve/distill-guards.js +20 -81
  49. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  50. package/dist/commands/improve/distill.js +608 -1041
  51. package/dist/commands/improve/eligibility.js +126 -390
  52. package/dist/commands/improve/execution.js +8 -10
  53. package/dist/commands/improve/extract-prompt.js +1 -2
  54. package/dist/commands/improve/extract.js +487 -1046
  55. package/dist/commands/improve/feedback-valence.js +0 -25
  56. package/dist/commands/improve/improve-cli.js +75 -169
  57. package/dist/commands/improve/improve-result-file.js +10 -66
  58. package/dist/commands/improve/improve-strategies.js +52 -4
  59. package/dist/commands/improve/improve-usage-report.js +18 -64
  60. package/dist/commands/improve/improve.js +480 -1074
  61. package/dist/commands/improve/ledger.js +119 -0
  62. package/dist/commands/improve/locks.js +2 -8
  63. package/dist/commands/improve/loop-stages.js +415 -1073
  64. package/dist/commands/improve/memory/derived-ref.js +12 -77
  65. package/dist/commands/improve/memory/memory-belief.js +16 -118
  66. package/dist/commands/improve/memory/memory-improve.js +266 -14
  67. package/dist/commands/improve/outcome-loop.js +28 -156
  68. package/dist/commands/improve/planner.js +5 -15
  69. package/dist/commands/improve/preparation.js +779 -2319
  70. package/dist/commands/improve/proactive-maintenance.js +34 -101
  71. package/dist/commands/improve/reflect-noise.js +104 -280
  72. package/dist/commands/improve/reflect.js +642 -1353
  73. package/dist/commands/improve/retrieval-gate.js +127 -0
  74. package/dist/commands/improve/retrieval-scope.js +92 -0
  75. package/dist/commands/improve/salience.js +41 -240
  76. package/dist/commands/improve/session-asset.js +19 -100
  77. package/dist/commands/improve/stage.js +322 -0
  78. package/dist/commands/lint/base-linter.js +37 -15
  79. package/dist/commands/proposal/drain.js +261 -578
  80. package/dist/commands/proposal/proposal-cli.js +19 -20
  81. package/dist/commands/proposal/proposal-types.js +31 -24
  82. package/dist/commands/proposal/proposal.js +38 -8
  83. package/dist/commands/proposal/propose.js +134 -160
  84. package/dist/commands/proposal/repository.js +1097 -1394
  85. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  86. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  87. package/dist/commands/proposal/validators/proposals.js +22 -89
  88. package/dist/commands/read/curate.js +105 -462
  89. package/dist/commands/read/knowledge.js +3 -2
  90. package/dist/commands/read/search-cli.js +16 -33
  91. package/dist/commands/read/search.js +17 -23
  92. package/dist/commands/read/show.js +57 -108
  93. package/dist/commands/sources/bundle-cli.js +25 -2
  94. package/dist/commands/sources/bundle-config-ops.js +4 -0
  95. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  96. package/dist/commands/sources/info.js +127 -29
  97. package/dist/commands/sources/installed-stashes.js +197 -746
  98. package/dist/commands/sources/schema-repair.js +98 -129
  99. package/dist/commands/sources/source-add.js +62 -12
  100. package/dist/commands/sources/source-manage.js +9 -2
  101. package/dist/commands/sources/stash-cli.js +24 -4
  102. package/dist/commands/tasks/explain.js +10 -13
  103. package/dist/commands/tasks/tasks-cli.js +12 -13
  104. package/dist/commands/tasks/tasks.js +350 -936
  105. package/dist/commands/tasks/validate.js +26 -24
  106. package/dist/commands/workflow/plan.js +22 -29
  107. package/dist/commands/workflow-cli.js +4 -4
  108. package/dist/core/adapter/adapters/akm-adapter.js +2 -1
  109. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  110. package/dist/core/adapter/adapters/akm-metadata.js +42 -12
  111. package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
  112. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  113. package/dist/core/adapter/execution-source.js +17 -29
  114. package/dist/core/asset/asset-placement.js +4 -13
  115. package/dist/core/asset/frontmatter.js +106 -1
  116. package/dist/core/asset/resolve-ref.js +1 -1
  117. package/dist/core/bundle-id.js +42 -5
  118. package/dist/core/bundle-rename.js +285 -0
  119. package/dist/core/config/config-io.js +1 -2
  120. package/dist/core/config/config-schema.js +9 -34
  121. package/dist/core/config/config-walker.js +1 -1
  122. package/dist/core/config/config.js +184 -111
  123. package/dist/core/config/engine-semantics.js +0 -2
  124. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  125. package/dist/core/config/schema/embedding.js +20 -5
  126. package/dist/core/config/schema/engines.js +5 -0
  127. package/dist/core/config/schema/execution.js +1 -1
  128. package/dist/core/config/schema/experimental.js +1 -1
  129. package/dist/core/config/schema/improve-processes.js +54 -125
  130. package/dist/core/config/schema/improve.js +4 -42
  131. package/dist/core/config/schema/index-config.js +9 -48
  132. package/dist/core/config/schema/scheduler.js +12 -12
  133. package/dist/core/config/schema/search.js +6 -22
  134. package/dist/core/env-secret-ref.js +0 -1
  135. package/dist/core/errors.js +8 -9
  136. package/dist/core/file-change.js +13 -5
  137. package/dist/core/file-lock.js +76 -173
  138. package/dist/core/improve-result.js +35 -7
  139. package/dist/core/improve-types.js +0 -1
  140. package/dist/core/logs-db.js +2 -2
  141. package/dist/core/loopback.js +7 -12
  142. package/dist/core/non-task-input.js +20 -0
  143. package/dist/core/parse.js +13 -16
  144. package/dist/core/paths.js +0 -24
  145. package/dist/core/redaction.js +109 -2
  146. package/dist/core/run-lock.js +2 -5
  147. package/dist/core/spawn-env.js +1 -1
  148. package/dist/core/state/migrations.js +123 -61
  149. package/dist/core/state-db-scope.js +2 -4
  150. package/dist/core/state-db.js +126 -692
  151. package/dist/core/time.js +0 -20
  152. package/dist/core/type-presentation.js +1 -9
  153. package/dist/core/write-source.js +294 -1005
  154. package/dist/execution/input-contract.js +1 -1
  155. package/dist/execution/resolved-request.js +135 -689
  156. package/dist/execution/source.js +63 -257
  157. package/dist/execution/target-ref.js +1 -1
  158. package/dist/indexer/bundle-identity-guard.js +2 -2
  159. package/dist/indexer/db/llm-cache.js +2 -2
  160. package/dist/indexer/ensure-index.js +77 -73
  161. package/dist/indexer/index-rebuild-lock.js +3 -11
  162. package/dist/indexer/index-writer-lock.js +8 -17
  163. package/dist/indexer/index-written-assets.js +141 -154
  164. package/dist/indexer/indexer.js +400 -1124
  165. package/dist/indexer/links/declared-links.js +90 -0
  166. package/dist/indexer/materialize-embeddings.js +60 -397
  167. package/dist/indexer/passes/memory-inference.js +96 -90
  168. package/dist/indexer/passes/metadata.js +132 -219
  169. package/dist/indexer/read-preflight.js +0 -7
  170. package/dist/indexer/scan/doc-to-entry.js +2 -3
  171. package/dist/indexer/scan/drain-dir.js +1 -1
  172. package/dist/indexer/search/db-search.js +190 -590
  173. package/dist/indexer/search/fts-query.js +30 -41
  174. package/dist/indexer/search/ranking.js +28 -154
  175. package/dist/indexer/search/search-attribution.js +12 -32
  176. package/dist/indexer/search/search-fields.js +11 -15
  177. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  178. package/dist/indexer/search/search-source.js +1 -4
  179. package/dist/indexer/usage/usage-events.js +36 -7
  180. package/dist/indexer/walk/walker.js +3 -4
  181. package/dist/integrations/agent/engine-fallback.js +23 -40
  182. package/dist/integrations/agent/engine-resolution.js +93 -183
  183. package/dist/integrations/agent/execution.js +507 -0
  184. package/dist/integrations/agent/model-map.js +28 -156
  185. package/dist/integrations/agent/request-lowering.js +66 -141
  186. package/dist/integrations/agent/runner-dispatch.js +143 -321
  187. package/dist/integrations/agent/runner.js +54 -14
  188. package/dist/integrations/lockfile.js +53 -101
  189. package/dist/llm/client.js +18 -6
  190. package/dist/llm/embedders/deterministic.js +2 -3
  191. package/dist/llm/embedders/profile.js +71 -0
  192. package/dist/llm/embedders/remote.js +11 -17
  193. package/dist/llm/feature-gate.js +0 -8
  194. package/dist/llm/index-passes.js +3 -5
  195. package/dist/llm/memory-infer.js +1 -2
  196. package/dist/llm/structured-call.js +5 -24
  197. package/dist/output/generic-render.js +23 -11
  198. package/dist/output/html-render.js +13 -10
  199. package/dist/output/render-registry.js +3 -32
  200. package/dist/output/shapes/helpers.js +25 -38
  201. package/dist/output/shapes/passthrough.js +1 -9
  202. package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
  203. package/dist/output/text/command-format.js +69 -31
  204. package/dist/output/text/helpers.js +1 -1
  205. package/dist/output/text/migrate.js +5 -14
  206. package/dist/output/text/proposal-format.js +48 -3
  207. package/dist/output/text/show-format.js +13 -17
  208. package/dist/output/text/workflow-format.js +0 -32
  209. package/dist/output/text.js +2 -0
  210. package/dist/registry/factory.js +4 -19
  211. package/dist/registry/network.js +66 -220
  212. package/dist/registry/providers/index.js +0 -2
  213. package/dist/registry/providers/skills-sh.js +3 -14
  214. package/dist/registry/providers/static-index.js +24 -26
  215. package/dist/registry/resolve.js +55 -131
  216. package/dist/scripts/akm-migrate-node.js +42948 -92369
  217. package/dist/scripts/akm-migrate.js +42935 -92354
  218. package/dist/setup/registry-stash-loader.js +4 -13
  219. package/dist/setup/semantic-assets.js +3 -44
  220. package/dist/setup/setup.js +1 -1
  221. package/dist/setup/steps/connection.js +5 -6
  222. package/dist/setup/steps/platforms.js +2 -2
  223. package/dist/setup/steps/tasks.js +25 -15
  224. package/dist/sources/provider-factory.js +17 -18
  225. package/dist/sources/providers/filesystem.js +2 -3
  226. package/dist/sources/providers/git-install.js +7 -1
  227. package/dist/sources/providers/git-provider.js +0 -3
  228. package/dist/sources/providers/git-stash.js +83 -21
  229. package/dist/sources/providers/npm.js +2 -4
  230. package/dist/sources/providers/provider-utils.js +5 -10
  231. package/dist/sources/providers/website.js +0 -2
  232. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  233. package/dist/sources/website-url.js +2 -2
  234. package/dist/storage/database.js +9 -35
  235. package/dist/storage/repositories/improve-ledger-repository.js +209 -0
  236. package/dist/storage/repositories/index-connection.js +39 -72
  237. package/dist/storage/repositories/index-entries-repository.js +131 -129
  238. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  239. package/dist/storage/repositories/index-entry-schema.js +101 -268
  240. package/dist/storage/repositories/index-fts-repository.js +86 -256
  241. package/dist/storage/repositories/index-links-repository.js +143 -0
  242. package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
  243. package/dist/storage/repositories/index-meta-repository.js +6 -4
  244. package/dist/storage/repositories/index-schema.js +257 -325
  245. package/dist/storage/repositories/index-utility-repository.js +8 -29
  246. package/dist/storage/repositories/index-vec-repository.js +133 -414
  247. package/dist/storage/repositories/outcome-repository.js +2 -1
  248. package/dist/storage/repositories/proposals-repository.js +104 -1
  249. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  250. package/dist/storage/repositories/salience-repository.js +1 -19
  251. package/dist/storage/repositories/task-history-repository.js +26 -4
  252. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  253. package/dist/storage/sqlite-migrations.js +136 -0
  254. package/dist/storage/sqlite-pragmas.js +11 -9
  255. package/dist/storage/sqlite-transaction.js +170 -0
  256. package/dist/storage/state-db-integrity.js +130 -0
  257. package/dist/tasks/activation-config.js +134 -62
  258. package/dist/tasks/backends/cron.js +191 -302
  259. package/dist/tasks/backends/exec-utils.js +2 -5
  260. package/dist/tasks/backends/launchd.js +141 -748
  261. package/dist/tasks/backends/schtasks.js +119 -623
  262. package/dist/tasks/prepare/prepare-support.js +5 -15
  263. package/dist/tasks/prepare/prepare.js +0 -2
  264. package/dist/tasks/resolve-akm-bin.js +20 -79
  265. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  266. package/dist/tasks/run/load-task.js +1 -1
  267. package/dist/tasks/scheduler-binding.js +20 -238
  268. package/dist/tasks/scheduler-invocation.js +136 -244
  269. package/dist/tasks/scheduler-lock.js +53 -0
  270. package/dist/tasks/scheduler-sync.js +368 -679
  271. package/dist/tasks/source/parse-task-source.js +55 -9
  272. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  273. package/dist/tasks/source/task-to-v4.js +464 -88
  274. package/dist/workflows/authoring/authoring.js +3 -12
  275. package/dist/workflows/compile.js +211 -0
  276. package/dist/workflows/concurrency-policy.js +13 -74
  277. package/dist/workflows/exec/child-invocation.js +3 -17
  278. package/dist/workflows/exec/child-workflow.js +32 -141
  279. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  280. package/dist/workflows/exec/environment.js +98 -0
  281. package/dist/workflows/exec/exec-unit.js +33 -140
  282. package/dist/workflows/exec/frozen-judge.js +7 -59
  283. package/dist/workflows/exec/native-executor.js +82 -341
  284. package/dist/workflows/exec/param-secrets.js +29 -47
  285. package/dist/workflows/exec/run-workflow.js +154 -387
  286. package/dist/workflows/exec/scheduler.js +9 -36
  287. package/dist/workflows/exec/step-work.js +127 -430
  288. package/dist/workflows/exec/unit-dispatch.js +11 -63
  289. package/dist/workflows/exec/unit-writer.js +8 -52
  290. package/dist/workflows/exec/worktree.js +39 -273
  291. package/dist/workflows/freeze/child-output-references.js +4 -15
  292. package/dist/workflows/freeze/environment.js +99 -92
  293. package/dist/workflows/freeze/freeze.js +172 -0
  294. package/dist/workflows/freeze/step-values.js +19 -21
  295. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  296. package/dist/workflows/freeze/targets/command.js +10 -33
  297. package/dist/workflows/freeze/targets/script.js +5 -12
  298. package/dist/workflows/freeze/targets/shell.js +3 -6
  299. package/dist/workflows/freeze/targets/task.js +25 -80
  300. package/dist/workflows/freeze/task-bindings.js +20 -67
  301. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  302. package/dist/workflows/ir/params.js +6 -51
  303. package/dist/workflows/ir/plan-hash.js +2 -34
  304. package/dist/workflows/parser.js +140 -43
  305. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  306. package/dist/workflows/renderer.js +36 -69
  307. package/dist/workflows/resource-limits.js +12 -120
  308. package/dist/workflows/runtime/agent-identity.js +8 -40
  309. package/dist/workflows/runtime/run-outputs.js +3 -6
  310. package/dist/workflows/runtime/run-plan.js +316 -0
  311. package/dist/workflows/runtime/runs.js +48 -200
  312. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  313. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  314. package/dist/workflows/validate-summary.js +2 -7
  315. package/docs/integration/bundling-akm.md +49 -42
  316. package/docs/migration/README.md +1 -0
  317. package/docs/migration/release-notes/0.9.17.md +43 -0
  318. package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
  319. package/docs/reference/cli.md +232 -135
  320. package/docs/reference/configuration.md +71 -57
  321. package/docs/reference/data-and-telemetry.md +20 -21
  322. package/docs/reference/tasks.md +105 -39
  323. package/docs/reference/workflow-schema.md +14 -18
  324. package/docs/reference/workflows.md +6 -9
  325. package/package.json +1 -1
  326. package/schemas/akm-config.json +115 -738
  327. package/schemas/akm-workflow.json +1 -0
  328. package/dist/assets/improve-strategies/graph-refresh.json +0 -15
  329. package/dist/assets/prompts/contradiction-judge.md +0 -33
  330. package/dist/assets/prompts/graph-extract-system.md +0 -1
  331. package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
  332. package/dist/assets/prompts/metadata-enhance-system.md +0 -1
  333. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
  334. package/dist/commands/health/advisories.js +0 -150
  335. package/dist/commands/health/metrics.js +0 -329
  336. package/dist/commands/health/surfaces.js +0 -102
  337. package/dist/commands/improve/anti-collapse.js +0 -83
  338. package/dist/commands/improve/collapse-detector.js +0 -432
  339. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  340. package/dist/commands/improve/consolidate/merge.js +0 -149
  341. package/dist/commands/improve/distill/promote-memory.js +0 -291
  342. package/dist/commands/improve/distill/quality-gate.js +0 -337
  343. package/dist/commands/improve/eval-cases.js +0 -52
  344. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  345. package/dist/commands/improve/proposal-envelope.js +0 -31
  346. package/dist/commands/improve/run-context.js +0 -123
  347. package/dist/commands/improve/shared.js +0 -31
  348. package/dist/commands/improve/source-identity.js +0 -28
  349. package/dist/commands/improve/triage.js +0 -96
  350. package/dist/commands/proposal/drain-policies.js +0 -151
  351. package/dist/commands/sources/update-transaction.js +0 -220
  352. package/dist/core/action-contributors.js +0 -28
  353. package/dist/core/config/config-version-shim.js +0 -101
  354. package/dist/core/fs-txn.js +0 -405
  355. package/dist/core/lexical-score.js +0 -25
  356. package/dist/core/maintenance-barrier.js +0 -167
  357. package/dist/execution/executable-identity.js +0 -105
  358. package/dist/execution/guarded-source.js +0 -427
  359. package/dist/indexer/db/graph-db.js +0 -444
  360. package/dist/indexer/graph/graph-boost.js +0 -427
  361. package/dist/indexer/graph/graph-dedup.js +0 -95
  362. package/dist/indexer/graph/graph-extraction.js +0 -1108
  363. package/dist/indexer/search/name-match.js +0 -35
  364. package/dist/indexer/search/ranking-contributors.js +0 -515
  365. package/dist/indexer/search/ranking-types.js +0 -4
  366. package/dist/indexer/walk/project-context.js +0 -192
  367. package/dist/integrations/agent/execution-cascade.js +0 -566
  368. package/dist/integrations/agent/execution-definitions.js +0 -202
  369. package/dist/integrations/agent/execution-lowering.js +0 -841
  370. package/dist/integrations/agent/execution-preparation.js +0 -98
  371. package/dist/integrations/agent/inline-execution.js +0 -74
  372. package/dist/llm/graph-extract.js +0 -728
  373. package/dist/llm/metadata-enhance.js +0 -96
  374. package/dist/registry/create-provider-registry.js +0 -29
  375. package/dist/registry/pinned-request-helper.js +0 -247
  376. package/dist/registry/pinned-transport.js +0 -717
  377. package/dist/sources/providers/index.js +0 -14
  378. package/dist/storage/engines/sqlite-migrations.js +0 -271
  379. package/dist/storage/repositories/canaries-repository.js +0 -107
  380. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  381. package/dist/storage/repositories/registry-cache.js +0 -113
  382. package/dist/tasks/scheduler-sync-preview.js +0 -52
  383. package/dist/tasks/source/task-to-v3.js +0 -507
  384. package/dist/workflows/freeze/resolve-steps.js +0 -86
  385. package/dist/workflows/freeze/source-freeze.js +0 -64
  386. package/dist/workflows/ir/compile.js +0 -321
  387. package/dist/workflows/ir/environment-v4.js +0 -330
  388. package/dist/workflows/ir/freeze-v4.js +0 -153
  389. package/dist/workflows/ir/schema-v4.js +0 -745
  390. package/dist/workflows/ir/schema.js +0 -354
  391. package/dist/workflows/program/schema.js +0 -77
  392. package/dist/workflows/runtime/checkin.js +0 -57
  393. package/dist/workflows/runtime/plan-classifier.js +0 -196
  394. package/dist/workflows/runtime/unit-checkin.js +0 -45
  395. package/dist/workflows/runtime/unit-phases.js +0 -20
  396. package/dist/workflows/schema.js +0 -4
  397. package/dist/workflows/source-ir/compile.js +0 -200
  398. package/dist/workflows/source-ir/program.js +0 -50
  399. package/dist/workflows/source-ir/result.js +0 -26
  400. package/dist/workflows/source-ir/schema.js +0 -786
  401. package/dist/workflows/source-ir/triggers.js +0 -79
  402. package/dist/workflows/source-ir/uses.js +0 -40
  403. 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);