akm-cli 0.9.1 → 0.9.2-alpha.1

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 (341) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +3 -1
  3. package/SECURITY.md +1 -1
  4. package/STABILITY.md +1 -1
  5. package/dist/akm +2 -2
  6. package/dist/akm-migrate +2 -2
  7. package/dist/assets/hints/cli-hints-full.md +14 -9
  8. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
  9. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  10. package/dist/assets/models.json +35 -0
  11. package/dist/assets/tasks/core/extract.yml +6 -5
  12. package/dist/assets/tasks/core/improve.yml +6 -5
  13. package/dist/assets/tasks/core/index-refresh.yml +6 -5
  14. package/dist/assets/tasks/core/sync.yml +6 -5
  15. package/dist/assets/tasks/core/version-check.yml +6 -5
  16. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
  17. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
  18. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
  19. package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
  20. package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
  21. package/dist/cli/confirm.js +2 -2
  22. package/dist/cli/parse-args.js +3 -24
  23. package/dist/cli/retired-commands.js +1 -1
  24. package/dist/cli/shared.js +2 -2
  25. package/dist/cli.js +11 -9
  26. package/dist/commands/agent/agent-dispatch.js +55 -89
  27. package/dist/commands/agent/contribute-cli.js +12 -45
  28. package/dist/commands/command/builtin-action.js +32 -0
  29. package/dist/commands/command/command-cli.js +99 -0
  30. package/dist/commands/command/command-execution.js +308 -0
  31. package/dist/commands/command/execution-source-loader.js +176 -0
  32. package/dist/commands/command/portable-template.js +60 -0
  33. package/dist/commands/config-cli.js +10 -4
  34. package/dist/commands/env/env.js +4 -2
  35. package/dist/commands/feedback-cli.js +1 -1
  36. package/dist/commands/health/checks.js +241 -29
  37. package/dist/commands/health/html-report.js +0 -14
  38. package/dist/commands/health/report-view-model.js +0 -1
  39. package/dist/commands/health/surfaces.js +6 -7
  40. package/dist/commands/health/types.js +0 -2
  41. package/dist/commands/health.js +63 -18
  42. package/dist/commands/improve/collapse-detector.js +2 -2
  43. package/dist/commands/improve/consolidate.js +251 -214
  44. package/dist/commands/improve/distill/promote-memory.js +71 -34
  45. package/dist/commands/improve/distill/quality-gate.js +17 -5
  46. package/dist/commands/improve/distill.js +232 -155
  47. package/dist/commands/improve/eligibility.js +112 -79
  48. package/dist/commands/improve/execution.js +57 -0
  49. package/dist/commands/improve/extract-cli.js +5 -5
  50. package/dist/commands/improve/extract.js +521 -342
  51. package/dist/commands/improve/improve-strategies.js +43 -14
  52. package/dist/commands/improve/improve.js +249 -29
  53. package/dist/commands/improve/loop-stages.js +11 -17
  54. package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
  55. package/dist/commands/improve/outcome-loop.js +22 -38
  56. package/dist/commands/improve/planner.js +134 -0
  57. package/dist/commands/improve/preparation.js +730 -409
  58. package/dist/commands/improve/reflect.js +386 -223
  59. package/dist/commands/improve/run-context.js +3 -4
  60. package/dist/commands/improve/salience.js +6 -58
  61. package/dist/commands/improve/session-asset.js +12 -12
  62. package/dist/commands/lint/index.js +101 -29
  63. package/dist/commands/migrate-cli.js +11 -69
  64. package/dist/commands/migration-tool.js +6 -9
  65. package/dist/commands/models-cli.js +27 -0
  66. package/dist/commands/proposal/drain.js +258 -186
  67. package/dist/commands/proposal/proposal-cli.js +32 -10
  68. package/dist/commands/proposal/proposal.js +2 -5
  69. package/dist/commands/proposal/propose.js +192 -172
  70. package/dist/commands/proposal/repository.js +54 -91
  71. package/dist/commands/proposal/validators/proposal-validators.js +9 -7
  72. package/dist/commands/read/curate.js +10 -0
  73. package/dist/commands/read/registry-search.js +25 -9
  74. package/dist/commands/read/remember-cli.js +14 -2
  75. package/dist/commands/read/search.js +10 -4
  76. package/dist/commands/read/show.js +139 -153
  77. package/dist/commands/registry-cli.js +16 -7
  78. package/dist/commands/remember.js +33 -18
  79. package/dist/commands/sources/add-cli.js +19 -178
  80. package/dist/commands/sources/bundle-cli.js +15 -3
  81. package/dist/commands/sources/dangerous-env-audit.js +135 -0
  82. package/dist/commands/sources/info.js +2 -1
  83. package/dist/commands/sources/installed-stashes.js +835 -152
  84. package/dist/commands/sources/schema-repair.js +174 -95
  85. package/dist/commands/sources/self-update.js +30 -74
  86. package/dist/commands/sources/source-add.js +3 -5
  87. package/dist/commands/sources/sources-cli.js +2 -15
  88. package/dist/commands/sources/update-transaction.js +220 -0
  89. package/dist/commands/tasks/tasks-cli.js +3 -3
  90. package/dist/commands/tasks/tasks.js +736 -317
  91. package/dist/commands/workflow-cli.js +2 -2
  92. package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
  93. package/dist/core/adapter/adapters/akm-adapter.js +83 -34
  94. package/dist/core/adapter/adapters/akm-lint.js +54 -39
  95. package/dist/core/adapter/adapters/akm-metadata.js +45 -45
  96. package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
  97. package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
  98. package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
  99. package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
  100. package/dist/core/adapter/adapters/index.js +0 -9
  101. package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
  102. package/dist/core/adapter/adapters/okf-adapter.js +4 -0
  103. package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
  104. package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
  105. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
  106. package/dist/core/adapter/execution-source.js +308 -0
  107. package/dist/core/adapter/recognize-match.js +36 -13
  108. package/dist/core/adapter/registry.js +0 -9
  109. package/dist/core/asset/stash-meta.js +94 -4
  110. package/dist/core/common.js +6 -11
  111. package/dist/core/config/config-io.js +3 -3
  112. package/dist/core/config/config-schema.js +18 -40
  113. package/dist/core/config/config-sources.js +11 -21
  114. package/dist/core/config/config-walker.js +31 -13
  115. package/dist/core/config/config.js +21 -20
  116. package/dist/core/config/schema/engines.js +8 -7
  117. package/dist/core/config/schema/improve-processes.js +29 -5
  118. package/dist/core/config/schema/primitives.js +1 -23
  119. package/dist/core/config/schema/sources-bundles.js +13 -16
  120. package/dist/core/errors.js +2 -0
  121. package/dist/core/events.js +68 -32
  122. package/dist/core/extra-params.js +1 -0
  123. package/dist/core/improve-result.js +315 -0
  124. package/dist/core/lesson-lint.js +0 -6
  125. package/dist/core/maintenance-barrier.js +4 -4
  126. package/dist/core/network-policy.js +152 -0
  127. package/dist/core/paths.js +1 -1
  128. package/dist/core/recognition-util.js +4 -4
  129. package/dist/core/registry-url.js +456 -0
  130. package/dist/core/state/migrations.js +161 -47
  131. package/dist/core/state-db.js +453 -80
  132. package/dist/core/system-error.js +32 -0
  133. package/dist/core/time.js +2 -12
  134. package/dist/core/write-source.js +0 -18
  135. package/dist/execution/directory-identity.js +52 -0
  136. package/dist/execution/executable-identity.js +107 -0
  137. package/dist/execution/guarded-source.js +398 -0
  138. package/dist/execution/json.js +95 -0
  139. package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
  140. package/dist/execution/record.js +55 -0
  141. package/dist/execution/resolved-request.js +730 -0
  142. package/dist/execution/source.js +320 -0
  143. package/dist/indexer/bundle-identity-guard.js +5 -4
  144. package/dist/indexer/db/graph-db.js +33 -0
  145. package/dist/indexer/graph/graph-boost.js +3 -4
  146. package/dist/indexer/graph/graph-extraction.js +562 -373
  147. package/dist/indexer/index-written-assets.js +65 -34
  148. package/dist/indexer/indexer.js +443 -222
  149. package/dist/indexer/installations.js +6 -0
  150. package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
  151. package/dist/indexer/passes/memory-inference.js +227 -174
  152. package/dist/indexer/scan/doc-to-entry.js +7 -9
  153. package/dist/indexer/scan/drain-dir.js +51 -23
  154. package/dist/indexer/search/db-search.js +98 -22
  155. package/dist/indexer/search/search-fields.js +1 -1
  156. package/dist/indexer/search/search-hit-enrichers.js +1 -1
  157. package/dist/indexer/search/search-source.js +7 -16
  158. package/dist/indexer/search/semantic-status.js +10 -1
  159. package/dist/indexer/usage/show-usage.js +105 -0
  160. package/dist/indexer/usage/usage-events.js +7 -2
  161. package/dist/indexer/walk/matchers.js +40 -10
  162. package/dist/indexer/walk/path-resolver.js +5 -2
  163. package/dist/indexer/walk/walker.js +20 -2
  164. package/dist/integrations/agent/builder-shared.js +3 -6
  165. package/dist/integrations/agent/conversation-fallback.js +16 -0
  166. package/dist/integrations/agent/engine-resolution.js +87 -87
  167. package/dist/integrations/agent/execution-cascade.js +566 -0
  168. package/dist/integrations/agent/execution-definitions.js +211 -0
  169. package/dist/integrations/agent/execution-lowering.js +811 -0
  170. package/dist/integrations/agent/execution-preparation.js +67 -0
  171. package/dist/integrations/agent/index.js +0 -2
  172. package/dist/integrations/agent/inline-execution.js +74 -0
  173. package/dist/integrations/agent/model-map.js +515 -0
  174. package/dist/integrations/agent/persona-fallback.js +30 -0
  175. package/dist/integrations/agent/request-lowering.js +186 -0
  176. package/dist/integrations/agent/runner-dispatch.js +230 -37
  177. package/dist/integrations/agent/runner.js +12 -83
  178. package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
  179. package/dist/integrations/harnesses/aider/index.js +0 -1
  180. package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
  181. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  182. package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
  183. package/dist/integrations/harnesses/claude/index.js +1 -5
  184. package/dist/integrations/harnesses/claude/session-log.js +3 -33
  185. package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
  186. package/dist/integrations/harnesses/codex/index.js +0 -1
  187. package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
  188. package/dist/integrations/harnesses/copilot/index.js +0 -1
  189. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
  190. package/dist/integrations/harnesses/gemini/index.js +0 -1
  191. package/dist/integrations/harnesses/index.js +4 -44
  192. package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
  193. package/dist/integrations/harnesses/opencode/index.js +0 -2
  194. package/dist/integrations/harnesses/opencode/session-log.js +14 -204
  195. package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
  196. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
  197. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
  198. package/dist/integrations/harnesses/openhands/index.js +0 -1
  199. package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
  200. package/dist/integrations/harnesses/pi/index.js +0 -1
  201. package/dist/integrations/harnesses/shared.js +0 -1
  202. package/dist/integrations/harnesses/types.js +1 -3
  203. package/dist/integrations/lockfile.js +82 -79
  204. package/dist/integrations/session-logs/index.js +6 -17
  205. package/dist/integrations/session-logs/provider-base.js +1 -29
  206. package/dist/llm/client.js +10 -5
  207. package/dist/llm/embedder.js +6 -7
  208. package/dist/llm/embedders/local.js +37 -88
  209. package/dist/llm/embedders/types.js +1 -1
  210. package/dist/llm/graph-extract.js +75 -50
  211. package/dist/llm/index-passes.js +43 -5
  212. package/dist/llm/memory-infer.js +8 -6
  213. package/dist/llm/metadata-enhance.js +5 -3
  214. package/dist/llm/structured-call.js +122 -25
  215. package/dist/output/format-exempt.js +1 -1
  216. package/dist/output/render-registry.js +0 -16
  217. package/dist/output/renderers.js +12 -7
  218. package/dist/output/shapes/curate.js +1 -0
  219. package/dist/output/shapes/helpers.js +10 -2
  220. package/dist/output/shapes/passthrough.js +2 -0
  221. package/dist/output/text/command-format.js +28 -32
  222. package/dist/output/text/health-format.js +1 -29
  223. package/dist/output/text/migrate.js +6 -56
  224. package/dist/output/text/proposal-format.js +16 -1
  225. package/dist/output/text/workflow-format.js +16 -0
  226. package/dist/registry/network.js +279 -0
  227. package/dist/registry/pinned-request-helper.js +247 -0
  228. package/dist/registry/pinned-transport.js +717 -0
  229. package/dist/registry/providers/skills-sh.js +18 -6
  230. package/dist/registry/providers/static-index.js +20 -7
  231. package/dist/registry/resolve.js +53 -28
  232. package/dist/scripts/akm-migrate-node.js +19122 -52218
  233. package/dist/scripts/akm-migrate.js +19064 -51567
  234. package/dist/setup/registry-stash-loader.js +64 -20
  235. package/dist/setup/semantic-assets.js +9 -34
  236. package/dist/setup/setup.js +12 -30
  237. package/dist/setup/source-identity.js +17 -0
  238. package/dist/setup/steps/sources.js +36 -15
  239. package/dist/setup/steps/tasks.js +39 -11
  240. package/dist/sources/providers/git-provider.js +3 -3
  241. package/dist/sources/providers/npm.js +2 -2
  242. package/dist/sources/providers/provider-utils.js +4 -3
  243. package/dist/sources/providers/website.js +11 -7
  244. package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
  245. package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
  246. package/dist/sources/website-url.js +73 -0
  247. package/dist/storage/engines/sqlite-migrations.js +81 -26
  248. package/dist/storage/managed-db.js +27 -24
  249. package/dist/storage/repositories/events-repository.js +3 -0
  250. package/dist/storage/repositories/index-connection.js +42 -10
  251. package/dist/storage/repositories/index-entries-repository.js +167 -207
  252. package/dist/storage/repositories/index-entry-mapper.js +8 -12
  253. package/dist/storage/repositories/index-entry-schema.js +255 -0
  254. package/dist/storage/repositories/index-fts-repository.js +10 -10
  255. package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
  256. package/dist/storage/repositories/index-meta-repository.js +0 -11
  257. package/dist/storage/repositories/index-schema.js +71 -342
  258. package/dist/storage/repositories/index-utility-repository.js +12 -17
  259. package/dist/storage/repositories/index-vec-repository.js +1 -1
  260. package/dist/storage/repositories/proposals-repository.js +4 -127
  261. package/dist/storage/repositories/registry-cache.js +2 -1
  262. package/dist/storage/repositories/task-history-repository.js +20 -40
  263. package/dist/storage/repositories/workflow-runs-repository.js +228 -129
  264. package/dist/storage/sqlite-read-snapshot.js +148 -0
  265. package/dist/tasks/backends/cron.js +170 -42
  266. package/dist/tasks/backends/index.js +1 -1
  267. package/dist/tasks/backends/launchd.js +787 -202
  268. package/dist/tasks/backends/schtasks.js +282 -83
  269. package/dist/tasks/embedded.js +7 -7
  270. package/dist/tasks/frozen-script.js +50 -0
  271. package/dist/tasks/resolve-akm-bin.js +5 -1
  272. package/dist/tasks/runner.js +239 -251
  273. package/dist/tasks/runtime-v3.js +281 -0
  274. package/dist/tasks/scheduler-binding.js +272 -0
  275. package/dist/tasks/scheduler-invocation.js +57 -43
  276. package/dist/tasks/scheduler-sync.js +654 -0
  277. package/dist/tasks/source-v3.js +752 -0
  278. package/dist/tasks/standalone-script-entry.js +5 -0
  279. package/dist/tasks/task-id.js +29 -0
  280. package/dist/workflows/authoring/authoring.js +15 -32
  281. package/dist/workflows/exec/dispatch-redaction.js +14 -8
  282. package/dist/workflows/exec/exec-unit.js +7 -28
  283. package/dist/workflows/exec/frozen-judge.js +57 -89
  284. package/dist/workflows/exec/lowering-notices.js +23 -0
  285. package/dist/workflows/exec/native-executor.js +301 -458
  286. package/dist/workflows/exec/param-secrets.js +4 -3
  287. package/dist/workflows/exec/run-workflow.js +26 -32
  288. package/dist/workflows/exec/step-work.js +105 -109
  289. package/dist/workflows/exec/unit-dispatch.js +103 -27
  290. package/dist/workflows/exec/unit-writer.js +3 -3
  291. package/dist/workflows/exec/worktree.js +2 -2
  292. package/dist/workflows/ir/compile.js +86 -72
  293. package/dist/workflows/ir/environment-v4.js +328 -0
  294. package/dist/workflows/ir/freeze-v4.js +122 -0
  295. package/dist/workflows/ir/plan-hash.js +13 -7
  296. package/dist/workflows/ir/schema-v4.js +525 -0
  297. package/dist/workflows/ir/schema.js +25 -284
  298. package/dist/workflows/ir/source-freeze-v4.js +506 -0
  299. package/dist/workflows/parser.js +27 -24
  300. package/dist/workflows/program/schema.js +1 -2
  301. package/dist/workflows/renderer.js +42 -29
  302. package/dist/workflows/resource-limits.js +4 -5
  303. package/dist/workflows/runtime/agent-identity.js +11 -13
  304. package/dist/workflows/runtime/plan-classifier.js +8 -8
  305. package/dist/workflows/runtime/runs.js +27 -43
  306. package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
  307. package/dist/workflows/source-files.js +373 -0
  308. package/dist/workflows/source-ir/compile.js +196 -0
  309. package/dist/workflows/source-ir/github-yaml.js +577 -0
  310. package/dist/workflows/source-ir/ordering.js +38 -0
  311. package/dist/workflows/source-ir/program.js +50 -0
  312. package/dist/workflows/source-ir/result.js +26 -0
  313. package/dist/workflows/source-ir/schema.js +772 -0
  314. package/dist/workflows/source-ir/semantics.js +242 -0
  315. package/dist/workflows/source-ir/uses.js +14 -0
  316. package/docs/README.md +2 -0
  317. package/docs/migration/README.md +3 -1
  318. package/docs/migration/release-notes/0.9.2.md +55 -0
  319. package/docs/migration/release-notes/README.md +5 -0
  320. package/docs/migration/v0.8-to-v0.9.md +76 -1077
  321. package/docs/migration/v0.9.0-troubleshooting.md +104 -516
  322. package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
  323. package/docs/reference/README.md +1 -0
  324. package/docs/reference/cli.md +230 -98
  325. package/docs/reference/configuration.md +153 -27
  326. package/docs/reference/data-and-telemetry.md +19 -1
  327. package/docs/reference/supported-formats.md +23 -3
  328. package/docs/reference/tasks.md +182 -0
  329. package/docs/reference/workflow-schema.md +91 -40
  330. package/docs/reference/workflows.md +33 -6
  331. package/package.json +10 -6
  332. package/schemas/akm-config.json +372 -216
  333. package/schemas/akm-task.json +324 -80
  334. package/schemas/akm-workflow.json +6 -9
  335. package/dist/core/migration-operation.js +0 -75
  336. package/dist/integrations/agent/model-aliases.js +0 -74
  337. package/dist/tasks/parser.js +0 -380
  338. package/dist/tasks/schema.js +0 -123
  339. package/dist/tasks/validator.js +0 -80
  340. package/dist/workflows/ir/freeze.js +0 -320
  341. package/dist/workflows/runtime/document-cache.js +0 -13
