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
@@ -1,1108 +0,0 @@
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
- * Graph-extraction pass for `akm index` (#207).
6
- *
7
- * Walks the primary stash for `memory:` and `knowledge:` assets, asks the
8
- * configured LLM to extract entities and relations from each one, and
9
- * persists the result to stash-local SQLite graph tables keyed by stash root.
10
- * The artifact is consumed by the search
11
- * pipeline (see `src/indexer/graph/graph-boost.ts`) as a single boost component
12
- * inside the existing FTS5+boosts loop — there is NO second SearchHit
13
- * scorer and no parallel ranking track.
14
- *
15
- * Disabling — three preconditions must ALL hold for the pass to run:
16
- * 1. An LLM profile must be configured (no provider = no extraction). When
17
- * absent, `resolveIndexPassExecution("graph", config).runner` is
18
- * `undefined` and the pass short-circuits.
19
- * 2. The selected strategy's `processes.graphExtraction.enabled !== false`
20
- * — the feature-gate layer (historically v1 spec §14, since superseded by
21
- * the 0.8.0 profile shape). Set to `false` to block the pass at the
22
- * feature-gate layer (no network call may ever issue).
23
- * 3. `index.graph.llm !== false` — the per-pass opt-out layer (#208).
24
- * Set to `false` to skip just this pass while leaving other passes
25
- * that share the same LLM profile enabled.
26
- * Toggling any one off does NOT delete the existing persisted graph — the
27
- * user keeps the boost component they already have, it just stops
28
- * refreshing.
29
- *
30
- * Locked v1 contract:
31
- * - LLM access is exclusively via the frozen runner returned by
32
- * `resolveIndexPassExecution("graph", config)`.
33
- * - The graph rows are an indexer artifact, NOT a user-visible
34
- * asset. It does not have an asset ref, does not appear in search
35
- * hits, and is not addressable via `akm show`. The persisted artifact
36
- * lives in indexer-owned SQLite tables (`replaceStoredGraph` /
37
- * `loadStoredGraphSnapshot` in `../db/graph-db.ts`), NOT as a file on
38
- * disk (R-065 #3 — this comment previously described a retired
39
- * `fs.writeFile`-based storage layout) — `writeAssetToSource` is
40
- * reserved for asset writes (CLAUDE.md / spec §10 step 5).
41
- */
42
- import fs from "node:fs";
43
- import path from "node:path";
44
- import { stashDirFor } from "../../core/asset/asset-placement.js";
45
- import { parseFrontmatter } from "../../core/asset/frontmatter.js";
46
- import { concurrentMap } from "../../core/concurrent.js";
47
- import { getIndexPassConfig, loadConfig, resolveBatchSize } from "../../core/config/config.js";
48
- import { ConfigError, rethrowIfTestIsolationError } from "../../core/errors.js";
49
- import { warn, warnVerbose } from "../../core/warn.js";
50
- import { disposeLoweredExecutionDispatchLease, } from "../../integrations/agent/execution-lowering.js";
51
- import { isProcessEnabled } from "../../llm/feature-gate.js";
52
- import * as graphExtract from "../../llm/graph-extract.js";
53
- import { resolveIndexPassExecution } from "../../llm/index-passes.js";
54
- import { preflightStructuredLlmRunner } from "../../llm/structured-call.js";
55
- import { computeBodyHash, getLlmCacheEntriesByRefs, upsertLlmCacheEntry, } from "../../storage/repositories/index-llm-cache-repository.js";
56
- import { GRAPH_SCHEMA_VERSION } from "../../storage/repositories/index-schema.js";
57
- import { acknowledgeExtractionQueueEntry, enqueueGraphExtraction, loadStoredGraphSnapshot, peekExtractionQueue, replaceStoredGraph, } from "../db/graph-db.js";
58
- import { walkMarkdownFiles } from "../walk/walker.js";
59
- import { deduplicateGraph } from "./graph-dedup.js";
60
- /** Schema version for the persisted artifact — bumps trigger a full rebuild. */
61
- export const GRAPH_FILE_SCHEMA_VERSION = GRAPH_SCHEMA_VERSION;
62
- function graphExecutionForContext(ctx, config) {
63
- if (Object.hasOwn(ctx, "llmRunner")) {
64
- return Object.freeze({ runner: ctx.llmRunner ?? undefined, notices: Object.freeze([]) });
65
- }
66
- return resolveIndexPassExecution("graph", config);
67
- }
68
- const EMPTY_QUALITY = {
69
- consideredFiles: 0,
70
- extractedFiles: 0,
71
- entityCount: 0,
72
- relationCount: 0,
73
- extractionCoverage: 0,
74
- density: 0,
75
- };
76
- const EMPTY_RESULT = {
77
- considered: 0,
78
- extracted: 0,
79
- totalEntities: 0,
80
- totalRelations: 0,
81
- written: false,
82
- quality: { ...EMPTY_QUALITY },
83
- telemetry: {
84
- cacheHits: 0,
85
- cacheMisses: 0,
86
- truncationCount: 0,
87
- failureCount: 0,
88
- retryAttempts: 0,
89
- },
90
- warnings: [],
91
- };
92
- function roundMetric(value) {
93
- return Number(value.toFixed(4));
94
- }
95
- function computeGraphQualityTelemetry(consideredFiles, extractedFiles, entityCount, relationCount) {
96
- const extractionCoverage = consideredFiles > 0 ? extractedFiles / consideredFiles : 0;
97
- const maxEdges = entityCount > 1 ? (entityCount * (entityCount - 1)) / 2 : 0;
98
- const density = maxEdges > 0 ? relationCount / maxEdges : 0;
99
- return {
100
- consideredFiles,
101
- extractedFiles,
102
- entityCount,
103
- relationCount,
104
- extractionCoverage: roundMetric(extractionCoverage),
105
- density: roundMetric(density),
106
- };
107
- }
108
- export const DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES = ["memory", "knowledge"];
109
- /**
110
- * Max number of lazy-extraction queue rows drained per pass (#624-P3). Bounds
111
- * per-run work so a large backlog is spread across runs rather than processed
112
- * all at once. Generous default — the queue is normally near-empty.
113
- */
114
- const GRAPH_EXTRACTION_QUEUE_DRAIN_LIMIT = 100;
115
- const SUPPORTED_GRAPH_EXTRACTION_INCLUDE_TYPES = new Set([
116
- "memory",
117
- "knowledge",
118
- "skill",
119
- "command",
120
- "agent",
121
- "workflow",
122
- "lesson",
123
- "task",
124
- ]);
125
- const GRAPH_CACHE_VARIANT_PREFIX = "graph-extraction";
126
- function normalizeConfidence(raw) {
127
- if (typeof raw !== "number" || !Number.isFinite(raw))
128
- return undefined;
129
- return Math.max(0, Math.min(1, raw));
130
- }
131
- function getGraphExtractorId(config) {
132
- const fingerprint = computeBodyHash(JSON.stringify({
133
- promptVersion: graphExtract.GRAPH_EXTRACT_PROMPT_VERSION,
134
- model: config.model,
135
- batchSize: config.batchSize,
136
- includeTypes: config.includeTypes,
137
- maxChunkBodyChars: 1600,
138
- maxBatchBodyChars: 1600,
139
- })).slice(0, 16);
140
- return `${GRAPH_CACHE_VARIANT_PREFIX}:${graphExtract.GRAPH_EXTRACT_PROMPT_VERSION}:${config.model}:${fingerprint}`;
141
- }
142
- function buildLowQualityWarnings(quality, telemetry) {
143
- const warnings = [];
144
- if (quality.consideredFiles >= 5 && quality.extractionCoverage < 0.3) {
145
- warnings.push(`Low graph extraction coverage (${quality.extractedFiles}/${quality.consideredFiles}, ${quality.extractionCoverage}).`);
146
- }
147
- if (quality.entityCount >= 8 && quality.relationCount === 0) {
148
- warnings.push("Graph extraction produced many entities but no relations.");
149
- }
150
- if (telemetry.failureCount > 0) {
151
- warnings.push(`Graph extraction encountered ${telemetry.failureCount} failed file extraction(s).`);
152
- }
153
- return warnings;
154
- }
155
- export function getGraphExtractionIncludeTypes(config) {
156
- const configured = getIndexPassConfig(config.index, "graph")?.graphExtractionIncludeTypes;
157
- if (!configured || configured.length === 0)
158
- return [...DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES];
159
- const out = [];
160
- const seen = new Set();
161
- for (const rawType of configured) {
162
- const type = rawType.trim().toLowerCase();
163
- if (!type || seen.has(type))
164
- continue;
165
- if (!SUPPORTED_GRAPH_EXTRACTION_INCLUDE_TYPES.has(type))
166
- continue;
167
- seen.add(type);
168
- out.push(type);
169
- }
170
- return out.length > 0 ? out : [...DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES];
171
- }
172
- function validateGraphCacheShape(raw) {
173
- if (!raw || typeof raw !== "object")
174
- return undefined;
175
- const obj = raw;
176
- if (!Array.isArray(obj.entities) || !obj.entities.every((e) => typeof e === "string"))
177
- return undefined;
178
- if (obj.relations !== undefined &&
179
- (!Array.isArray(obj.relations) ||
180
- !obj.relations.every((r) => {
181
- if (!r || typeof r !== "object")
182
- return false;
183
- const rel = r;
184
- if (typeof rel.from !== "string" || typeof rel.to !== "string")
185
- return false;
186
- if (rel.type !== undefined && typeof rel.type !== "string")
187
- return false;
188
- if (rel.confidence !== undefined && (typeof rel.confidence !== "number" || !Number.isFinite(rel.confidence))) {
189
- return false;
190
- }
191
- return true;
192
- }))) {
193
- return undefined;
194
- }
195
- return {
196
- entities: obj.entities,
197
- relations: Array.isArray(obj.relations) ? obj.relations : [],
198
- confidence: normalizeConfidence(obj.confidence),
199
- ...(typeof obj.status === "string" ? { status: obj.status } : {}),
200
- ...(typeof obj.reason === "string" ? { reason: obj.reason } : {}),
201
- };
202
- }
203
- function loadGraphFile(stashRoot, db) {
204
- if (!db)
205
- return { files: [] };
206
- const graph = loadStoredGraphSnapshot(stashRoot, db);
207
- if (!graph)
208
- return { files: [] };
209
- const out = [];
210
- for (const node of graph.files) {
211
- const cacheShape = validateGraphCacheShape({ entities: node.entities, relations: node.relations });
212
- if (!cacheShape)
213
- continue;
214
- out.push({
215
- path: node.path,
216
- type: node.type,
217
- bodyHash: node.bodyHash,
218
- entities: cacheShape.entities,
219
- relations: cacheShape.relations,
220
- confidence: normalizeConfidence(node.confidence),
221
- ...(node.status ? { status: node.status } : {}),
222
- ...(node.reason ? { reason: node.reason } : {}),
223
- ...(node.extractionRunId ? { extractionRunId: node.extractionRunId } : {}),
224
- });
225
- }
226
- return {
227
- files: out,
228
- ...(graph.telemetry ? { telemetry: graph.telemetry } : {}),
229
- };
230
- }
231
- function mergeGraphNodes(previousNodes, refreshedNodes, candidatePaths) {
232
- if (!candidatePaths)
233
- return refreshedNodes;
234
- const refreshedByPath = new Map(refreshedNodes.map((node) => [node.path, node]));
235
- const merged = [];
236
- for (const node of previousNodes) {
237
- if (candidatePaths.has(node.path))
238
- continue;
239
- merged.push(node);
240
- }
241
- for (const node of refreshedNodes)
242
- merged.push(refreshedByPath.get(node.path) ?? node);
243
- return merged;
244
- }
245
- function reuseGraphNode(previousNodes, candidate, bodyHash) {
246
- const node = previousNodes.get(candidate.absPath);
247
- if (!node)
248
- return undefined;
249
- if (node.type !== candidate.type)
250
- return undefined;
251
- if (typeof node.bodyHash !== "string" || node.bodyHash.length === 0)
252
- return undefined;
253
- if (node.bodyHash !== bodyHash)
254
- return undefined;
255
- const validated = validateGraphCacheShape({ entities: node.entities, relations: node.relations });
256
- if (!validated)
257
- return undefined;
258
- return {
259
- entities: validated.entities,
260
- relations: validated.relations,
261
- confidence: normalizeConfidence(node.confidence),
262
- ...(node.status ? { status: node.status } : {}),
263
- ...(node.reason ? { reason: node.reason } : {}),
264
- };
265
- }
266
- function planEligibleGraphExtractions(args) {
267
- const { eligible, db, reEnrich, cacheVariant, previousNodes, canReusePreviousGraph } = args;
268
- const bodyHashes = eligible.map((candidate) => computeBodyHash(candidate.body));
269
- const cacheEntries = db && !reEnrich
270
- ? getLlmCacheEntriesByRefs(db, eligible.map((candidate) => candidate.absPath), cacheVariant)
271
- : new Map();
272
- return eligible.map((candidate, index) => {
273
- const bodyHash = bodyHashes[index] ?? "";
274
- if (!reEnrich && db) {
275
- const entry = cacheEntries.get(candidate.absPath);
276
- if (entry?.bodyHash === bodyHash) {
277
- try {
278
- const cached = validateGraphCacheShape(JSON.parse(entry.resultJson));
279
- if (cached)
280
- return { kind: "cache-hit", candidate, bodyHash, cached, persistCache: false };
281
- }
282
- catch {
283
- // Corrupt cache rows are immutable model plans for this pass.
284
- }
285
- }
286
- }
287
- if (!reEnrich && (!db || canReusePreviousGraph)) {
288
- const cached = reuseGraphNode(previousNodes, candidate, bodyHash);
289
- if (cached)
290
- return { kind: "cache-hit", candidate, bodyHash, cached, persistCache: Boolean(db) };
291
- }
292
- return { kind: "model", candidate, bodyHash };
293
- });
294
- }
295
- function graphRecordFromCachePlan(plan) {
296
- return {
297
- absPath: plan.candidate.absPath,
298
- type: plan.candidate.type,
299
- bodyHash: plan.bodyHash,
300
- entities: plan.cached.entities,
301
- relations: plan.cached.relations,
302
- ...(plan.cached.confidence !== undefined ? { confidence: plan.cached.confidence } : {}),
303
- ...(plan.cached.status ? { status: plan.cached.status } : {}),
304
- ...(plan.cached.reason ? { reason: plan.cached.reason } : {}),
305
- };
306
- }
307
- async function extractGraphBatches(args) {
308
- const { plans, batchSize, signal, db, cacheVariant, telemetry, llmRunner, lease, featureConfig, onFallback, batchState, runtimeTelemetry, onNotices, reportProgress, } = args;
309
- const results = new Array(plans.length).fill(undefined);
310
- const chunkStarts = [];
311
- for (let start = 0; start < plans.length; start += batchSize)
312
- chunkStarts.push(start);
313
- let configFailure;
314
- await concurrentMap(chunkStarts, async (start) => {
315
- if (signal?.aborted)
316
- return;
317
- const chunk = plans.slice(start, start + batchSize);
318
- const reportChunkProgress = () => {
319
- for (let j = 0; j < chunk.length; j++) {
320
- const plan = chunk[j];
321
- if (plan)
322
- reportProgress(plan.candidate.absPath, results[start + j]);
323
- }
324
- };
325
- for (let index = 0; index < chunk.length; index++) {
326
- const plan = chunk[index];
327
- if (!plan || plan.kind !== "cache-hit")
328
- continue;
329
- telemetry.cacheHits += 1;
330
- results[start + index] = graphRecordFromCachePlan(plan);
331
- if (db && plan.persistCache) {
332
- upsertLlmCacheEntry(db, plan.candidate.absPath, plan.bodyHash, JSON.stringify(plan.cached), cacheVariant);
333
- }
334
- }
335
- const modelPlans = chunk.filter((plan) => plan.kind === "model");
336
- if (modelPlans.length === 0) {
337
- reportChunkProgress();
338
- return;
339
- }
340
- telemetry.cacheMisses += modelPlans.length;
341
- let batchExtractions;
342
- try {
343
- batchExtractions = await graphExtract.extractGraphFromBodies(llmRunner, modelPlans.map((plan) => plan.candidate.body), signal, featureConfig, onFallback, {
344
- batchState,
345
- telemetry: runtimeTelemetry,
346
- onNotices,
347
- ...(lease ? { lease } : {}),
348
- });
349
- }
350
- catch (error) {
351
- if (error instanceof ConfigError) {
352
- configFailure ??= error;
353
- return;
354
- }
355
- throw error;
356
- }
357
- let llmIndex = 0;
358
- for (let index = 0; index < chunk.length; index++) {
359
- const plan = chunk[index];
360
- if (!plan || plan.kind !== "model")
361
- continue;
362
- const extraction = batchExtractions[llmIndex++];
363
- if (!extraction)
364
- continue;
365
- const cacheShape = {
366
- entities: extraction.entities,
367
- relations: extraction.relations,
368
- ...(extraction.confidence !== undefined ? { confidence: extraction.confidence } : {}),
369
- ...(extraction.status ? { status: extraction.status } : {}),
370
- ...(extraction.reason ? { reason: extraction.reason } : {}),
371
- };
372
- if (db) {
373
- upsertLlmCacheEntry(db, plan.candidate.absPath, plan.bodyHash, JSON.stringify(cacheShape), cacheVariant);
374
- }
375
- results[start + index] = {
376
- absPath: plan.candidate.absPath,
377
- type: plan.candidate.type,
378
- bodyHash: plan.bodyHash,
379
- ...cacheShape,
380
- };
381
- }
382
- reportChunkProgress();
383
- }, llmRunner.connection.concurrency ?? 1);
384
- return { results, ...(configFailure ? { configFailure } : {}) };
385
- }
386
- function readCurrentGraphBodyHash(filePath) {
387
- try {
388
- const body = parseFrontmatter(fs.readFileSync(filePath, "utf8")).content.trim();
389
- return body ? computeBodyHash(body) : undefined;
390
- }
391
- catch {
392
- return undefined;
393
- }
394
- }
395
- function planQueuedGraphExtractions(args) {
396
- const { db, stashRoot, previousNodes, signal, reEnrich } = args;
397
- if (!db)
398
- return [];
399
- return peekExtractionQueue(db, stashRoot, GRAPH_EXTRACTION_QUEUE_DRAIN_LIMIT).map((queued) => {
400
- if (signal?.aborted) {
401
- return {
402
- kind: "deferred",
403
- filePath: queued.filePath,
404
- queuedBodyHash: queued.bodyHash,
405
- priority: queued.priority,
406
- };
407
- }
408
- let raw;
409
- try {
410
- raw = fs.readFileSync(queued.filePath, "utf8");
411
- }
412
- catch {
413
- return { kind: "discard", filePath: queued.filePath, queuedBodyHash: queued.bodyHash, priority: queued.priority };
414
- }
415
- const body = parseFrontmatter(raw).content.trim();
416
- if (!body) {
417
- return { kind: "discard", filePath: queued.filePath, queuedBodyHash: queued.bodyHash, priority: queued.priority };
418
- }
419
- const currentBodyHash = computeBodyHash(body);
420
- const type = inferGraphTypeForPath(stashRoot, queued.filePath) ?? "memory";
421
- if (!reEnrich && reuseGraphNode(previousNodes, { absPath: queued.filePath, type }, currentBodyHash)) {
422
- return {
423
- kind: "hit",
424
- filePath: queued.filePath,
425
- queuedBodyHash: queued.bodyHash,
426
- currentBodyHash,
427
- priority: queued.priority,
428
- };
429
- }
430
- return {
431
- kind: "model",
432
- filePath: queued.filePath,
433
- queuedBodyHash: queued.bodyHash,
434
- currentBodyHash,
435
- priority: queued.priority,
436
- };
437
- });
438
- }
439
- async function executeQueuedGraphPlans(args) {
440
- const { plans, db, stashRoot, featureConfig, signal, llmRunner, lease, onNotices } = args;
441
- if (!db)
442
- return { graphChanged: false, acknowledgements: [] };
443
- let graphChanged = false;
444
- const acknowledgements = [];
445
- for (const plan of plans) {
446
- if (signal?.aborted || plan.kind === "deferred")
447
- break;
448
- if (plan.kind === "discard") {
449
- const currentBodyHash = readCurrentGraphBodyHash(plan.filePath);
450
- if (currentBodyHash) {
451
- enqueueGraphExtraction(db, stashRoot, plan.filePath, currentBodyHash, plan.priority);
452
- continue;
453
- }
454
- acknowledgements.push({ filePath: plan.filePath, queuedBodyHash: plan.queuedBodyHash });
455
- continue;
456
- }
457
- if (plan.kind === "hit") {
458
- const currentBodyHash = readCurrentGraphBodyHash(plan.filePath);
459
- if (currentBodyHash !== plan.currentBodyHash) {
460
- if (currentBodyHash)
461
- enqueueGraphExtraction(db, stashRoot, plan.filePath, currentBodyHash, plan.priority);
462
- continue;
463
- }
464
- acknowledgements.push({ filePath: plan.filePath, queuedBodyHash: plan.queuedBodyHash });
465
- continue;
466
- }
467
- if (plan.kind === "model") {
468
- const outcome = await extractGraphForSingleFileRevision(db, stashRoot, plan.filePath, {
469
- config: featureConfig,
470
- signal,
471
- llmRunner,
472
- lease,
473
- onNotices,
474
- });
475
- if (!outcome.written)
476
- continue;
477
- graphChanged = true;
478
- const currentBodyHash = readCurrentGraphBodyHash(plan.filePath);
479
- if (currentBodyHash !== outcome.bodyHash) {
480
- if (currentBodyHash)
481
- enqueueGraphExtraction(db, stashRoot, plan.filePath, currentBodyHash, plan.priority);
482
- continue;
483
- }
484
- }
485
- acknowledgements.push({ filePath: plan.filePath, queuedBodyHash: plan.queuedBodyHash });
486
- }
487
- return { graphChanged, acknowledgements };
488
- }
489
- function acknowledgeQueuedGraphPlans(db, stashRoot, execution) {
490
- if (!db)
491
- return;
492
- for (const intent of execution.acknowledgements) {
493
- acknowledgeExtractionQueueEntry(db, stashRoot, intent.filePath, intent.queuedBodyHash);
494
- }
495
- }
496
- /**
497
- * Top-level entry point. Returns a no-op result when the pass is disabled.
498
- *
499
- * Three preconditions — ALL must hold for the pass to run:
500
- *
501
- * 1. **Provider configured** — an LLM profile must be selectable. Without a
502
- * configured provider, `resolveIndexPassExecution("graph", config).runner`
503
- * is `undefined` (the pass cannot run because there is no model to call).
504
- * 2. **Feature gate** — the selected strategy's `processes.graphExtraction.enabled`
505
- * (defaults to `true`). When `false`, no network call may issue regardless
506
- * of per-pass settings.
507
- * 3. **Per-pass gate** — `index.graph.llm` (defaults to `true`). When
508
- * `false`, the indexer simply skips this pass for the current run.
509
- *
510
- * If any of the three is missing or `false`, this function short-circuits
511
- * to an empty no-op result, leaving any existing persisted graph untouched.
512
- *
513
- * When `config.index.graph.graphExtractionBatchSize > 1`, eligible files are
514
- * chunked into batches and each chunk is processed with a single LLM call via
515
- * `extractGraphFromBodies`. Default batch size is 1 (one call per asset —
516
- * preserves existing behaviour, fully opt-in).
517
- */
518
- export async function runGraphExtractionPass(ctx) {
519
- const { config, sources, signal, db, reEnrich, onProgress, options = {} } = ctx;
520
- const invocationOwnsRunner = Object.hasOwn(ctx, "llmRunner");
521
- // Gate 1 — feature gate via isProcessEnabled, which reads the 0.8.0 path
522
- // (selected strategy's processes.graphExtraction.enabled). Defaults to
523
- // enabled when the key is absent.
524
- if (!invocationOwnsRunner && !isProcessEnabled("index", "graph_extraction", config))
525
- return { ...EMPTY_RESULT };
526
- const noticesByKey = new Map();
527
- const onNotices = (notices) => {
528
- for (const notice of notices)
529
- noticesByKey.set(JSON.stringify(notice), notice);
530
- };
531
- const emptyResult = () => ({
532
- ...EMPTY_RESULT,
533
- ...(noticesByKey.size > 0 ? { notices: Object.freeze([...noticesByKey.values()]) } : {}),
534
- });
535
- // Gate 2 — per-pass opt-out (#208). Retain the whole frozen resolution so
536
- // selection-time lowering notices cannot be separated from the runner.
537
- const execution = graphExecutionForContext(ctx, config);
538
- onNotices(execution.notices);
539
- const llmRunner = execution.runner;
540
- if (!llmRunner) {
541
- const reason = getIndexPassConfig(config.index, "graph")?.enabled === false
542
- ? "index.graph.enabled is false"
543
- : "no LLM engine is configured";
544
- warnVerbose(`graph extraction: skipped because ${reason}.`);
545
- return emptyResult();
546
- }
547
- const featureConfig = invocationOwnsRunner
548
- ? { ...config, index: { ...config.index, graph: { ...config.index?.graph, enabled: true } } }
549
- : config;
550
- // The pass only writes to the primary (working) stash. Read-only caches
551
- // (git, npm, website) are deliberately untouched — the graph artifact for
552
- // those sources would be clobbered by the next sync().
553
- const primary = sources[0];
554
- if (!primary) {
555
- warnVerbose("graph extraction: skipped because no primary stash source is available.");
556
- return emptyResult();
557
- }
558
- const includeTypes = options.includeTypes ?? getGraphExtractionIncludeTypes(config);
559
- let previousGraph = loadGraphFile(primary.path, db);
560
- const previousNodes = new Map(previousGraph.files.map((node) => [node.path, node]));
561
- const batchSize = resolveBatchSize(options.batchSize ?? getIndexPassConfig(config.index, "graph")?.graphExtractionBatchSize, llmRunner.connection.contextLength);
562
- const extractorId = getGraphExtractorId({ model: llmRunner.connection.model, batchSize, includeTypes });
563
- const cacheVariant = extractorId;
564
- const canReusePreviousGraph = previousGraph.telemetry?.extractorId === extractorId;
565
- const queuePlans = planQueuedGraphExtractions({
566
- db,
567
- stashRoot: primary.path,
568
- previousNodes,
569
- signal,
570
- reEnrich,
571
- });
572
- const queuedPaths = new Set(queuePlans.map((plan) => plan.filePath));
573
- let eligible = collectEligibleFiles(primary.path, includeTypes).filter((candidate) => (!options.candidatePaths || options.candidatePaths.has(candidate.absPath)) && !queuedPaths.has(candidate.absPath));
574
- // P2 (#624): when topN is set and a DB is available, rank the (already
575
- // candidate-filtered) eligible set by utility_scores DESC and keep only the
576
- // top-N. Default (topN unset) is byte-identical to today — no ranking query
577
- // is issued and the eligible set is untouched. Ranking composes WITH the
578
- // candidatePaths filter: scoped-then-ranked-then-sliced.
579
- if (db && options.topN != null && options.topN >= 0) {
580
- eligible = rankCandidatesByUtility(db, eligible, primary.path).slice(0, options.topN);
581
- }
582
- const considered = eligible.length;
583
- const eligiblePlans = planEligibleGraphExtractions({
584
- eligible,
585
- db,
586
- reEnrich,
587
- cacheVariant,
588
- previousNodes,
589
- canReusePreviousGraph,
590
- });
591
- const queueNeedsModel = queuePlans.some((plan) => plan.kind === "model");
592
- const eligibleNeedsModel = !signal?.aborted && eligiblePlans.some((plan) => plan.kind === "model");
593
- if (signal?.aborted)
594
- return emptyResult();
595
- // Validate exactly once iff classification found real model work. Queue
596
- // acknowledgements, cache writes, and graph replacement all happen after
597
- // this boundary, so a missing credential cannot partially mutate a batch.
598
- const dispatchLease = queueNeedsModel || eligibleNeedsModel ? await preflightStructuredLlmRunner(llmRunner) : undefined;
599
- try {
600
- const queueExecution = await executeQueuedGraphPlans({
601
- plans: queuePlans,
602
- db,
603
- stashRoot: primary.path,
604
- featureConfig,
605
- signal,
606
- llmRunner,
607
- lease: dispatchLease,
608
- onNotices,
609
- });
610
- if (queueExecution.graphChanged) {
611
- previousGraph = loadGraphFile(primary.path, db);
612
- }
613
- if (considered === 0) {
614
- acknowledgeQueuedGraphPlans(db, primary.path, queueExecution);
615
- const scoped = options.candidatePaths ? ` matching ${options.candidatePaths.size} candidate path(s)` : "";
616
- warnVerbose(`graph extraction: skipped because no eligible files${scoped} were found under ${primary.path}. ` +
617
- `includeTypes=${includeTypes.join(",")}`);
618
- return emptyResult();
619
- }
620
- const nodes = [];
621
- let totalEntities = 0;
622
- let totalRelations = 0;
623
- let processed = 0;
624
- let extracted = 0;
625
- onProgress?.({ processed, total: considered, extracted, totalEntities, totalRelations });
626
- const reportProgress = (currentPath, result) => {
627
- processed += 1;
628
- if (result) {
629
- if (result.entities.length > 0)
630
- extracted += 1;
631
- totalEntities += result.entities.length;
632
- totalRelations += result.relations.length;
633
- }
634
- onProgress?.({
635
- processed,
636
- total: considered,
637
- extracted,
638
- totalEntities,
639
- totalRelations,
640
- currentPath,
641
- });
642
- };
643
- const extractionRunId = crypto.randomUUID();
644
- const telemetry = {
645
- extractorId,
646
- extractionRunId,
647
- model: llmRunner.connection.model,
648
- promptVersion: graphExtract.GRAPH_EXTRACT_PROMPT_VERSION,
649
- batchSize,
650
- cacheHits: 0,
651
- cacheMisses: 0,
652
- truncationCount: 0,
653
- failureCount: 0,
654
- htmlErrorCount: 0,
655
- retryAttempts: 0,
656
- nonArrayBatchFailures: 0,
657
- };
658
- const runtimeTelemetry = {
659
- truncationCount: 0,
660
- failureCount: 0,
661
- htmlErrorCount: 0,
662
- retryAttempts: 0,
663
- filteredGenericEntities: 0,
664
- filteredInvalidRelations: 0,
665
- filteredLowConfidenceRelations: 0,
666
- contextBatchRetries: 0,
667
- nonArrayBatchFailures: 0,
668
- };
669
- const batchState = {
670
- batchingDisabled: false,
671
- nonArrayBatchFailures: 0,
672
- };
673
- warnVerbose(`graph extraction: starting for ${considered} eligible file(s) under ${primary.path}; ` +
674
- `includeTypes=${includeTypes.join(",")}, batchSize=${batchSize}, concurrency=${llmRunner.connection.concurrency ?? 1}, ` +
675
- `reEnrich=${reEnrich === true}, candidateScoped=${options.candidatePaths ? "true" : "false"}.`);
676
- const onFallback = (evt) => {
677
- warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
678
- };
679
- let extractionResults;
680
- let configFailure;
681
- if (batchSize <= 1) {
682
- // ── Original per-asset path (with incremental cache) ─────────────────
683
- extractionResults = await concurrentMap(eligiblePlans, async (plan) => {
684
- const { candidate, bodyHash } = plan;
685
- if (signal?.aborted) {
686
- reportProgress(candidate.absPath, undefined);
687
- return undefined;
688
- }
689
- let cached;
690
- if (plan.kind === "cache-hit") {
691
- telemetry.cacheHits += 1;
692
- cached = plan.cached;
693
- if (db && plan.persistCache) {
694
- upsertLlmCacheEntry(db, candidate.absPath, bodyHash, JSON.stringify(cached), cacheVariant);
695
- }
696
- }
697
- else {
698
- telemetry.cacheMisses += 1;
699
- let extraction;
700
- try {
701
- extraction = await graphExtract.extractGraphFromBody(llmRunner, candidate.body, signal, featureConfig, onFallback, {
702
- batchState,
703
- telemetry: runtimeTelemetry,
704
- onNotices,
705
- ...(dispatchLease ? { lease: dispatchLease } : {}),
706
- });
707
- }
708
- catch (err) {
709
- if (err instanceof ConfigError) {
710
- configFailure ??= err;
711
- return undefined;
712
- }
713
- throw err;
714
- }
715
- cached = {
716
- entities: extraction.entities,
717
- relations: extraction.relations,
718
- ...(extraction.confidence !== undefined ? { confidence: extraction.confidence } : {}),
719
- ...(extraction.status ? { status: extraction.status } : {}),
720
- ...(extraction.reason ? { reason: extraction.reason } : {}),
721
- };
722
- if (db) {
723
- upsertLlmCacheEntry(db, candidate.absPath, bodyHash, JSON.stringify(cached), cacheVariant);
724
- }
725
- }
726
- const result = {
727
- absPath: candidate.absPath,
728
- type: candidate.type,
729
- bodyHash,
730
- entities: cached.entities,
731
- relations: cached.relations,
732
- ...(cached.confidence !== undefined ? { confidence: cached.confidence } : {}),
733
- ...(cached.status ? { status: cached.status } : {}),
734
- ...(cached.reason ? { reason: cached.reason } : {}),
735
- };
736
- reportProgress(candidate.absPath, result);
737
- return result;
738
- },
739
- // Caller-set connection concurrency or 1: `resolveLlmEngineUse` does
740
- // not forward `engines.<name>.concurrency`, so config cannot raise this.
741
- llmRunner.connection.concurrency ?? 1);
742
- }
743
- else {
744
- const batch = await extractGraphBatches({
745
- plans: eligiblePlans,
746
- batchSize,
747
- signal,
748
- db,
749
- cacheVariant,
750
- telemetry,
751
- llmRunner,
752
- lease: dispatchLease,
753
- featureConfig,
754
- onFallback,
755
- batchState,
756
- runtimeTelemetry,
757
- onNotices,
758
- reportProgress,
759
- });
760
- extractionResults = batch.results;
761
- configFailure ??= batch.configFailure;
762
- }
763
- if (configFailure)
764
- throw configFailure;
765
- acknowledgeQueuedGraphPlans(db, primary.path, queueExecution);
766
- for (const result of extractionResults) {
767
- if (!result)
768
- continue;
769
- nodes.push({
770
- path: result.absPath,
771
- type: result.type,
772
- bodyHash: result.bodyHash,
773
- entities: [...new Set(result.entities.map((entity) => entity.trim()).filter(Boolean))],
774
- relations: result.relations
775
- .map((r) => ({
776
- from: r.from.trim(),
777
- to: r.to.trim(),
778
- ...(r.type ? { type: r.type.trim() } : {}),
779
- ...(normalizeConfidence(r.confidence) !== undefined
780
- ? { confidence: normalizeConfidence(r.confidence) }
781
- : {}),
782
- }))
783
- .filter((relation) => relation.from && relation.to),
784
- ...(normalizeConfidence(result.confidence) !== undefined
785
- ? { confidence: normalizeConfidence(result.confidence) }
786
- : {}),
787
- status: result.status ?? (result.entities.length > 0 ? "extracted" : "empty"),
788
- reason: result.reason ?? (result.entities.length > 0 ? "none" : "no_graph_content"),
789
- extractionRunId,
790
- });
791
- }
792
- const queuedNodes = options.candidatePaths
793
- ? []
794
- : previousGraph.files.filter((node) => queuePlans.some((plan) => plan.filePath === node.path && plan.kind !== "discard") &&
795
- !nodes.some((candidate) => candidate.path === node.path));
796
- const mergedNodes = mergeGraphNodes(previousGraph.files, [...queuedNodes, ...nodes], options.candidatePaths);
797
- const assetRefs = mergedNodes.map((node) => node.path);
798
- const deduped = deduplicateGraph(mergedNodes.map((node) => ({ entities: node.entities, relations: node.relations })), assetRefs);
799
- telemetry.truncationCount = runtimeTelemetry.truncationCount ?? 0;
800
- telemetry.failureCount = runtimeTelemetry.failureCount ?? 0;
801
- telemetry.htmlErrorCount = runtimeTelemetry.htmlErrorCount ?? 0;
802
- telemetry.retryAttempts = runtimeTelemetry.retryAttempts ?? 0;
803
- telemetry.nonArrayBatchFailures = runtimeTelemetry.nonArrayBatchFailures ?? 0;
804
- const qualityConsidered = mergedNodes.length;
805
- const qualityExtracted = mergedNodes.filter((node) => node.status === "extracted" && node.entities.length > 0).length;
806
- const quality = computeGraphQualityTelemetry(qualityConsidered, qualityExtracted, deduped.entities.length, deduped.relations.length);
807
- const warnings = buildLowQualityWarnings(quality, telemetry);
808
- for (const warning of warnings)
809
- warnVerbose(`graph extraction quality: ${warning}`);
810
- const graph = {
811
- schemaVersion: GRAPH_FILE_SCHEMA_VERSION,
812
- generatedAt: new Date().toISOString(),
813
- stashRoot: primary.path,
814
- files: mergedNodes,
815
- entities: deduped.entities,
816
- relations: deduped.relations,
817
- quality,
818
- telemetry,
819
- };
820
- const written = writeGraphFile(primary.path, graph, db);
821
- warnVerbose(`graph extraction: ${written ? "persisted" : "did not persist"} graph for ${primary.path}; ` +
822
- `considered=${considered}, extractedThisRun=${extracted}, storedFiles=${mergedNodes.length}, ` +
823
- `entities=${deduped.entities.length}, relations=${deduped.relations.length}, coverage=${quality.extractionCoverage}.`);
824
- return {
825
- considered,
826
- extracted,
827
- totalEntities,
828
- totalRelations,
829
- written,
830
- quality,
831
- telemetry,
832
- warnings,
833
- ...(noticesByKey.size > 0 ? { notices: Object.freeze([...noticesByKey.values()]) } : {}),
834
- };
835
- }
836
- finally {
837
- if (dispatchLease)
838
- disposeLoweredExecutionDispatchLease(dispatchLease);
839
- }
840
- }
841
- /**
842
- * Infer the asset type (`memory`, `knowledge`, …) for a path from the stash
843
- * directory segment it lives under. Returns the matching include-type, or
844
- * `undefined` when the path is not under a known graph-eligible type dir.
845
- */
846
- function inferGraphTypeForPath(stashRoot, absPath) {
847
- const rel = path.relative(stashRoot, absPath);
848
- const firstSeg = rel.split(path.sep)[0];
849
- if (!firstSeg)
850
- return undefined;
851
- for (const type of SUPPORTED_GRAPH_EXTRACTION_INCLUDE_TYPES) {
852
- if (stashDirFor(type) === firstSeg)
853
- return type;
854
- }
855
- return undefined;
856
- }
857
- /**
858
- * #624-P3 — extract graph data for a SINGLE file and merge it into the stored
859
- * graph WITHOUT clobbering other files' rows.
860
- *
861
- * Re-reads the body from disk at call time (the queued body_hash is NOT trusted
862
- * blindly — the file may have been deleted or changed since enqueue) and skips
863
- * silently (returns `false`) when the file is gone or empty. Resolves a frozen
864
- * LLM execution via {@link resolveIndexPassExecution} (model-available guard:
865
- * returns `false` when no provider is configured) UNLESS a caller supplies
866
- * `opts.llmRunner` or `opts.llmOverride`. Those seams let a command reuse an
867
- * invocation-owned selection or provide a test extractor without re-resolving.
868
- *
869
- * Returns `true` when a graph row was written for the file, `false` on any
870
- * skip (missing file, empty body, unknown type, no model, or extraction error).
871
- */
872
- async function extractGraphForSingleFileRevision(db, stashRoot, filePath, opts) {
873
- let ownedLease;
874
- try {
875
- // Re-read from disk — never trust a stale queued body.
876
- let raw;
877
- try {
878
- raw = fs.readFileSync(filePath, "utf8");
879
- }
880
- catch {
881
- return { written: false }; // file gone / unreadable → silent skip
882
- }
883
- const parsed = parseFrontmatter(raw);
884
- const body = parsed.content.trim();
885
- if (!body)
886
- return { written: false };
887
- const type = inferGraphTypeForPath(stashRoot, filePath) ?? "memory";
888
- const effectiveHash = computeBodyHash(body);
889
- // Extract — via the injected seam, or the real per-asset path.
890
- let extraction;
891
- if (opts?.llmOverride) {
892
- const out = await opts.llmOverride(body);
893
- extraction = {
894
- entities: out.entities,
895
- relations: out.relations,
896
- ...(out.confidence !== undefined ? { confidence: out.confidence } : {}),
897
- };
898
- }
899
- else {
900
- const config = opts?.config ?? loadConfig();
901
- const invocationOwnsRunner = Object.hasOwn(opts ?? {}, "llmRunner");
902
- if (!invocationOwnsRunner && !isProcessEnabled("index", "graph_extraction", config))
903
- return { written: false };
904
- const execution = Object.hasOwn(opts ?? {}, "llmRunner")
905
- ? Object.freeze({ runner: opts?.llmRunner ?? undefined, notices: Object.freeze([]) })
906
- : resolveIndexPassExecution("graph", config);
907
- opts?.onNotices?.(execution.notices);
908
- const llmRunner = execution.runner;
909
- if (!llmRunner)
910
- return { written: false }; // model-available guard
911
- const featureConfig = invocationOwnsRunner
912
- ? { ...config, index: { ...config.index, graph: { ...config.index?.graph, enabled: true } } }
913
- : config;
914
- const lease = opts?.lease ?? (await preflightStructuredLlmRunner(llmRunner));
915
- if (!opts?.lease)
916
- ownedLease = lease;
917
- const result = await graphExtract.extractGraphFromBody(llmRunner, body, opts?.signal, featureConfig, undefined, {
918
- ...(opts?.onNotices ? { onNotices: opts.onNotices } : {}),
919
- lease,
920
- });
921
- extraction = {
922
- entities: result.entities,
923
- relations: result.relations,
924
- ...(result.confidence !== undefined ? { confidence: result.confidence } : {}),
925
- };
926
- }
927
- const entities = [...new Set(extraction.entities.map((e) => e.trim()).filter(Boolean))];
928
- const relations = extraction.relations
929
- .map((r) => ({
930
- from: r.from.trim(),
931
- to: r.to.trim(),
932
- ...(r.type ? { type: r.type.trim() } : {}),
933
- ...(normalizeConfidence(r.confidence) !== undefined ? { confidence: normalizeConfidence(r.confidence) } : {}),
934
- }))
935
- .filter((r) => r.from && r.to);
936
- const node = {
937
- path: filePath,
938
- type,
939
- bodyHash: effectiveHash,
940
- entities,
941
- relations,
942
- ...(normalizeConfidence(extraction.confidence) !== undefined
943
- ? { confidence: normalizeConfidence(extraction.confidence) }
944
- : {}),
945
- status: entities.length > 0 ? "extracted" : "empty",
946
- reason: entities.length > 0 ? "none" : "no_graph_content",
947
- extractionRunId: crypto.randomUUID(),
948
- };
949
- // Merge with the previously-stored nodes, scoping the refresh to JUST this
950
- // path so other files' rows are preserved (and graph_meta counts refresh).
951
- const previousGraph = loadGraphFile(stashRoot, db);
952
- const candidatePaths = new Set([filePath]);
953
- const mergedNodes = mergeGraphNodes(previousGraph.files, [node], candidatePaths);
954
- const assetRefs = mergedNodes.map((n) => n.path);
955
- const deduped = deduplicateGraph(mergedNodes.map((n) => ({ entities: n.entities, relations: n.relations })), assetRefs);
956
- const qualityExtracted = mergedNodes.filter((n) => n.status === "extracted" && n.entities.length > 0).length;
957
- const quality = computeGraphQualityTelemetry(mergedNodes.length, qualityExtracted, deduped.entities.length, deduped.relations.length);
958
- const graph = {
959
- schemaVersion: GRAPH_FILE_SCHEMA_VERSION,
960
- generatedAt: new Date().toISOString(),
961
- stashRoot,
962
- files: mergedNodes,
963
- entities: deduped.entities,
964
- relations: deduped.relations,
965
- quality,
966
- ...(previousGraph.telemetry ? { telemetry: previousGraph.telemetry } : {}),
967
- };
968
- return writeGraphFile(stashRoot, graph, db) ? { written: true, bodyHash: effectiveHash } : { written: false };
969
- }
970
- catch (err) {
971
- if (err instanceof ConfigError)
972
- throw err;
973
- rethrowIfTestIsolationError(err);
974
- // A genuine extraction/write failure, distinct from the deliberate
975
- // "nothing to do" skips above (missing file, empty body, no model). Warn
976
- // so it is visible instead of looking identical to a no-op skip; the
977
- // entry stays queued and is retried on the next pass.
978
- warn(`graph extraction: failed to extract graph for ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
979
- return { written: false };
980
- }
981
- finally {
982
- if (ownedLease)
983
- disposeLoweredExecutionDispatchLease(ownedLease);
984
- }
985
- }
986
- export async function extractGraphForSingleFile(db, stashRoot, filePath, _bodyHash, opts) {
987
- return (await extractGraphForSingleFileRevision(db, stashRoot, filePath, opts)).written;
988
- }
989
- // ── Eligible-file detection ─────────────────────────────────────────────────
990
- /**
991
- * Rank eligible graph-extraction candidates by their entry `utility_scores`,
992
- * highest first, for the incremental high-signal-first sweep (P2 of #624).
993
- *
994
- * The join is READ-ONLY (`entries.file_path = candidate.absPath`, then
995
- * `entries.id -> utility_scores.entry_id`) and does NOT re-couple the graph
996
- * rows to `entries`. It reads the GLOBAL `utility_scores` table (not the
997
- * per-scope `utility_scores_scoped`), so ranking is corpus-wide; `stashRoot`
998
- * is accepted for call-site symmetry/future scoping but is not used to filter
999
- * (the global table has no `stash_root` column).
1000
- *
1001
- * Candidates with no matching `entries` row, or an entry with no
1002
- * `utility_scores` row, get an effective utility of 0 (LEFT JOIN + COALESCE)
1003
- * and sort LAST — they are deprioritized, never dropped, so a `topN >= total`
1004
- * slice still includes them and they remain reachable on later runs.
1005
- *
1006
- * Ties (equal utility) break by `file_path` ASC for deterministic output.
1007
- * Returns a NEW array; the input is not mutated. SQLite's ~999 bound-parameter
1008
- * cap is respected by chunking the `IN (...)` lookup at 500.
1009
- *
1010
- * Exported for direct unit testing.
1011
- */
1012
- export function rankCandidatesByUtility(db, candidates, _stashRoot) {
1013
- // Cannot rank without a DB → return the input unranked rather than throw.
1014
- // Keeps the DB-less code path (reuse-from-memory) working when topN is set.
1015
- if (!db || candidates.length === 0)
1016
- return candidates;
1017
- const utilityByPath = new Map();
1018
- const CHUNK = 500;
1019
- for (let start = 0; start < candidates.length; start += CHUNK) {
1020
- const chunk = candidates.slice(start, start + CHUNK);
1021
- const paths = chunk.map((c) => c.absPath);
1022
- const placeholders = paths.map(() => "?").join(", ");
1023
- const rows = db
1024
- .prepare(`SELECT e.file_path AS file_path, COALESCE(MAX(u.utility), 0) AS utility
1025
- FROM entries e
1026
- LEFT JOIN utility_scores u ON u.entry_id = e.id
1027
- WHERE e.file_path IN (${placeholders})
1028
- GROUP BY e.file_path`)
1029
- .all(...paths);
1030
- for (const row of rows) {
1031
- utilityByPath.set(row.file_path, row.utility ?? 0);
1032
- }
1033
- }
1034
- return [...candidates].sort((a, b) => {
1035
- const ua = utilityByPath.get(a.absPath) ?? 0;
1036
- const ub = utilityByPath.get(b.absPath) ?? 0;
1037
- if (ub !== ua)
1038
- return ub - ua; // utility DESC
1039
- return a.absPath < b.absPath ? -1 : a.absPath > b.absPath ? 1 : 0; // tie-break: path ASC
1040
- });
1041
- }
1042
- /**
1043
- * Scan the primary stash for `memory:` and `knowledge:` markdown files
1044
- * suitable for graph extraction. The directory layout convention is the
1045
- * same one the rest of the indexer uses: `<stashRoot>/<type>/...`.
1046
- *
1047
- * Inferred-child memories (frontmatter `inferred: true`) are skipped — they
1048
- * are already derived summaries, with no additional internal graph structure worth
1049
- * extracting.
1050
- *
1051
- * Exported for direct unit testing.
1052
- */
1053
- export function collectEligibleFiles(stashRoot, includeTypes = [...DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES]) {
1054
- const out = [];
1055
- for (const rawType of includeTypes) {
1056
- const type = rawType.trim().toLowerCase();
1057
- if (!SUPPORTED_GRAPH_EXTRACTION_INCLUDE_TYPES.has(type))
1058
- continue;
1059
- const stashDir = stashDirFor(type);
1060
- if (!stashDir)
1061
- continue;
1062
- const dir = path.join(stashRoot, stashDir);
1063
- if (!fs.existsSync(dir))
1064
- continue;
1065
- const walked = walkMarkdownFiles(dir);
1066
- if (!walked.complete) {
1067
- warn(`graph extraction: directory scan under ${dir} is incomplete — some files may be missing`);
1068
- }
1069
- for (const filePath of walked.files) {
1070
- let raw;
1071
- try {
1072
- raw = fs.readFileSync(filePath, "utf8");
1073
- }
1074
- catch (err) {
1075
- warn(`graph extraction: failed to read candidate file ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
1076
- continue;
1077
- }
1078
- const parsed = parseFrontmatter(raw);
1079
- // Skip inferred memory children — they are atomic and there's no
1080
- // graph to extract from a single-fact body.
1081
- if (type === "memory" && parsed.data.inferred === true)
1082
- continue;
1083
- const body = parsed.content.trim();
1084
- if (!body)
1085
- continue;
1086
- out.push({ absPath: filePath, type, body });
1087
- }
1088
- }
1089
- return out;
1090
- }
1091
- // ── Persistence ─────────────────────────────────────────────────────────────
1092
- /**
1093
- * Persist graph rows into the SQLite index DB.
1094
- */
1095
- function writeGraphFile(stashRoot, graph, db) {
1096
- if (!db) {
1097
- warn("graph extraction: no database handle available; skipping graph persistence.");
1098
- return false;
1099
- }
1100
- try {
1101
- replaceStoredGraph(db, graph);
1102
- return true;
1103
- }
1104
- catch (err) {
1105
- warn(`graph extraction: failed to persist graph for ${stashRoot}: ${err instanceof Error ? err.message : String(err)}`);
1106
- return false;
1107
- }
1108
- }