akm-cli 0.9.0-beta.6 → 0.9.0-rc.0

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 (326) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/help/help-improve.md +9 -6
  6. package/dist/assets/hints/cli-hints-full.md +6 -5
  7. package/dist/assets/profiles/default.json +9 -4
  8. package/dist/assets/profiles/frequent.json +1 -1
  9. package/dist/assets/profiles/memory-focus.json +1 -1
  10. package/dist/assets/profiles/proactive-maintenance.json +25 -0
  11. package/dist/assets/profiles/quick.json +1 -1
  12. package/dist/assets/profiles/recombine-only.json +21 -0
  13. package/dist/assets/profiles/reflect-distill.json +30 -0
  14. package/dist/assets/profiles/synthesize.json +15 -0
  15. package/dist/assets/profiles/thorough.json +1 -1
  16. package/dist/assets/prompts/consolidate-system.md +23 -0
  17. package/dist/assets/prompts/contradiction-judge.md +33 -0
  18. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  19. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  20. package/dist/assets/prompts/extract-session.md +11 -3
  21. package/dist/assets/prompts/graph-extract-system.md +1 -0
  22. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  23. package/dist/assets/prompts/memory-infer-system.md +1 -0
  24. package/dist/assets/prompts/memory-infer-user.md +5 -0
  25. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  26. package/dist/assets/prompts/procedural-system.md +44 -0
  27. package/dist/assets/prompts/recombine-system.md +40 -0
  28. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  29. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  39. package/dist/assets/templates/html/health.html +281 -111
  40. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  41. package/dist/assets/wiki/schema-template.md +4 -4
  42. package/dist/cli/clack.js +56 -0
  43. package/dist/cli/config-migrate.js +7 -1
  44. package/dist/cli/confirm.js +1 -1
  45. package/dist/cli/parse-args.js +46 -1
  46. package/dist/cli/shared.js +28 -0
  47. package/dist/cli.js +25 -14
  48. package/dist/commands/agent/agent-dispatch.js +3 -2
  49. package/dist/commands/agent/agent-support.js +0 -7
  50. package/dist/commands/agent/contribute-cli.js +26 -7
  51. package/dist/commands/config-cli.js +26 -13
  52. package/dist/commands/env/child-env.js +47 -0
  53. package/dist/commands/env/env-cli.js +220 -227
  54. package/dist/commands/env/env.js +14 -67
  55. package/dist/commands/env/secret-cli.js +140 -138
  56. package/dist/commands/feedback-cli.js +153 -147
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +76 -72
  59. package/dist/commands/health/advisories.js +151 -0
  60. package/dist/commands/health/checks.js +103 -16
  61. package/dist/commands/health/html-report.js +447 -81
  62. package/dist/commands/health/improve-metrics.js +771 -0
  63. package/dist/commands/health/llm-usage.js +65 -0
  64. package/dist/commands/health/md-report.js +103 -0
  65. package/dist/commands/health/metrics.js +278 -0
  66. package/dist/commands/health/stash-exposure.js +46 -0
  67. package/dist/commands/health/surfaces.js +216 -0
  68. package/dist/commands/health/task-runs.js +135 -0
  69. package/dist/commands/health/types.js +26 -0
  70. package/dist/commands/health/windows.js +195 -0
  71. package/dist/commands/health.js +91 -1083
  72. package/dist/commands/improve/anti-collapse.js +170 -0
  73. package/dist/commands/improve/calibration.js +161 -0
  74. package/dist/commands/improve/collapse-detector.js +421 -0
  75. package/dist/commands/improve/consolidate/chunking.js +141 -0
  76. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  77. package/dist/commands/improve/consolidate/merge.js +145 -0
  78. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  79. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  80. package/dist/commands/improve/consolidate.js +1313 -1278
  81. package/dist/commands/improve/dedup.js +482 -0
  82. package/dist/commands/improve/distill/content-repair.js +202 -0
  83. package/dist/commands/improve/distill/promote-memory.js +229 -0
  84. package/dist/commands/improve/distill/quality-gate.js +236 -0
  85. package/dist/commands/improve/distill-guards.js +127 -0
  86. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  87. package/dist/commands/improve/distill.js +243 -599
  88. package/dist/commands/improve/eligibility.js +434 -0
  89. package/dist/commands/improve/encoding-salience.js +205 -0
  90. package/dist/commands/improve/extract-cli.js +179 -59
  91. package/dist/commands/improve/extract-prompt.js +55 -4
  92. package/dist/commands/improve/extract-watch.js +140 -0
  93. package/dist/commands/improve/extract.js +409 -43
  94. package/dist/commands/improve/feedback-valence.js +54 -0
  95. package/dist/commands/improve/hot-probation.js +45 -0
  96. package/dist/commands/improve/improve-auto-accept.js +160 -7
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +32 -8
  99. package/dist/commands/improve/improve-result-file.js +15 -25
  100. package/dist/commands/improve/improve-session.js +58 -0
  101. package/dist/commands/improve/improve.js +510 -2537
  102. package/dist/commands/improve/locks.js +154 -0
  103. package/dist/commands/improve/loop-stages.js +1100 -0
  104. package/dist/commands/improve/memory/memory-belief.js +14 -15
  105. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  106. package/dist/commands/improve/memory/memory-improve.js +27 -27
  107. package/dist/commands/improve/outcome-loop.js +270 -0
  108. package/dist/commands/improve/preparation.js +2002 -0
  109. package/dist/commands/improve/proactive-maintenance.js +115 -0
  110. package/dist/commands/improve/procedural.js +398 -0
  111. package/dist/commands/improve/recombine.js +818 -0
  112. package/dist/commands/improve/reflect-noise.js +0 -0
  113. package/dist/commands/improve/reflect.js +212 -45
  114. package/dist/commands/improve/salience.js +455 -0
  115. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  116. package/dist/commands/improve/shared.js +51 -0
  117. package/dist/commands/improve/triage.js +93 -0
  118. package/dist/commands/lint/agent-linter.js +19 -24
  119. package/dist/commands/lint/base-linter.js +173 -60
  120. package/dist/commands/lint/command-linter.js +19 -24
  121. package/dist/commands/lint/env-key-rules.js +38 -1
  122. package/dist/commands/lint/fact-linter.js +39 -0
  123. package/dist/commands/lint/index.js +31 -13
  124. package/dist/commands/lint/memory-linter.js +1 -1
  125. package/dist/commands/lint/registry.js +7 -2
  126. package/dist/commands/lint/task-linter.js +3 -3
  127. package/dist/commands/lint/workflow-linter.js +26 -1
  128. package/dist/commands/observability-cli.js +4 -4
  129. package/dist/commands/proposal/drain-policies.js +13 -4
  130. package/dist/commands/proposal/drain.js +45 -51
  131. package/dist/commands/proposal/legacy-import.js +115 -0
  132. package/dist/commands/proposal/proposal-cli.js +24 -34
  133. package/dist/commands/proposal/proposal.js +7 -1
  134. package/dist/commands/proposal/propose.js +8 -3
  135. package/dist/commands/proposal/repository.js +829 -0
  136. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  137. package/dist/commands/proposal/validators/proposals.js +93 -882
  138. package/dist/commands/read/curate.js +419 -103
  139. package/dist/commands/read/knowledge.js +10 -3
  140. package/dist/commands/read/remember-cli.js +133 -138
  141. package/dist/commands/read/search-cli.js +15 -8
  142. package/dist/commands/read/search.js +22 -11
  143. package/dist/commands/read/show.js +106 -14
  144. package/dist/commands/registry-cli.js +76 -87
  145. package/dist/commands/remember.js +11 -12
  146. package/dist/commands/sources/add-cli.js +91 -95
  147. package/dist/commands/sources/history.js +1 -1
  148. package/dist/commands/sources/init.js +66 -18
  149. package/dist/commands/sources/installed-stashes.js +11 -3
  150. package/dist/commands/sources/schema-repair.js +44 -46
  151. package/dist/commands/sources/self-update.js +2 -2
  152. package/dist/commands/sources/source-add.js +7 -3
  153. package/dist/commands/sources/sources-cli.js +3 -3
  154. package/dist/commands/sources/stash-cli.js +29 -41
  155. package/dist/commands/sources/stash-skeleton.js +57 -8
  156. package/dist/commands/tasks/default-tasks.js +15 -2
  157. package/dist/commands/tasks/tasks-cli.js +20 -29
  158. package/dist/commands/tasks/tasks.js +39 -11
  159. package/dist/commands/wiki-cli.js +23 -38
  160. package/dist/commands/workflow-cli.js +15 -1
  161. package/dist/core/asset/asset-registry.js +3 -1
  162. package/dist/core/asset/asset-spec.js +21 -4
  163. package/dist/core/asset/frontmatter.js +188 -167
  164. package/dist/core/asset/markdown.js +8 -0
  165. package/dist/core/authoring-rules.js +92 -0
  166. package/dist/core/common.js +4 -23
  167. package/dist/core/concurrent.js +10 -1
  168. package/dist/core/config/config-io.js +10 -1
  169. package/dist/core/config/config-migration.js +18 -40
  170. package/dist/core/config/config-schema.js +389 -58
  171. package/dist/core/config/config-types.js +3 -3
  172. package/dist/core/config/config.js +67 -22
  173. package/dist/core/deep-merge.js +38 -0
  174. package/dist/core/errors.js +1 -0
  175. package/dist/core/eval/rank-metrics.js +113 -0
  176. package/dist/core/events.js +4 -7
  177. package/dist/core/improve-types.js +47 -8
  178. package/dist/core/logs-db.js +14 -75
  179. package/dist/core/parse.js +36 -16
  180. package/dist/core/paths.js +21 -18
  181. package/dist/core/standards/resolve-standards-context.js +87 -0
  182. package/dist/core/standards/resolve-stash-standards.js +99 -0
  183. package/dist/core/standards/resolve-type-conventions.js +66 -0
  184. package/dist/core/state/migrations.js +770 -0
  185. package/dist/core/state-db.js +142 -1091
  186. package/dist/core/structured.js +69 -0
  187. package/dist/core/time.js +53 -0
  188. package/dist/core/warn.js +21 -0
  189. package/dist/core/write-source.js +37 -0
  190. package/dist/indexer/db/db.js +356 -780
  191. package/dist/indexer/db/entry-mapper.js +41 -0
  192. package/dist/indexer/db/graph-db.js +129 -86
  193. package/dist/indexer/db/llm-cache.js +2 -2
  194. package/dist/indexer/db/schema.js +516 -0
  195. package/dist/indexer/ensure-index.js +103 -24
  196. package/dist/indexer/feedback/utility-policy.js +75 -0
  197. package/dist/indexer/graph/graph-boost.js +51 -41
  198. package/dist/indexer/graph/graph-extraction.js +207 -4
  199. package/dist/indexer/index-writer-lock.js +106 -0
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +291 -310
  202. package/dist/indexer/passes/dir-staleness.js +114 -0
  203. package/dist/indexer/passes/memory-inference.js +13 -5
  204. package/dist/indexer/passes/metadata.js +20 -0
  205. package/dist/indexer/read-preflight.js +23 -0
  206. package/dist/indexer/search/db-search.js +89 -13
  207. package/dist/indexer/search/fts-query.js +51 -0
  208. package/dist/indexer/search/ranking-contributors.js +95 -9
  209. package/dist/indexer/search/ranking.js +79 -3
  210. package/dist/indexer/search/search-fields.js +6 -0
  211. package/dist/indexer/search/search-source.js +32 -21
  212. package/dist/indexer/search/semantic-status.js +4 -0
  213. package/dist/indexer/walk/matchers.js +9 -0
  214. package/dist/indexer/walk/walker.js +21 -13
  215. package/dist/integrations/agent/builders.js +39 -13
  216. package/dist/integrations/agent/config.js +20 -59
  217. package/dist/integrations/agent/detect.js +9 -0
  218. package/dist/integrations/agent/index.js +3 -19
  219. package/dist/integrations/agent/model-aliases.js +7 -2
  220. package/dist/integrations/agent/profiles.js +7 -1
  221. package/dist/integrations/agent/prompts.js +75 -9
  222. package/dist/integrations/agent/runner-dispatch.js +59 -0
  223. package/dist/integrations/agent/runner.js +13 -9
  224. package/dist/integrations/agent/spawn.js +69 -67
  225. package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
  226. package/dist/integrations/harnesses/claude/index.js +2 -0
  227. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  228. package/dist/integrations/harnesses/index.js +2 -3
  229. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  230. package/dist/integrations/harnesses/opencode/index.js +2 -0
  231. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  232. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  233. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
  234. package/dist/integrations/harnesses/types.js +1 -0
  235. package/dist/integrations/session-logs/index.js +16 -0
  236. package/dist/llm/call-ai.js +2 -2
  237. package/dist/llm/client.js +57 -15
  238. package/dist/llm/embedder.js +67 -4
  239. package/dist/llm/embedders/cache.js +3 -1
  240. package/dist/llm/embedders/deterministic.js +66 -0
  241. package/dist/llm/embedders/local.js +73 -3
  242. package/dist/llm/feature-gate.js +16 -15
  243. package/dist/llm/graph-extract.js +67 -44
  244. package/dist/llm/memory-infer-impl.js +138 -0
  245. package/dist/llm/memory-infer.js +1 -127
  246. package/dist/llm/metadata-enhance.js +44 -31
  247. package/dist/llm/structured-call.js +49 -0
  248. package/dist/migrate-storage-node.mjs +8 -0
  249. package/dist/output/context.js +5 -5
  250. package/dist/output/renderers.js +85 -14
  251. package/dist/output/shapes/curate.js +14 -2
  252. package/dist/output/shapes/helpers.js +0 -3
  253. package/dist/output/shapes/passthrough.js +2 -1
  254. package/dist/output/text/helpers.js +29 -1
  255. package/dist/output/text/workflow.js +1 -0
  256. package/dist/registry/providers/skills-sh.js +21 -147
  257. package/dist/registry/providers/static-index.js +15 -157
  258. package/dist/registry/resolve.js +27 -9
  259. package/dist/runtime.js +25 -1
  260. package/dist/scripts/migrate-storage.js +2718 -2354
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +891 -597
  262. package/dist/setup/detect.js +9 -0
  263. package/dist/setup/legacy-config.js +106 -0
  264. package/dist/setup/prompt.js +57 -0
  265. package/dist/setup/providers.js +14 -0
  266. package/dist/setup/registry-stash-loader.js +12 -0
  267. package/dist/setup/semantic-assets.js +124 -0
  268. package/dist/setup/setup.js +52 -1614
  269. package/dist/setup/steps/connection.js +734 -0
  270. package/dist/setup/steps/output.js +31 -0
  271. package/dist/setup/steps/platforms.js +124 -0
  272. package/dist/setup/steps/semantic.js +27 -0
  273. package/dist/setup/steps/sources.js +222 -0
  274. package/dist/setup/steps/stashdir.js +42 -0
  275. package/dist/setup/steps/tasks.js +152 -0
  276. package/dist/sources/include.js +6 -2
  277. package/dist/sources/providers/filesystem.js +0 -1
  278. package/dist/sources/providers/git-install.js +210 -0
  279. package/dist/sources/providers/git-provider.js +234 -0
  280. package/dist/sources/providers/git-stash.js +248 -0
  281. package/dist/sources/providers/git.js +10 -661
  282. package/dist/sources/providers/npm.js +2 -6
  283. package/dist/sources/providers/provider-utils.js +13 -7
  284. package/dist/sources/providers/sync-from-ref.js +9 -1
  285. package/dist/sources/providers/tar-utils.js +16 -8
  286. package/dist/sources/providers/website.js +9 -5
  287. package/dist/sources/website-ingest.js +187 -29
  288. package/dist/sources/wiki-fetchers/registry.js +53 -0
  289. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  290. package/dist/storage/database.js +45 -10
  291. package/dist/storage/managed-db.js +82 -0
  292. package/dist/storage/repositories/canaries-repository.js +107 -0
  293. package/dist/storage/repositories/consolidation-repository.js +38 -0
  294. package/dist/storage/repositories/embeddings-repository.js +72 -0
  295. package/dist/storage/repositories/events-repository.js +187 -0
  296. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  297. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  298. package/dist/storage/repositories/index-db.js +14 -8
  299. package/dist/storage/repositories/proposals-repository.js +220 -0
  300. package/dist/storage/repositories/recombine-repository.js +213 -0
  301. package/dist/storage/repositories/registry-cache.js +93 -0
  302. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  303. package/dist/storage/repositories/task-history-repository.js +93 -0
  304. package/dist/storage/sqlite-pragmas.js +146 -0
  305. package/dist/tasks/backends/cron.js +1 -1
  306. package/dist/tasks/backends/index.js +9 -0
  307. package/dist/tasks/backends/launchd.js +1 -1
  308. package/dist/tasks/backends/schtasks.js +1 -1
  309. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  310. package/dist/tasks/runner.js +15 -13
  311. package/dist/text-import-hook.mjs +0 -0
  312. package/dist/wiki/wiki.js +52 -11
  313. package/dist/workflows/cli.js +1 -0
  314. package/dist/workflows/db.js +3 -4
  315. package/dist/workflows/runtime/runs.js +43 -118
  316. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  317. package/dist/workflows/validate-summary.js +2 -7
  318. package/docs/README.md +69 -18
  319. package/docs/data-and-telemetry.md +5 -4
  320. package/docs/migration/release-notes/0.7.0.md +1 -1
  321. package/docs/migration/release-notes/0.9.0.md +39 -0
  322. package/package.json +10 -10
  323. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  324. package/dist/commands/db-cli.js +0 -23
  325. package/dist/indexer/db/db-backup.js +0 -376
  326. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -10,8 +10,10 @@ import { getDbPath } from "../core/paths.js";