@@ -29,7 +29,8 @@
29
29
  * NEVER deleted — the user keeps what was already produced.
30
30
  *
31
31
  * Locked v1 contract:
32
- * - LLM access is exclusively via `resolveIndexPassLLM("memory", config)`.
32
+ * - LLM access is exclusively via the frozen runner returned by
33
+ * `resolveIndexPassExecution("memory", config)`.
33
34
  * - All child memory writes go through `writeAssetToSource` in
34
35
  * `src/core/write-source.ts`. The parent's frontmatter rewrite is an
35
36
  * explicit narrow exception — see {@link markParentProcessed}.
@@ -40,14 +41,19 @@ import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
40
41
  import { assembleAsset } from "../../core/asset/asset-serialize.js";
41
42
  import { parseFrontmatter, parseFrontmatterBlock } from "../../core/asset/frontmatter.js";
42
43
  import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
44
+ import { bestEffort } from "../../core/best-effort.js";
43
45
  import { todayIso } from "../../core/common.js";
44
46
  import { concurrentMap } from "../../core/concurrent.js";
47
+ import { ConfigError } from "../../core/errors.js";
45
48
  import { warn } from "../../core/warn.js";
46
49
  import { recordWrittenPath } from "../../core/write-provenance.js";
47
50
  import { writeAssetToSource } from "../../core/write-source.js";
51
+ import { disposeLoweredExecutionDispatchLease, } from "../../integrations/agent/execution-lowering.js";
48
52
  import { isProcessEnabled } from "../../llm/feature-gate.js";
49
- import { resolveIndexPassLLM } from "../../llm/index-passes.js";
53
+ import { resolveIndexPassExecution } from "../../llm/index-passes.js";
50
54
  import * as memoryInfer from "../../llm/memory-infer.js";
55
+ import { preflightStructuredLlmRunner } from "../../llm/structured-call.js";
56
+ import { computeBodyHash, getLlmCacheEntry } from "../../storage/repositories/index-llm-cache-repository.js";
51
57
  import { withLlmCache } from "../db/llm-cache.js";
52
58
  import { walkMarkdownFiles } from "../walk/walker.js";
53
59
  /**
@@ -58,6 +64,114 @@ const FM_INFERRED = "inferred";
58
64
  const FM_INFERENCE_PROCESSED = "inferenceProcessed";
59
65
  const FM_SOURCE = "source";
60
66
  const FM_CAPTURE_MODE = "captureMode";
67
+ function validateDerivedMemoryDraft(raw) {
68
+ if (!raw || typeof raw !== "object")
69
+ return undefined;
70
+ const parsed = raw;
71
+ const title = typeof parsed.title === "string" ? parsed.title : "";
72
+ const description = typeof parsed.description === "string" ? parsed.description : "";
73
+ const content = typeof parsed.content === "string" ? parsed.content : "";
74
+ const tags = Array.isArray(parsed.tags) ? parsed.tags.filter((tag) => typeof tag === "string") : [];
75
+ const searchHints = Array.isArray(parsed.searchHints)
76
+ ? parsed.searchHints.filter((hint) => typeof hint === "string")
77
+ : [];
78
+ return title && description && content && tags.length > 0 && searchHints.length > 0
79
+ ? { title, description, tags, searchHints, content }
80
+ : undefined;
81
+ }
82
+ function planPendingMemoryRecord(record, ctx) {
83
+ if (ctx.signal?.aborted)
84
+ return { kind: "aborted", record };
85
+ if (fs.existsSync(derivedChildPath(record)))
86
+ return { kind: "existing-child", record };
87
+ if (ctx.db && !ctx.reEnrich) {
88
+ const cached = bestEffort(() => {
89
+ const entry = getLlmCacheEntry(ctx.db, record.filePath, computeBodyHash(record.body), "memory-inference-v2");
90
+ return entry ? validateDerivedMemoryDraft(JSON.parse(entry.resultJson)) : undefined;
91
+ }, "memory inference cache read corrupt — fall through to recompute");
92
+ if (cached)
93
+ return { kind: "cache-hit", record, derived: cached };
94
+ }
95
+ return { kind: "model", record };
96
+ }
97
+ function memoryExecutionForContext(ctx, config) {
98
+ if (Object.hasOwn(ctx, "llmRunner")) {
99
+ return Object.freeze({ runner: ctx.llmRunner ?? undefined, notices: Object.freeze([]) });
100
+ }
101
+ return resolveIndexPassExecution("memory", config);
102
+ }
103
+ async function inferPendingMemoryRecord(plan, ctx) {
104
+ const { record } = plan;
105
+ const { config, featureConfig, signal, db, reEnrich, llmRunner, lease, inferTelemetry, compressMemoryToDerivedMemory, onNotices, onConfigFailure, } = ctx;
106
+ if (signal?.aborted || plan.kind === "aborted")
107
+ return { aborted: true };
108
+ // Existing children are complete, but the parent may be unmarked after a
109
+ // crash between the child write and parent update (or an external write).
110
+ if (fs.existsSync(derivedChildPath(record))) {
111
+ markParentProcessed(record);
112
+ return {
113
+ skipped: false,
114
+ splitParent: false,
115
+ written: 0,
116
+ fromCache: false,
117
+ retryAttempts: 0,
118
+ childExists: true,
119
+ precheck: true,
120
+ };
121
+ }
122
+ // The child can disappear after the read-only classification pass (for
123
+ // example, an external cleanup racing this index run). The plan is not proof
124
+ // that the child still exists at the mutation boundary: leave the parent
125
+ // pending so a later run can classify and preflight the now-real model work.
126
+ if (plan.kind === "existing-child") {
127
+ return { skipped: true, fromCache: false, retryAttempts: 0 };
128
+ }
129
+ let fromCache = plan.kind === "cache-hit";
130
+ let retryAttempts = 0;
131
+ const onRetryAttempt = () => {
132
+ retryAttempts += 1;
133
+ };
134
+ const infer = () => compressMemoryToDerivedMemory(llmRunner, record.body, signal, featureConfig, (event) => warn(`[akm] LLM fallback for ${event.feature}: ${event.reason}`), inferTelemetry, onRetryAttempt, onNotices, lease);
135
+ let derived = plan.kind === "cache-hit" ? plan.derived : undefined;
136
+ try {
137
+ if (plan.kind === "model") {
138
+ derived = db
139
+ ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, infer, validateDerivedMemoryDraft, undefined, "memory-inference-v2", {
140
+ onCacheHit: () => {
141
+ fromCache = true;
142
+ },
143
+ })
144
+ : await compressMemoryToDerivedMemory(llmRunner, record.body, signal, config, (event) => warn(`[akm] LLM fallback for ${event.feature}: ${event.reason}`), inferTelemetry, onRetryAttempt, onNotices, lease);
145
+ }
146
+ }
147
+ catch (error) {
148
+ if (error instanceof ConfigError) {
149
+ onConfigFailure(error);
150
+ return undefined;
151
+ }
152
+ throw error;
153
+ }
154
+ if (!derived)
155
+ return { skipped: true, fromCache, retryAttempts };
156
+ const writeOutcome = await writeDerivedMemory(record, derived);
157
+ if (writeOutcome.written > 0) {
158
+ markParentProcessed(record);
159
+ return { skipped: false, splitParent: true, written: writeOutcome.written, fromCache, retryAttempts };
160
+ }
161
+ // A child that appeared mid-flight is complete and can safely mark its
162
+ // parent. A genuine write failure remains unmarked for a later retry.
163
+ if (writeOutcome.childExists)
164
+ markParentProcessed(record);
165
+ return {
166
+ skipped: false,
167
+ splitParent: false,
168
+ written: 0,
169
+ fromCache,
170
+ retryAttempts,
171
+ childExists: true,
172
+ precheck: false,
173
+ };
174
+ }
61
175
  /**
62
176
  * Top-level entry point. Returns a no-op result when the pass is disabled.
63
177
  *
@@ -66,8 +180,8 @@ const FM_CAPTURE_MODE = "captureMode";
66
180
  * 1. **Feature gate** — the selected strategy's `processes.memoryInference.enabled`
67
181
  * (defaults to `true`). When `false`, no network call may issue regardless
68
182
  * of per-pass settings.
69
- * 2. **Per-pass gate** — `resolveIndexPassLLM("memory", config)` (which
70
- * reads `index.memory.llm`). When `false`, the indexer simply skips
183
+ * 2. **Per-pass gate** — `resolveIndexPassExecution("memory", config)` reads
184
+ * the index pass selection. Without a runner, the indexer simply skips
71
185
  * this pass for the current run.
72
186
  *
73
187
  * Both must allow the call for the pass to run. Either set to `false`
@@ -75,7 +189,7 @@ const FM_CAPTURE_MODE = "captureMode";
75
189
  */