10
10
  import { isVerbose, warn, warnVerbose } from "../core/warn.js";
11
11
  import { resolveIndexPassLLM } from "../llm/index-passes.js";
12
12
  import { takeWorkflowDocument } from "../workflows/runtime/document-cache.js";
13
- import { clearStaleCacheEntries, closeDatabase, deleteEntriesByDir, deleteEntriesByIds, deleteEntriesByStashDir, deleteIndexDirStatesByStashDir, getAllEntriesForEmbedding, getEmbeddableEntryCount, getEmbeddingCount, getEntriesByDir, getEntryCount, getIndexDirState, getMeta, isVecAvailable, openDatabase, openExistingDatabase, rebuildFts, relinkUsageEvents, setMeta, upsertEmbedding, upsertEntry, upsertIndexDirState, upsertUtilityScore, upsertWorkflowDocument, warnIfVecMissing, } from "./db/db.js";
13
+ import { clearStaleCacheEntries, closeDatabase, deleteEntriesByDir, deleteEntriesByIds, deleteEntriesByStashDir, deleteIndexDirStatesByStashDir, getAllEntriesForEmbedding, getEmbeddableEntryCount, getEmbeddingCount, getEntryCount, getMeta, isVecAvailable, openExistingDatabase, openIndexDatabase, purgeEmbeddings, rebuildFts, relinkUsageEvents, setMeta, upsertEmbedding, upsertEntry, upsertIndexDirState, upsertUtilityScore, upsertWorkflowDocument, warnIfVecMissing, } from "./db/db.js";
14
14
  import { deleteStoredGraph } from "./db/graph-db.js";