76
190
  export async function runMemoryInferencePass(ctx) {
77
191
  const { config, sources, signal, db, reEnrich, onProgress, options = {} } = ctx;
78
- const invocationOwnsConnection = Object.hasOwn(ctx, "llmConfig");
192
+ const invocationOwnsRunner = Object.hasOwn(ctx, "llmRunner");
79
193
  const compressMemoryToDerivedMemory = options.compressMemoryToDerivedMemory ?? memoryInfer.compressMemoryToDerivedMemory;
80
194
  const result = {
81
195
  considered: 0,
@@ -93,17 +207,29 @@ export async function runMemoryInferencePass(ctx) {
93
207
  // HTML-error categorization (which is otherwise swallowed inside the feature
94
208
  // gate) bubbles up into the pass result.
95
209
  const inferTelemetry = {};
210
+ const noticesByKey = new Map();
211
+ const onNotices = (notices) => {
212
+ for (const notice of notices)
213
+ noticesByKey.set(JSON.stringify(notice), notice);
214
+ };
215
+ const completeResult = () => {
216
+ if (noticesByKey.size > 0)
217
+ result.notices = Object.freeze([...noticesByKey.values()]);
218
+ return result;
219
+ };
96
220
  // Gate 1 — feature gate via isProcessEnabled, which reads the 0.8.0 path
97
221
  // (selected strategy's processes.memoryInference.enabled). Defaults to
98
222
  // enabled when the key is absent.
99
- if (!invocationOwnsConnection && !isProcessEnabled("index", "memory_inference", config))
100
- return result;
223
+ if (!invocationOwnsRunner && !isProcessEnabled("index", "memory_inference", config))
224
+ return completeResult();
101
225
  // Gate 2 — per-pass opt-out (#208). Returns the resolved llm config or
102
226
  // `undefined` when the pass should not run.
103
- const llmConfig = Object.hasOwn(ctx, "llmConfig") ? ctx.llmConfig : resolveIndexPassLLM("memory", config);
104
- if (!llmConfig)
105
- return result;
106
- const featureConfig = invocationOwnsConnection
227
+ const execution = memoryExecutionForContext(ctx, config);
228
+ onNotices(execution.notices);
229
+ const llmRunner = execution.runner;
230
+ if (!llmRunner)
231
+ return completeResult();
232
+ const featureConfig = invocationOwnsRunner
107
233
  ? { ...config, index: { ...config.index, memory: { ...config.index?.memory, enabled: true } } }
108
234
  : config;
109
235
  // The pass only writes to the primary (working) stash. Read-only caches
@@ -111,128 +237,94 @@ export async function runMemoryInferencePass(ctx) {
111
237
  // children there would be clobbered by the next sync().
112
238
  const primary = sources[0];
113
239
  if (!primary || (primary.adapterId ?? detectAdapterId(primary.path)) !== "akm")
114
- return result;
240
+ return completeResult();
115
241
  const pending = collectPendingMemories(primary.path).filter((record) => !options.candidateRefs || options.candidateRefs.has(record.ref));
116
242
  result.considered = pending.length;
117
243
  if (pending.length === 0)
118
- return result;
119
- let processed = 0;
120
- const total = pending.length;
121
- onProgress?.({ processed, total, writtenFacts: 0, skippedNoFacts: 0 });
122
- const perRecordResults = await concurrentMap(pending, async (record) => {
123
- // Aborted BEFORE a fresh LLM call. Returned as a typed outcome so the
124
- // for-loop below increments `skippedAborted` instead of silently
125
- // dropping the record (which historically inflated freshAttempts and
126
- // dragged the health-reported yield rate down — see investigation
127
- // 2026-05-26).
128
- if (signal?.aborted)
129
- return { aborted: true };
130
- // Pre-check (#588): when `<parent>.derived.md` is already on disk the
131
- // inference is by definition complete — the parent only looks pending
132
- // because `markParentProcessed` never ran (process killed between the
133
- // child write and the mark) or the child was created externally (e.g.
134
- // consolidation). Skip the LLM/cache call entirely and mark the parent
135
- // so it never re-pends. Before this check, production measurements
136
- // showed ~55% of the pass's LLM budget re-deriving such parents only to
137
- // discover the existing child after the fact.
138
- if (fs.existsSync(derivedChildPath(record))) {
139
- markParentProcessed(record);
140
- return {
141
- skipped: false,
142
- splitParent: false,
143
- written: 0,
144
- fromCache: false,
145
- retryAttempts: 0,
146
- childExists: true,
147
- precheck: true,
148
- };
149
- }
150
- // Incremental cache: skip LLM call when body hash is unchanged and
151
- // --re-enrich was not requested. The cache ref is the absolute file path.
152
- const validate = (raw) => {
153
- if (!raw || typeof raw !== "object")
154
- return undefined;
155
- const parsed = raw;
156
- const title = typeof parsed.title === "string" ? parsed.title : "";
157
- const description = typeof parsed.description === "string" ? parsed.description : "";
158
- const content = typeof parsed.content === "string" ? parsed.content : "";
159
- const tags = Array.isArray(parsed.tags) ? parsed.tags.filter((t) => typeof t === "string") : [];
160
- const searchHints = Array.isArray(parsed.searchHints)
161
- ? parsed.searchHints.filter((h) => typeof h === "string")
162
- : [];
163
- if (title && description && content && tags.length > 0 && searchHints.length > 0) {
164
- return { title, description, tags, searchHints, content };
244
+ return completeResult();
245
+ // Classify the whole batch without mutation. Required credentials are
246
+ // materialized only when at least one record will actually dispatch; this
247
+ // keeps aborted, existing-child, and validated-cache-only batches available
248
+ // offline while preserving all-or-nothing preflight for mixed batches.
249
+ const plans = pending.map((record) => planPendingMemoryRecord(record, { signal, db, reEnrich }));
250
+ const dispatchLease = plans.some((plan) => plan.kind === "model")
251
+ ? await preflightStructuredLlmRunner(llmRunner)
252
+ : undefined;
253
+ try {
254
+ let processed = 0;
255
+ const total = pending.length;
256
+ onProgress?.({ processed, total, writtenFacts: 0, skippedNoFacts: 0 });
257
+ let configFailure;
258
+ const perRecordResults = await concurrentMap(plans, (plan) => inferPendingMemoryRecord(plan, {
259
+ config,
260
+ featureConfig,
261
+ signal,
262
+ db,
263
+ reEnrich,
264
+ llmRunner,
265
+ lease: dispatchLease,
266
+ inferTelemetry,
267
+ compressMemoryToDerivedMemory,
268
+ onNotices,
269
+ onConfigFailure: (error) => {
270
+ configFailure ??= error;
271
+ },
272
+ }),
273
+ // Caller-set connection concurrency or 1: `resolveLlmEngineUse` does
274
+ // not forward `engines.<name>.concurrency`, so config cannot raise this.
275
+ llmRunner.connection.concurrency ?? 1);
276
+ if (configFailure)
277
+ throw configFailure;
278
+ for (let i = 0; i < perRecordResults.length; i++) {
279
+ const res = perRecordResults[i];
280
+ if (!res)
281
+ continue;
282
+ if ("aborted" in res && res.aborted) {
283
+ result.skippedAborted += 1;
284
+ processed++;
285
+ onProgress?.({
286
+ processed,
287
+ total,
288
+ writtenFacts: result.writtenFacts,
289
+ skippedNoFacts: result.skippedNoFacts,
290
+ currentRef: pending[i]?.ref,
291
+ });
292
+ continue;
293
+ }
294
+ if (res.fromCache) {
295
+ result.cacheHits += 1;
296
+ }
297
+ if ("retryAttempts" in res) {
298
+ result.retryAttempts += res.retryAttempts;
299
+ }
300
+ if (res.skipped) {
301
+ result.skippedNoFacts += 1;
302
+ // Intentionally NOT marked processed — a transient LLM failure should
303
+ // be retried on the next index run.
304
+ }
305
+ else if (res.splitParent) {
306
+ result.splitParents += 1;
307
+ result.writtenFacts += res.written;
308
+ }
309
+ else if ("childExists" in res && res.childExists) {
310
+ // Derived child already on disk. Track separately so this category is
311
+ // observable in health output and stops bleeding into the
312
+ // freshAttempts denominator. Pre-check skips (#588) are the routine
313
+ // self-healing path — no LLM attempt was consumed and the parent has
314
+ // been marked processed — so only the rare post-LLM case (mid-flight
315
+ // race or write failure) warrants a per-ref warning.
316
+ result.skippedChildExists += 1;
317
+ if (!res.precheck) {
318
+ warn(`memory inference: derived child for ${pending[i]?.ref ?? "<unknown>"} already existed or write failed; counted as skippedChildExists`);
319
+ }
320
+ }
321
+ else {
322
+ // The per-record state machine should cover every outcome. A hit here
323
+ // means a new code path slipped past the categorisation — surface it
324
+ // loudly so health metrics stay honest and we get a signal to fix.
325
+ result.unaccounted += 1;
326
+ warn(`memory inference: unaccounted per-record outcome for ${pending[i]?.ref ?? "<unknown>"}`);
165
327
  }
166
- return undefined;
167
- };
168
- // Track whether THIS candidate's result came from the body-hash
169
- // cache vs. a fresh LLM call. The cache short-circuits when the
170
- // parent body has not changed since a prior derived write — surfacing
171
- // the hit count separately so the operational yield rate
172
- // (writtenFacts / freshAttempts) is interpretable as the cache warms.
173
- let fromCache = false;
174
- // Count single bounded retries for transient LLM failures on this
175
- // candidate. Bumped via the `onRetryAttempt` callback threaded into
176
- // `chatCompletion`; surfaced as `retryAttempts` telemetry, never as a
177
- // failure for the same call.
178
- let retryAttempts = 0;
179
- const onRetryAttempt = () => {
180
- retryAttempts += 1;
181
- };
182
- const derived = db
183
- ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () => compressMemoryToDerivedMemory(llmConfig, record.body, signal, featureConfig, (evt) => {
184
- warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
185
- }, inferTelemetry, onRetryAttempt), validate, undefined, "memory-inference-v2", {
186
- onCacheHit: () => {
187
- fromCache = true;
188
- },
189
- })
190
- : await compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
191
- warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
192
- }, inferTelemetry, onRetryAttempt);
193
- if (!derived) {
194
- return { skipped: true, fromCache, retryAttempts };
195
- }
196
- const writeOutcome = await writeDerivedMemory(record, derived);
197
- if (writeOutcome.written > 0) {
198
- markParentProcessed(record);
199
- return { skipped: false, splitParent: true, written: writeOutcome.written, fromCache, retryAttempts };
200
- }
201
- // LLM produced a valid derived draft but no file was written — either
202
- // because `<parent>.derived.md` appeared on disk after the pre-check
203
- // above (a rare mid-flight race) or `writeAssetToSource` threw.
204
- // Categorise as `childExists` so the consumed attempt is accounted for
205
- // in health metrics rather than vanishing into the freshAttempts
206
- // denominator.
207
- //
208
- // When the child exists the inference is, by definition, complete — so
209
- // mark the parent processed here too (#550), otherwise
210
- // `isPendingMemory()` re-queues the same parent every run. A genuine
211
- // write *failure* (`writeAssetToSource` threw) must NOT mark the parent
212
- // — it should be retried next run — so we key off the explicit
213
- // `childExists` outcome rather than the conflated `written === 0`.
214
- if (writeOutcome.childExists) {
215
- markParentProcessed(record);
216
- }
217
- return {
218
- skipped: false,
219
- splitParent: false,
220
- written: 0,
221
- fromCache,
222
- retryAttempts,
223
- childExists: true,
224
- precheck: false,
225
- };
226
- },
227
- // Caller-set connection concurrency or 1: `resolveLlmEngineUse` does
228
- // not forward `engines.<name>.concurrency`, so config cannot raise this.
229
- llmConfig.concurrency ?? 1);
230
- for (let i = 0; i < perRecordResults.length; i++) {
231
- const res = perRecordResults[i];
232
- if (!res)
233
- continue;
234
- if ("aborted" in res && res.aborted) {
235
- result.skippedAborted += 1;
236
328
  processed++;
237
329
  onProgress?.({
238
330
  processed,
@@ -241,53 +333,14 @@ export async function runMemoryInferencePass(ctx) {
241
333
  skippedNoFacts: result.skippedNoFacts,
242
334
  currentRef: pending[i]?.ref,
243
335
  });
244
- continue;
245
- }
246
- if (res.fromCache) {
247
- result.cacheHits += 1;
248
- }
249
- if ("retryAttempts" in res) {
250
- result.retryAttempts += res.retryAttempts;
251
336
  }
252
- if (res.skipped) {
253
- result.skippedNoFacts += 1;
254
- // Intentionally NOT marked processed — a transient LLM failure should
255
- // be retried on the next index run.
256
- }
257
- else if (res.splitParent) {
258
- result.splitParents += 1;
259
- result.writtenFacts += res.written;
260
- }
261
- else if ("childExists" in res && res.childExists) {
262
- // Derived child already on disk. Track separately so this category is
263
- // observable in health output and stops bleeding into the
264
- // freshAttempts denominator. Pre-check skips (#588) are the routine
265
- // self-healing path — no LLM attempt was consumed and the parent has
266
- // been marked processed — so only the rare post-LLM case (mid-flight
267
- // race or write failure) warrants a per-ref warning.
268
- result.skippedChildExists += 1;
269
- if (!res.precheck) {
270
- warn(`memory inference: derived child for ${pending[i]?.ref ?? "<unknown>"} already existed or write failed; counted as skippedChildExists`);
271
- }
272
- }
273
- else {
274
- // The per-record state machine should cover every outcome. A hit here
275
- // means a new code path slipped past the categorisation — surface it
276
- // loudly so health metrics stay honest and we get a signal to fix.
277
- result.unaccounted += 1;
278
- warn(`memory inference: unaccounted per-record outcome for ${pending[i]?.ref ?? "<unknown>"}`);
279
- }
280
- processed++;
281
- onProgress?.({
282
- processed,
283
- total,
284
- writtenFacts: result.writtenFacts,
285
- skippedNoFacts: result.skippedNoFacts,
286
- currentRef: pending[i]?.ref,
287
- });
337
+ result.htmlErrorCount = inferTelemetry.htmlErrorCount ?? 0;
338
+ return completeResult();
339
+ }
340
+ finally {
341
+ if (dispatchLease)
342
+ disposeLoweredExecutionDispatchLease(dispatchLease);
288
343
  }
289
- result.htmlErrorCount = inferTelemetry.htmlErrorCount ?? 0;
290
- return result;
291
344
  }
292
345
  // ── Pending detection ───────────────────────────────────────────────────────
293
346
  /**
@@ -6,13 +6,11 @@
6
6
  * `indexDocumentFromEntry` (`adapters/akm-adapter.ts`). akm 0.9.0 Chunk 5,
7
7
  * milestone F4a (M2 groundwork).
8
8
  *
9
- * The engine swap drains an `IndexDocument` stream (`drainDirDocuments` × the
10
- * dispatched adapter's `recognize`) in place of the per-dir flat-walk matcher-pass
11
- * `IndexDocument` stream, then persists it. But the durable `entries.entry_json`
12
- * column stays a faithful `IndexDocument` every reader (`rowToIndexedEntry` →
13
- * `DbIndexedEntry.entry`) consumes it as one, and the byte-for-byte goldens pin
14
- * it. So the persist path must reconstruct the exact `IndexDocument` the old
15
- * pipeline stored FROM the `IndexDocument` the new pipeline produces.
9
+ * The indexer drains a single `IndexDocument` stream (`drainDirDocuments` × the
10
+ * dispatched adapter's `recognize`) and persists its canonical projection in
11
+ * `entries.document_json`. Every reader (`rowToIndexedEntry`
12
+ * `DbIndexedEntry.entry`) consumes that same projection, and parity tests pin
13
+ * the boundary.
16
14
  *
17
15
  * That reconstruction is lossless by construction: the akm adapter's `recognize`
18
16
  * assembles a full `IndexDocument` (P1/P2 → fold → P4) and then maps it onto the
@@ -20,8 +18,8 @@
20
18
  * onto named members, every other search-surface/provenance field onto
21
19
  * `documentJson` (the `DOCUMENT_JSON_CARRIED_FIELDS` set). This function reverses
22
20
  * that mapping exactly, so `indexDocumentToStashEntry(recognize(file))` deep-
23
- * equals the entry the legacy flat-walk pass stored for the same file (the
24
- * `tests/integration/shadow-scan-parity.test.ts` gate pins the persisted index).
21
+ * equals the current persisted document for the same file (the
22
+ * `tests/integration/shadow-scan-parity.test.ts` gate pins this boundary).
25
23
  *
26
24
  * Two deliberate non-round-trip fields:
27
25
  * - `filename` — dropped by `indexDocumentFromEntry` (no IndexDocument home),
@@ -14,26 +14,23 @@
14
14
  * - **Broken-workflow drop (item 3).** The live path dropped a broken workflow
15
15
  * via the renderer contributor's throw → metadata-pass skip-with-warning; the
16
16
  * `akm` adapter's synchronous `foldRecognizedMetadata` SWALLOWS the parse
17
- * error, so a broken workflow would otherwise silently index. We re-run
18
- * `parseWorkflow` on drained workflow docs and DROP
17
+ * error, so a broken workflow would otherwise silently index. We run the
18
+ * shared source-IR compiler on drained workflow docs and DROP
19
19
  * the entry with the same `Skipped workflow …` warning
20
20
  * ({@link buildMetadataSkipWarning}), so the workflow-skip summary counts it.
21
- * - **Workflow-document side-table (workflow-md only).** The valid parsed
22
- * `WorkflowDocument` is handed to the persist layer through the same
23
- * `document-cache` side channel the live renderer contributor used, keyed by
24
- * the reconstructed entry — so `takeWorkflowDocument(entry)` in the persist
25
- * loop writes the `workflow_documents` row exactly as before.
26
21
  *
27
22
  * `doc.hash` (= sha256 of the file content) is surfaced per recognized file so
28
23
  * the persist layer can populate the `content_hash` column (item 2). It is keyed
29
24
  * by the file's absolute path rather than by the entry object.
30
25
  *
31
- * Pure of DB/global state beyond the workflow-document side channel; a new leaf
32
- * (nothing imports it back), so it joins no import cycle.
26
+ * Pure of DB/global state; a new leaf (nothing imports it back), so it joins no
27
+ * import cycle.
33
28
  */
29
+ import path from "node:path";
34
30
  import { akmAdapter } from "../../core/adapter/adapters/akm-adapter.js";
35
- import { parseWorkflow } from "../../workflows/parser.js";
36
- import { cacheWorkflowDocument } from "../../workflows/runtime/document-cache.js";
31
+ import { canonicalizeWorkflowName } from "../../core/recognition-util.js";
32
+ import { resolveUniqueWorkflowSource, WorkflowSourceRejectionError, workflowNameForSourcePath, } from "../../workflows/source-files.js";
33
+ import { compileWorkflowSource } from "../../workflows/source-ir/compile.js";
37
34
  import { buildMetadataSkipWarning } from "../passes/metadata.js";
38
35
  import { buildFileContext } from "../walk/file-context.js";
39
36
  import { indexDocumentToStashEntry } from "./doc-to-entry.js";
@@ -53,7 +50,34 @@ export function drainDirDocuments(adapter, component, fileContexts) {
53
50
  const warnings = [];
54
51
  const hashByFile = new Map();
55
52
  const conceptIdByFile = new Map();
53
+ const rejectedPaths = new Set();
54
+ const rejectedConceptIds = new Set();
55
+ const workflowLookups = new Map();
56
56
  for (const file of fileContexts) {
57
+ const workflowName = workflowNameForSourcePath(component.root, adapter.id, file.absPath);
58
+ if (workflowName !== undefined) {
59
+ const canonicalName = canonicalizeWorkflowName(workflowName);
60
+ if (!workflowLookups.has(canonicalName))
61
+ workflowLookups.set(canonicalName, workflowName);
62
+ }
63
+ }
64
+ for (const [canonicalName, workflowName] of [...workflowLookups].sort(([left], [right]) => comparePaths(left, right))) {
65
+ try {
66
+ resolveUniqueWorkflowSource(component.root, adapter.id, workflowName);
67
+ }
68
+ catch (error) {
69
+ if (!(error instanceof WorkflowSourceRejectionError))
70
+ throw error;
71
+ rejectedConceptIds.add(adapter.id === "akm" ? `workflows/${canonicalName}` : canonicalName);
72
+ for (const relativePath of error.sourcePaths) {
73
+ rejectedPaths.add(path.join(component.root, relativePath));
74
+ }
75
+ warnings.push(error.message);
76
+ }
77
+ }
78
+ for (const file of fileContexts) {
79
+ if (rejectedPaths.has(file.absPath))
80
+ continue;
57
81
  const doc = adapter.recognize(component, file);
58
82
  if (doc === null)
59
83
  continue;
@@ -62,9 +86,9 @@ export function drainDirDocuments(adapter, component, fileContexts) {
62
86
  continue;
63
87
  }
64
88
  const entry = indexDocumentToStashEntry(doc);
65
- // Workflow docs: drop-with-warning if broken; otherwise cache the parsed
66
- // markdown document for the persist-time `workflow_documents` write.
67
- const dropWarning = handleWorkflowDoc(doc, entry, file);
89
+ // Workflow docs: drop-with-warning if broken; otherwise cache a lossless
90
+ // runtime projection when the current executor can represent the source.
91
+ const dropWarning = handleWorkflowDoc(doc, file, component.root);
68
92
  if (dropWarning !== null) {
69
93
  warnings.push(dropWarning);
70
94
  continue;
@@ -74,7 +98,10 @@ export function drainDirDocuments(adapter, component, fileContexts) {
74
98
  conceptIdByFile.set(file.absPath, doc.conceptId);
75
99
  entries.push(entry);
76
100
  }
77
- return { entries, warnings, hashByFile, conceptIdByFile };
101
+ return { entries, warnings, hashByFile, conceptIdByFile, rejectedPaths, rejectedConceptIds };
102
+ }
103
+ function comparePaths(left, right) {
104
+ return left < right ? -1 : left > right ? 1 : 0;
78
105
  }
79
106
  /**
80
107
  * `(stashRoot, files) → StashFile` drop-in for the deleted flat-walk matcher
@@ -98,18 +125,19 @@ export function recognizeStashEntries(stashRoot, files) {
98
125
  : { entries: drained.entries };
99
126
  }
100
127
  /**
101
- * If `doc` is a workflow, re-parse it: return a `Skipped workflow …` drop
102
- * warning when it is broken, or cache the parsed markdown `WorkflowDocument`
103
- * (workflow-md only) and return `null` when valid. Non-workflow docs return
104
- * `null` immediately.
128
+ * If `doc` is a workflow, compile it through source IR: return a
129
+ * `Skipped workflow …` drop warning when it is broken, or return `null` when
130
+ * it compiles. Non-workflow docs return `null` immediately.
105
131
  */
106
- function handleWorkflowDoc(doc, entry, file) {
107
- if (docRenderer(doc) !== WORKFLOW_MD_RENDERER)
132
+ function handleWorkflowDoc(doc, file, workspaceRoot) {
133
+ if (doc.type !== "workflow" ||
134
+ (doc.adapterId !== "akm" && doc.adapterId !== "akm-workflow") ||
135
+ (docRenderer(doc) !== WORKFLOW_MD_RENDERER && doc.adapterId !== "akm-workflow")) {
108
136
  return null;
109
- const result = parseWorkflow(file.content(), { path: file.relPath });
137
+ }
138
+ const result = compileWorkflowSource(file.content(), { path: file.relPath, workspaceRoot });
110
139
  if (!result.ok)
111
140
  return workflowDropWarning(file, result.errors);
112
- cacheWorkflowDocument(entry, result.document);
113
141
  return null;
114
142
  }
115
143
  /** The winning renderer name the `akm` adapter carries on `documentJson.renderer`, or `undefined`. */