15
+ import { withIndexWriterLease } from "./index-writer-lock.js";
16
+ import { canUseIncrementalSkip, computeDirFingerprint, getCachedZeroRowDirState, getDirIndexState, inferZeroRowReason, } from "./passes/dir-staleness.js";
15
17
  import { applyCuratedFrontmatter, applyWikiFrontmatter, generateMetadataFlat, isEnrichmentComplete, isWorkflowSkipWarning, loadStashFile, shouldIndexStashFile, } from "./passes/metadata.js";
16
18
  import { buildSearchText } from "./search/search-fields.js";
17
19
  import { classifySemanticFailure, clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "./search/semantic-status.js";
@@ -116,7 +118,7 @@ async function runWalkPhase(ctx) {
116
118
  ctx.timing.tWalkEnd = Date.now();
117
119
  throwIfAborted(signal);
118
120
  // LLM enrichment for directories that need it
119
- await enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal, true, reEnrich);
121
+ await enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal, reEnrich);
120
122
  onProgress({
121
123
  phase: "llm",
122
124
  message: resolveIndexPassLLM("enrichment", config)
@@ -141,6 +143,7 @@ async function runEmbeddingPhase(ctx) {
141
143
  */
142
144
  async function runFinalizePhase(ctx) {
143
145
  const { db, config, sources, sourceDirs, isIncremental, stashDir, signal, onProgress } = ctx;
146
+ ctx.timing.tFinalizeStart = Date.now();
144
147
  // Rebuild FTS after all inserts. Use incremental mode when this whole
145
148
  // index run is incremental — only entries touched by `upsertEntry`
146
149
  // since the last rebuild are re-indexed.
@@ -151,10 +154,13 @@ async function runFinalizePhase(ctx) {
151
154
  });
152
155
  ctx.timing.tFtsEnd = Date.now();
153
156
  // Re-link detached usage_events and recompute utility scores.
157
+ onProgress({ phase: "finalize", message: "Relinking usage events." });
154
158
  relinkUsageEvents(db);
159
+ onProgress({ phase: "finalize", message: "Recomputing utility scores." });
155
160
  recomputeUtilityScores(db);
156
161
  // Purge LLM cache entries for assets that no longer exist in the index.
157
162
  try {
163
+ onProgress({ phase: "finalize", message: "Clearing stale LLM cache entries." });
158
164
  clearStaleCacheEntries(db);
159
165
  }
160
166
  catch {
@@ -162,6 +168,7 @@ async function runFinalizePhase(ctx) {
162
168
  }
163
169
  // Regenerate each wiki's index.md from its pages' frontmatter. Best-effort.
164
170
  try {
171
+ onProgress({ phase: "finalize", message: "Regenerating wiki indexes." });
165
172
  const { regenerateAllWikiIndexes } = await import("../wiki/wiki.js");
166
173
  regenerateAllWikiIndexes(stashDir);
167
174
  }
@@ -178,6 +185,7 @@ async function runFinalizePhase(ctx) {
178
185
  warnIfVecMissing(db);
179
186
  const totalEntries = getEntryCount(db);
180
187
  const semanticEntryCount = getEmbeddableEntryCount(db);
188
+ onProgress({ phase: "finalize", message: "Verifying semantic search state." });
181
189
  const verification = verifyIndexState(db, config, semanticEntryCount, embeddingResult);
182
190
  if (config.semanticSearchMode === "off") {
183
191
  clearSemanticStatus();
@@ -195,8 +203,9 @@ async function runFinalizePhase(ctx) {
195
203
  }
196
204
  onProgress({ phase: "verify", message: verification.message });
197
205
  // Store verification result and totalEntries on ctx for the caller to use
198
- ctx._verification = verification;
199
- ctx._totalEntries = totalEntries;
206
+ ctx.verification = verification;
207
+ ctx.totalEntries = totalEntries;
208
+ ctx.timing.tFinalizeEnd = Date.now();
200
209
  // suppress unused warning — sources was previously used inline
201
210
  void sources;
202
211
  }
@@ -224,129 +233,186 @@ function runCleanPass(db, dryRun) {
224
233
  };
225
234
  }
226
235
  // ── Indexer ──────────────────────────────────────────────────────────────────
236
+ // ── Test seam ────────────────────────────────────────────────────────────────
237
+ // Swap-and-restore override. Inert in production; only tests call the setter.
238
+ let akmIndexOverride;
239
+ /** TEST-ONLY. Swap the implementation of `akmIndex`; pass undefined to restore. */
240
+ export function _setAkmIndexForTests(fake) {
241
+ akmIndexOverride = fake;
242
+ }
227
243
  export async function akmIndex(options) {
228
- const stashDir = options?.stashDir || resolveStashDir();
229
- const onProgress = options?.onProgress ?? (() => { });
230
- const signal = options?.signal;
231
- const reEnrich = options?.reEnrich === true;
232
- const full = options?.full === true;
233
- const clean = options?.clean === true;
234
- const dryRun = options?.dryRun === true;
235
- // Load config and resolve all stash sources
236
- const { loadConfig } = await import("../core/config/config.js");
237
- const config = loadConfig();
238
- // One-time, read-only guard: warn if the writable stash still holds an
239
- // un-migrated `vaults/` directory. In 0.9.0 the indexer skips `vaults/`
240
- // entirely, so an unmigrated vault's `.env` data would silently never be
241
- // indexed. Non-destructive only stats, never reads/writes/deletes.
242
- const { warnOnUnmigratedVaults } = await import("./usage/unmigrated-vaults-guard.js");
243
- warnOnUnmigratedVaults(stashDir);
244
- // Ensure git stash caches are extracted before resolving stash dirs,
245
- // so their content directories exist on disk for the walker to discover.
246
- const { ensureSourceCaches, resolveSourceEntries } = await import("./search/search-source.js");
247
- await ensureSourceCaches(config, { force: full });
248
- const allSourceEntries = resolveSourceEntries(stashDir, config);
249
- const allSourceDirs = allSourceEntries.map((s) => s.path);
250
- const t0 = Date.now();
251
- // Open database pass embedding dimension from config if available
252
- const dbPath = getDbPath();
253
- const embeddingDim = config.embedding?.dimension;
254
- const db = openDatabase(dbPath, embeddingDim ? { embeddingDim } : undefined);
255
- try {
256
- // Determine incremental vs full mode
257
- const prevStashDir = getMeta(db, "stashDir");
258
- const prevBuiltAt = getMeta(db, "builtAt");
259
- const isIncremental = !full && prevStashDir === stashDir && !!prevBuiltAt;
260
- const builtAtMs = isIncremental && prevBuiltAt ? new Date(prevBuiltAt).getTime() : 0;
261
- // Assemble the run context
262
- const ctx = {
263
- db,
264
- config,
265
- sources: allSourceEntries,
266
- sourceDirs: allSourceDirs,
267
- full,
268
- reEnrich,
269
- stashDir,
270
- onProgress,
271
- signal,
272
- timing: {
273
- t0,
274
- tWalkStart: t0,
275
- tWalkEnd: t0,
276
- tLlmEnd: t0,
277
- tFtsEnd: t0,
278
- tEmbedEnd: t0,
279
- },
280
- isIncremental,
281
- builtAtMs,
282
- hadRemovedSources: false,
283
- scannedDirs: 0,
284
- skippedDirs: 0,
285
- generatedCount: 0,
286
- walkWarnings: [],
287
- dirsNeedingLlm: [],
288
- embeddingResult: null,
289
- graphExtractionResult: null,
290
- };
244
+ if (akmIndexOverride)
245
+ return akmIndexOverride(options);
246
+ return akmIndexReal(options);
247
+ }
248
+ async function akmIndexReal(options) {
249
+ const requestedAt = Date.now();
250
+ let acquiredAt = requestedAt;
251
+ return withIndexWriterLease({
252
+ purpose: "akm-index",
253
+ signal: options?.signal,
254
+ onWait: ({ waitedMs }) => {
255
+ options?.onProgress?.({
256
+ phase: "preflight",
257
+ message: `Waiting for index writer lease (${Math.round(waitedMs / 1000)}s elapsed).`,
258
+ });
259
+ },
260
+ onAcquired: ({ waitedMs }) => {
261
+ acquiredAt = requestedAt + waitedMs;
262
+ },
263
+ }, async () => {
264
+ const stashDir = options?.stashDir || resolveStashDir();
265
+ const onProgress = options?.onProgress ?? (() => { });
266
+ const signal = options?.signal;
267
+ const reEnrich = options?.reEnrich === true;
268
+ const full = options?.full === true;
269
+ const clean = options?.clean === true;
270
+ const dryRun = options?.dryRun === true;
271
+ // Load config and resolve all stash sources
272
+ const { loadConfig } = await import("../core/config/config.js");
273
+ const config = loadConfig();
274
+ // One-time, read-only guard: warn if the writable stash still holds an
275
+ // un-migrated `vaults/` directory. In 0.9.0 the indexer skips `vaults/`
276
+ // entirely, so an unmigrated vault's `.env` data would silently never be
277
+ // indexed. Non-destructive only stats, never reads/writes/deletes.
278
+ const { warnOnUnmigratedVaults } = await import("./usage/unmigrated-vaults-guard.js");
279
+ warnOnUnmigratedVaults(stashDir);
280
+ // Ensure git stash caches are extracted before resolving stash dirs,
281
+ // so their content directories exist on disk for the walker to discover.
282
+ const sourceCacheStart = Date.now();
283
+ onProgress({ phase: "preflight", message: "Hydrating source caches." });
284
+ const { ensureSourceCaches, resolveSourceEntries } = await import("./search/search-source.js");
285
+ await ensureSourceCaches(config, { force: full });
286
+ const sourceCacheEnd = Date.now();
287
+ const allSourceEntries = resolveSourceEntries(stashDir, config);
288
+ const allSourceDirs = allSourceEntries.map((s) => s.path);
291
289
  onProgress({
292
- phase: "summary",
293
- message: buildIndexSummaryMessage({
294
- mode: isIncremental ? "incremental" : "full",
295
- sourcesCount: allSourceDirs.length,
296
- semanticSearchMode: config.semanticSearchMode,
297
- embeddingProvider: getEmbeddingProvider(config.embedding),
298
- llmEnabled: !!resolveIndexPassLLM("enrichment", config),
299
- vecAvailable: isVecAvailable(db),
300
- }),
290
+ phase: "preflight",
291
+ message: `Resolved ${allSourceDirs.length} stash source${allSourceDirs.length === 1 ? "" : "s"}.`,
301
292
  });
302
- // ── Phase sequence ───────────────────────────────────────────────────────
303
- await runSourceCachePhase(ctx);
304
- await runWalkPhase(ctx);
305
- await runEmbeddingPhase(ctx);
306
- await runFinalizePhase(ctx);
307
- // ────────────────────────────────────────────────────────────────────────
308
- const { _verification: verification, _totalEntries: totalEntries } = ctx;
309
- const { timing } = ctx;
310
- // ── Clean pass ───────────────────────────────────────────────────────────
311
- // After the normal index completes, remove entries whose source files no
312
- // longer exist on disk. Remote entries (empty file_path) are skipped.
313
- let cleanResult;
314
- if (clean) {
315
- cleanResult = runCleanPass(db, dryRun);
293
+ const t0 = Date.now();
294
+ // Open database — pass embedding dimension from config if available
295
+ const dbPath = getDbPath();
296
+ const embeddingDim = config.embedding?.dimension;
297
+ const db = openIndexDatabase(dbPath, embeddingDim ? { embeddingDim } : undefined);
298
+ try {
299
+ // Determine incremental vs full mode
300
+ const prevStashDir = getMeta(db, "stashDir");
301
+ const prevBuiltAt = getMeta(db, "builtAt");
302
+ const isIncremental = !full && prevStashDir === stashDir && !!prevBuiltAt;
303
+ const builtAtMs = isIncremental && prevBuiltAt ? new Date(prevBuiltAt).getTime() : 0;
304
+ // Assemble the run context
305
+ const ctx = {
306
+ db,
307
+ config,
308
+ sources: allSourceEntries,
309
+ sourceDirs: allSourceDirs,
310
+ full,
311
+ reEnrich,
312
+ stashDir,
313
+ onProgress,
314
+ signal,
315
+ timing: {
316
+ t0,
317
+ tWalkStart: t0,
318
+ tWalkEnd: t0,
319
+ tLlmEnd: t0,
320
+ tFtsEnd: t0,
321
+ tEmbedEnd: t0,
322
+ tFinalizeStart: t0,
323
+ tFinalizeEnd: t0,
324
+ },
325
+ isIncremental,
326
+ builtAtMs,
327
+ hadRemovedSources: false,
328
+ scannedDirs: 0,
329
+ skippedDirs: 0,
330
+ generatedCount: 0,
331
+ walkWarnings: [],
332
+ dirsNeedingLlm: [],
333
+ embeddingResult: null,
334
+ };
335
+ onProgress({
336
+ phase: "summary",
337
+ message: buildIndexSummaryMessage({
338
+ mode: isIncremental ? "incremental" : "full",
339
+ sourcesCount: allSourceDirs.length,
340
+ semanticSearchMode: config.semanticSearchMode,
341
+ embeddingProvider: getEmbeddingProvider(config.embedding),
342
+ llmEnabled: !!resolveIndexPassLLM("enrichment", config),
343
+ vecAvailable: isVecAvailable(db),
344
+ }),
345
+ });
346
+ // ── Phase sequence ───────────────────────────────────────────────────────
347
+ await runSourceCachePhase(ctx);
348
+ await runWalkPhase(ctx);
349
+ await runEmbeddingPhase(ctx);
350
+ await runFinalizePhase(ctx);
351
+ // ────────────────────────────────────────────────────────────────────────
352
+ // runFinalizePhase always populates these before returning.
353
+ const verification = ctx.verification;
354
+ const totalEntries = ctx.totalEntries;
355
+ const { timing } = ctx;
356
+ // ── Clean pass ───────────────────────────────────────────────────────────
357
+ // After the normal index completes, remove entries whose source files no
358
+ // longer exist on disk. Remote entries (empty file_path) are skipped.
359
+ let cleanResult;
360
+ const cleanStart = Date.now();
361
+ if (clean) {
362
+ onProgress({
363
+ phase: "finalize",
364
+ message: dryRun ? "Scanning for stale index entries (dry run)." : "Removing stale index entries.",
365
+ });
366
+ cleanResult = runCleanPass(db, dryRun);
367
+ }
368
+ const cleanEnd = Date.now();
369
+ // ────────────────────────────────────────────────────────────────────────
370
+ return {
371
+ stashDir,
372
+ totalEntries,
373
+ generatedMetadata: ctx.generatedCount,
374
+ indexPath: dbPath,
375
+ mode: isIncremental ? "incremental" : "full",
376
+ directoriesScanned: ctx.scannedDirs,
377
+ directoriesSkipped: ctx.skippedDirs,
378
+ ...(ctx.walkWarnings.length > 0 ? { warnings: ctx.walkWarnings } : {}),
379
+ verification,
380
+ timing: {
381
+ totalMs: Date.now() - timing.t0,
382
+ walkMs: timing.tWalkEnd - timing.tWalkStart,
383
+ llmMs: timing.tLlmEnd - timing.tWalkEnd,
384
+ embedMs: timing.tEmbedEnd - timing.tLlmEnd,
385
+ ftsMs: timing.tFtsEnd - timing.tEmbedEnd,
386
+ finalizeMs: timing.tFinalizeEnd - timing.tFinalizeStart,
387
+ cleanMs: clean ? cleanEnd - cleanStart : 0,
388
+ preflightMs: timing.t0 - requestedAt,
389
+ leaseWaitMs: acquiredAt - requestedAt,
390
+ sourceCacheMs: sourceCacheEnd - sourceCacheStart,
391
+ endToEndMs: Date.now() - requestedAt,
392
+ },
393
+ ...(cleanResult !== undefined ? { clean: cleanResult } : {}),
394
+ };
316
395
  }
317
- // ────────────────────────────────────────────────────────────────────────
318
- return {
319
- stashDir,
320
- totalEntries,
321
- generatedMetadata: ctx.generatedCount,
322
- indexPath: dbPath,
323
- mode: isIncremental ? "incremental" : "full",
324
- directoriesScanned: ctx.scannedDirs,
325
- directoriesSkipped: ctx.skippedDirs,
326
- ...(ctx.walkWarnings.length > 0 ? { warnings: ctx.walkWarnings } : {}),
327
- verification,
328
- timing: {
329
- totalMs: Date.now() - timing.t0,
330
- walkMs: timing.tWalkEnd - timing.tWalkStart,
331
- llmMs: timing.tLlmEnd - timing.tWalkEnd,
332
- embedMs: timing.tEmbedEnd - timing.tLlmEnd,
333
- ftsMs: timing.tFtsEnd - timing.tEmbedEnd,
334
- },
335
- ...(cleanResult !== undefined ? { clean: cleanResult } : {}),
336
- };
337
- }
338
- finally {
339
- closeDatabase(db);
340
- }
396
+ finally {
397
+ closeDatabase(db);
398
+ }
399
+ });
341
400
  }
342
- // ── Extracted helpers for indexing ────────────────────────────────────────────
343
- async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress) {
401
+ /**
402
+ * Phase 1 (async): walk every source directory and pre-generate all metadata
403
+ * outside any transaction, producing the per-directory scan records that
404
+ * {@link persistDirRecords} later writes.
405
+ *
406
+ * generateMetadataFlat is async (uses dynamic import for the matcher/renderer
407
+ * registry), so it cannot run inside a db.transaction() callback — hence the
408
+ * split into a pure-ish scan pass and a synchronous persist pass.
409
+ */
410
+ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress) {
344
411
  let scannedDirs = 0;
345
412
  let skippedDirs = 0;
346
413
  let generatedCount = 0;
347
414
  const warnings = [];
348
415
  const seenPaths = new Set();
349
- const dirsNeedingLlm = [];
350
416
  const dirRecords = [];
351
417
  let processedDirs = 0;
352
418
  let priorDirsChanged = hadRemovedSources;
@@ -366,6 +432,53 @@ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadR
366
432
  reportScanProgress(`${kind === "scan" ? "Rescanning" : "Skipping"} ${path.relative(currentStashDir, dirPath) || "."} ` +
367
433
  `from ${currentStashDir}: ${reason.kind}${detail}${rowInfo}`);
368
434
  };
435
+ // Duplicate-directory guard shared by the wiki-root and normal branches. A
436
+ // dir may surface via multiple stash roots; only the first occurrence is
437
+ // indexed, and the later ones are recorded as skips. Returns true when the
438
+ // dir was already seen (caller should skip further processing).
439
+ const markSeenOrSkipDuplicate = (dirPath, currentStashDir, files) => {
440
+ const resolved = path.resolve(dirPath);
441
+ if (seenPaths.has(resolved)) {
442
+ const reason = { kind: "duplicate-dir" };
443
+ dirRecords.push({ dirPath, currentStashDir, files, stash: null, skip: true, reason });
444
+ reportDirDecision("skip", dirPath, currentStashDir, reason);
445
+ return true;
446
+ }
447
+ seenPaths.add(resolved);
448
+ return false;
449
+ };
450
+ // Incremental freshness gate shared by both branches: consult the persisted
451
+ // dir state and record either a skip (unchanged + eligible for incremental
452
+ // skip) or a scan record carrying the candidate stash.
453
+ const recordFreshnessDecision = (dirPath, currentStashDir, stateFiles, stash) => {
454
+ const previousState = getDirIndexState(db, dirPath, stateFiles, builtAtMs);
455
+ if (isIncremental && !previousState.stale && canUseIncrementalSkip(previousState, priorDirsChanged)) {
456
+ skippedDirs++;
457
+ dirRecords.push({
458
+ dirPath,
459
+ currentStashDir,
460
+ files: stateFiles,
461
+ stash: null,
462
+ skip: true,
463
+ reason: previousState.reason,
464
+ });
465
+ reportDirDecision("skip", dirPath, currentStashDir, previousState.reason, previousState.persistedRowCount);
466
+ return;
467
+ }
468
+ scannedDirs++;
469
+ priorDirsChanged = true;
470
+ const reason = isIncremental ? previousState.reason : { kind: "full-rebuild" };
471
+ dirRecords.push({
472
+ dirPath,
473
+ currentStashDir,
474
+ files: stateFiles,
475
+ stash,
476
+ skip: false,
477
+ reason,
478
+ persistedRowCount: previousState.persistedRowCount,
479
+ });
480
+ reportDirDecision("scan", dirPath, currentStashDir, reason, previousState.persistedRowCount);
481
+ };
369
482
  for (const sourceAdded of allSourceEntries) {
370
483
  const currentStashDir = sourceAdded.path;
371
484
  const fileContexts = walkStashFlat(currentStashDir);
@@ -403,33 +516,9 @@ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadR
403
516
  }
404
517
  }
405
518
  for (const [dirPath, { files, entries }] of wikiDirGroups) {
406
- if (seenPaths.has(path.resolve(dirPath))) {
407
- const reason = { kind: "duplicate-dir" };
408
- dirRecords.push({ dirPath, currentStashDir, files, stash: null, skip: true, reason });
409
- reportDirDecision("skip", dirPath, currentStashDir, reason);
519
+ if (markSeenOrSkipDuplicate(dirPath, currentStashDir, files))
410
520
  continue;
411
- }
412
- seenPaths.add(path.resolve(dirPath));
413
- const previousState = getDirIndexState(db, dirPath, files, builtAtMs);
414
- if (isIncremental && !previousState.stale && canUseIncrementalSkip(previousState, priorDirsChanged)) {
415
- skippedDirs++;
416
- dirRecords.push({ dirPath, currentStashDir, files, stash: null, skip: true, reason: previousState.reason });
417
- reportDirDecision("skip", dirPath, currentStashDir, previousState.reason, previousState.persistedRowCount);
418
- continue;
419
- }
420
- scannedDirs++;
421
- priorDirsChanged = true;
422
- const reason = isIncremental ? previousState.reason : { kind: "full-rebuild" };
423
- dirRecords.push({
424
- dirPath,
425
- currentStashDir,
426
- files,
427
- stash: { entries },
428
- skip: false,
429
- reason,
430
- persistedRowCount: previousState.persistedRowCount,
431
- });
432
- reportDirDecision("scan", dirPath, currentStashDir, reason, previousState.persistedRowCount);
521
+ recordFreshnessDecision(dirPath, currentStashDir, files, { entries });
433
522
  }
434
523
  continue;
435
524
  }
@@ -444,13 +533,8 @@ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadR
444
533
  }
445
534
  for (const [dirPath, files] of dirGroups) {
446
535
  const indexableFiles = files.filter((file) => shouldIndexStashFile(currentStashDir, file));
447
- if (seenPaths.has(path.resolve(dirPath))) {
448
- const reason = { kind: "duplicate-dir" };
449
- dirRecords.push({ dirPath, currentStashDir, files: indexableFiles, stash: null, skip: true, reason });
450
- reportDirDecision("skip", dirPath, currentStashDir, reason);
536
+ if (markSeenOrSkipDuplicate(dirPath, currentStashDir, indexableFiles))
451
537
  continue;
452
- }
453
- seenPaths.add(path.resolve(dirPath));
454
538
  if (indexableFiles.length === 0) {
455
539
  skippedDirs++;
456
540
  const reason = { kind: "no-indexable-files" };
@@ -480,37 +564,17 @@ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadR
480
564
  if (generated.entries.length > 0) {
481
565
  generatedCount += generated.entries.length;
482
566
  }
483
- const previousState = getDirIndexState(db, dirPath, staleFiles, builtAtMs);
484
- if (isIncremental && !previousState.stale && canUseIncrementalSkip(previousState, priorDirsChanged)) {
485
- skippedDirs++;
486
- dirRecords.push({
487
- dirPath,
488
- currentStashDir,
489
- files: staleFiles,
490
- stash: null,
491
- skip: true,
492
- reason: previousState.reason,
493
- });
494
- reportDirDecision("skip", dirPath, currentStashDir, previousState.reason, previousState.persistedRowCount);
495
- continue;
496
- }
497
- scannedDirs++;
498
- priorDirsChanged = true;
499
- const reason = isIncremental ? previousState.reason : { kind: "full-rebuild" };
500
- dirRecords.push({
501
- dirPath,
502
- currentStashDir,
503
- files: staleFiles,
504
- stash,
505
- skip: false,
506
- reason,
507
- persistedRowCount: previousState.persistedRowCount,
508
- });
509
- reportDirDecision("scan", dirPath, currentStashDir, reason, previousState.persistedRowCount);
567
+ recordFreshnessDecision(dirPath, currentStashDir, staleFiles, stash);
510
568
  }
511
569
  }
512
- // Phase 2 (sync): write all pre-generated metadata inside a single transaction.
513
- //
570
+ return { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings };
571
+ }
572
+ /**
573
+ * Phase 2 (sync): write all pre-generated scan records inside a single
574
+ * transaction, returning the directories that still need LLM enrichment.
575
+ */
576
+ function persistDirRecords(db, dirRecords, doFullDelete, warnings) {
577
+ const dirsNeedingLlm = [];
514
578
  // Cross-stash dedup: track indexed assets by content identity
515
579
  // (type + filename + description) so the same asset from a lower-priority
516
580
  // stash root is skipped when a higher-priority root already covers it.
@@ -632,101 +696,17 @@ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadR
632
696
  }
633
697
  });
634
698
  insertTransaction();
635
- return { scannedDirs, skippedDirs, generatedCount, warnings, dirsNeedingLlm };
636
- }
637
- function getDirIndexState(db, dirPath, files, builtAtMs) {
638
- const prevEntries = getEntriesByDir(db, dirPath);
639
- const fingerprint = computeDirFingerprint(dirPath, files);
640
- if (prevEntries.length > 0) {
641
- const staleReason = getDirStaleReason(dirPath, files, prevEntries, builtAtMs);
642
- if (!staleReason) {
643
- return { stale: false, reason: { kind: "unchanged" }, persistedRowCount: prevEntries.length };
644
- }
645
- return { stale: true, reason: staleReason, persistedRowCount: prevEntries.length };
646
- }
647
- const cachedState = getIndexDirState(db, dirPath);
648
- if (cachedState &&
649
- cachedState.fileSetHash === fingerprint.fileSetHash &&
650
- cachedState.fileMtimeMaxMs === fingerprint.fileMtimeMaxMs) {
651
- return {
652
- stale: false,
653
- reason: { kind: "cached-zero-row-state", detail: cachedState.reason },
654
- persistedRowCount: 0,
655
- };
656
- }
657
- return {
658
- stale: true,
659
- reason: { kind: "no-previous-rows", detail: cachedState ? `cached=${cachedState.reason}` : undefined },
660
- persistedRowCount: 0,
661
- };
662
- }
663
- function getCachedZeroRowDirState(db, dirPath, files, builtAtMs, priorDirsChanged) {
664
- const state = getDirIndexState(db, dirPath, files, builtAtMs);
665
- if (state.stale || state.reason.kind !== "cached-zero-row-state")
666
- return undefined;
667
- if (!canUseIncrementalSkip(state, priorDirsChanged))
668
- return undefined;
669
- return state;
670
- }
671
- function canUseIncrementalSkip(state, priorDirsChanged) {
672
- return !(priorDirsChanged &&
673
- state.reason.kind === "cached-zero-row-state" &&
674
- state.reason.detail === "deduped-zero-row");
675
- }
676
- function computeDirFingerprint(_dirPath, files) {
677
- const normalizedFiles = [...new Set(files.map((file) => path.basename(file)))].sort();
678
- let fileMtimeMaxMs = 0;
679
- for (const file of files) {
680
- try {
681
- fileMtimeMaxMs = Math.max(fileMtimeMaxMs, fs.statSync(file).mtimeMs);
682
- }
683
- catch {
684
- fileMtimeMaxMs = Number.POSITIVE_INFINITY;
685
- break;
686
- }
687
- }
688
- return {
689
- fileSetHash: normalizedFiles.join("\0"),
690
- fileMtimeMaxMs,
691
- };
692
- }
693
- function getDirStaleReason(_dirPath, currentFiles, previousEntries, builtAtMs) {
694
- const prevFileNames = new Set(previousEntries
695
- .map((ie) => {
696
- const fromPath = path.basename(ie.filePath);
697
- return fromPath || ie.entry.filename;
698
- })
699
- .filter((e) => !!e));
700
- const currFileNames = new Set(currentFiles.map((f) => path.basename(f)));
701
- if (prevFileNames.size !== currFileNames.size) {
702
- return { kind: "file-set-changed", detail: `${prevFileNames.size} -> ${currFileNames.size} files` };
703
- }
704
- for (const name of currFileNames) {
705
- if (!prevFileNames.has(name))
706
- return { kind: "file-set-changed", detail: name };
707
- }
708
- for (const file of currentFiles) {
709
- try {
710
- if (fs.statSync(file).mtimeMs > builtAtMs)
711
- return { kind: "mtime-changed", detail: path.basename(file) };
712
- }
713
- catch {
714
- return { kind: "missing-file", detail: path.basename(file) };
715
- }
716
- }
717
- return undefined;
699
+ return { dirsNeedingLlm };
718
700
  }
719
- function inferZeroRowReason(stash, priorReason, warnings, dirPath, dedupedRows) {
720
- if (dedupedRows > 0)
721
- return "deduped-zero-row";
722
- const workflowNoise = warnings.some((warning) => warning.startsWith("Skipped workflow ") && warning.includes(dirPath));
723
- if (workflowNoise)
724
- return "workflow-noise";
725
- if (!stash || stash.entries.length === 0)
726
- return "empty-generated-set";
727
- return `zero-row:${priorReason?.kind ?? "unknown"}`;
701
+ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress) {
702
+ // Phase 1 (async): walk directories and pre-generate all metadata outside the
703
+ // transaction.
704
+ const { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings } = await scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress);
705
+ // Phase 2 (sync): write all pre-generated metadata inside a single transaction.
706
+ const { dirsNeedingLlm } = persistDirRecords(db, dirRecords, doFullDelete, warnings);
707
+ return { scannedDirs, skippedDirs, generatedCount, warnings, dirsNeedingLlm };
728
708
  }
729
- async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal, _enrich = false, reEnrich = false) {
709
+ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal, reEnrich = false) {
730
710
  // Resolve per-pass LLM config via the unified shim. Returns undefined when
731
711
  // either no `akm.llm` is configured or the user opted this pass out via
732
712
  // `index.enrichment.llm = false`. (#208)
@@ -896,21 +876,9 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal) {
896
876
  const currentFingerprint = deriveSemanticProviderFingerprint(config.embedding);
897
877
  const storedFingerprint = getMeta(db, "embeddingFingerprint");
898
878
  if (storedFingerprint && storedFingerprint !== currentFingerprint) {
899
- try {
900
- db.exec("DELETE FROM embeddings");
901
- }
902
- catch {
903
- /* ignore */
904
- }
905
- if (isVecAvailable(db)) {
906
- try {
907
- db.exec("DELETE FROM entries_vec");
908
- }
909
- catch {
910
- /* ignore */
911
- }
912
- }
913
- setMeta(db, "hasEmbeddings", "0");
879
+ // Model/provider changed → stored vectors are incompatible. Clear them
880
+ // (same dimension, so keep the vec table); re-embedded by this index run.
881
+ purgeEmbeddings(db);
914
882
  }
915
883
  try {
916
884
  const { embedBatch } = await import("../llm/embedder.js");
@@ -940,31 +908,44 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal) {
940
908
  warnVerbose(`[embed] ${ref} (${chars} chars, est. ${tokens} tokens) → batch ${batchNum}/${totalBatches}`);
941
909
  }
942
910
  }
943
- const embeddings = await embedBatch(texts, config.embedding, signal);
944
- throwIfAborted(signal);
945
- // Wrap all embedding upserts in a single transaction so partial
946
- // state is rolled back on failure rather than leaving the table half-filled.
947
- let storedCount = 0;
948
- let skippedCount = 0;
949
- db.transaction(() => {
950
- for (let i = 0; i < allEntries.length; i++) {
951
- if (upsertEmbedding(db, allEntries[i].id, embeddings[i])) {
952
- storedCount++;
953
- }
954
- else {
955
- skippedCount++;
911
+ let heartbeatTimer;
912
+ try {
913
+ heartbeatTimer = setInterval(() => {
914
+ onProgress({
915
+ phase: "embeddings",
916
+ message: `Still generating embeddings for ${allEntries.length} entr${allEntries.length === 1 ? "y" : "ies"}; waiting on embedding provider.`,
917
+ });
918
+ }, 15000);
919
+ const embeddings = await embedBatch(texts, config.embedding, signal);
920
+ throwIfAborted(signal);
921
+ // Wrap all embedding upserts in a single transaction so partial
922
+ // state is rolled back on failure rather than leaving the table half-filled.
923
+ let storedCount = 0;
924
+ let skippedCount = 0;
925
+ db.transaction(() => {
926
+ for (let i = 0; i < allEntries.length; i++) {
927
+ if (upsertEmbedding(db, allEntries[i].id, embeddings[i])) {
928
+ storedCount++;
929
+ }
930
+ else {
931
+ skippedCount++;
932
+ }
956
933
  }
934
+ })();
935
+ if (skippedCount > 0) {
936
+ warn(`[embed] ${skippedCount} embedding${skippedCount === 1 ? "" : "s"} skipped (entry deleted between queue and write)`);
957
937
  }
958
- })();
959
- if (skippedCount > 0) {
960
- warn(`[embed] ${skippedCount} embedding${skippedCount === 1 ? "" : "s"} skipped (entry deleted between queue and write)`);
938
+ onProgress({
939
+ phase: "embeddings",
940
+ message: `Stored ${storedCount} embedding${storedCount === 1 ? "" : "s"}.`,
941
+ });
942
+ setMeta(db, "embeddingFingerprint", currentFingerprint);
943
+ return { success: true };
944
+ }
945
+ finally {
946
+ if (heartbeatTimer)
947
+ clearInterval(heartbeatTimer);
961
948
  }
962
- onProgress({
963
- phase: "embeddings",
964
- message: `Stored ${storedCount} embedding${storedCount === 1 ? "" : "s"}.`,
965
- });
966
- setMeta(db, "embeddingFingerprint", currentFingerprint);
967
- return { success: true };
968
949
  }
969
950
  catch (error) {
970
951
  const message = error instanceof Error ? error.message : String(error);
@@ -1075,7 +1056,7 @@ function resolveIndexedFiles(dirPath, files, stash) {
1075
1056
  for (const entry of stash.entries) {
1076
1057
  const entryPath = entry.filename
1077
1058
  ? path.join(dirPath, entry.filename)
1078
- : matchEntryToFile(entry.name, fileBasenameMap, files);
1059
+ : matchEntryToFile(entry.name, fileBasenameMap);
1079
1060
  if (entryPath)
1080
1061
  resolved.add(entryPath);
1081
1062
  }
@@ -1194,7 +1175,7 @@ export function buildFileBasenameMap(files) {
1194
1175
  * try matching the last segment
1195
1176
  * 3. No implicit file fallback: ambiguous legacy entries are skipped
1196
1177
  */
1197
- export function matchEntryToFile(entryName, fileMap, _files) {
1178
+ export function matchEntryToFile(entryName, fileMap) {
1198
1179
  // Exact match on entry name
1199
1180
  const exact = fileMap.get(entryName);
1200
1181
  if (exact)