akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.4

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 (343) hide show
  1. package/CHANGELOG.md +756 -0
  2. package/dist/akm +94 -196
  3. package/dist/cli/shared.js +6 -2
  4. package/dist/cli.js +22 -9
  5. package/dist/commands/agent/agent-dispatch.js +1 -1
  6. package/dist/commands/command/command-execution.js +24 -62
  7. package/dist/commands/feedback-cli.js +0 -1
  8. package/dist/commands/health/accept-rate.js +2 -2
  9. package/dist/commands/health/checks.js +30 -75
  10. package/dist/commands/health/config-skew.js +38 -0
  11. package/dist/commands/health/egress.js +54 -0
  12. package/dist/commands/health/html-report.js +0 -38
  13. package/dist/commands/health/improve-metrics.js +123 -562
  14. package/dist/commands/health/plugin-staleness.js +53 -3
  15. package/dist/commands/health/renderers.js +12 -4
  16. package/dist/commands/health/report-view-model.js +11 -106
  17. package/dist/commands/health/types-improve.js +4 -19
  18. package/dist/commands/health/windows.js +64 -73
  19. package/dist/commands/health.js +122 -143
  20. package/dist/commands/improve/consolidate/chunking.js +25 -100
  21. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  22. package/dist/commands/improve/consolidate.js +538 -1075
  23. package/dist/commands/improve/content-hash.js +16 -24
  24. package/dist/commands/improve/distill/content-repair.js +18 -100
  25. package/dist/commands/improve/distill-guards.js +20 -81
  26. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  27. package/dist/commands/improve/distill.js +608 -1075
  28. package/dist/commands/improve/eligibility.js +126 -400
  29. package/dist/commands/improve/execution.js +3 -5
  30. package/dist/commands/improve/extract.js +487 -1046
  31. package/dist/commands/improve/feedback-valence.js +0 -25
  32. package/dist/commands/improve/improve-cli.js +29 -166
  33. package/dist/commands/improve/improve-result-file.js +10 -66
  34. package/dist/commands/improve/improve-strategies.js +12 -7
  35. package/dist/commands/improve/improve-usage-report.js +18 -64
  36. package/dist/commands/improve/improve.js +443 -1063
  37. package/dist/commands/improve/ledger.js +114 -0
  38. package/dist/commands/improve/locks.js +2 -8
  39. package/dist/commands/improve/loop-stages.js +459 -1172
  40. package/dist/commands/improve/memory/derived-ref.js +12 -77
  41. package/dist/commands/improve/memory/memory-belief.js +14 -118
  42. package/dist/commands/improve/memory/memory-improve.js +4 -3
  43. package/dist/commands/improve/outcome-loop.js +28 -156
  44. package/dist/commands/improve/planner.js +5 -10
  45. package/dist/commands/improve/preparation.js +851 -2339
  46. package/dist/commands/improve/proactive-maintenance.js +34 -101
  47. package/dist/commands/improve/reflect-noise.js +104 -280
  48. package/dist/commands/improve/reflect.js +621 -1367
  49. package/dist/commands/improve/salience.js +46 -232
  50. package/dist/commands/improve/session-asset.js +19 -100
  51. package/dist/commands/improve/stage.js +323 -0
  52. package/dist/commands/proposal/drain.js +251 -644
  53. package/dist/commands/proposal/proposal-cli.js +3 -18
  54. package/dist/commands/proposal/proposal-types.js +20 -41
  55. package/dist/commands/proposal/proposal.js +1 -2
  56. package/dist/commands/proposal/propose.js +134 -160
  57. package/dist/commands/proposal/repository.js +502 -1487
  58. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  59. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  60. package/dist/commands/proposal/validators/proposals.js +13 -89
  61. package/dist/commands/read/curate.js +63 -413
  62. package/dist/commands/read/search-cli.js +16 -33
  63. package/dist/commands/read/search.js +17 -23
  64. package/dist/commands/read/show.js +2 -13
  65. package/dist/commands/sources/bundle-cli.js +25 -2
  66. package/dist/commands/sources/bundle-config-ops.js +7 -0
  67. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  68. package/dist/commands/sources/info.js +2 -11
  69. package/dist/commands/sources/installed-stashes.js +197 -746
  70. package/dist/commands/sources/schema-repair.js +98 -129
  71. package/dist/commands/sources/source-add.js +62 -12
  72. package/dist/commands/sources/stash-cli.js +1 -1
  73. package/dist/commands/tasks/explain.js +10 -13
  74. package/dist/commands/tasks/tasks-cli.js +9 -8
  75. package/dist/commands/tasks/tasks.js +326 -930
  76. package/dist/commands/tasks/validate.js +42 -21
  77. package/dist/commands/workflow/plan.js +22 -29
  78. package/dist/commands/workflow-cli.js +4 -4
  79. package/dist/core/adapter/adapters/akm-adapter.js +0 -1
  80. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  81. package/dist/core/adapter/adapters/akm-metadata.js +11 -12
  82. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  83. package/dist/core/adapter/execution-source.js +17 -29
  84. package/dist/core/asset/resolve-ref.js +1 -1
  85. package/dist/core/bundle-id.js +42 -5
  86. package/dist/core/bundle-rename.js +291 -0
  87. package/dist/core/config/config-io.js +1 -2
  88. package/dist/core/config/config-schema.js +1 -33
  89. package/dist/core/config/config-walker.js +1 -1
  90. package/dist/core/config/config.js +163 -68
  91. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  92. package/dist/core/config/schema/embedding.js +20 -5
  93. package/dist/core/config/schema/engines.js +5 -0
  94. package/dist/core/config/schema/execution.js +1 -1
  95. package/dist/core/config/schema/experimental.js +1 -1
  96. package/dist/core/config/schema/improve-processes.js +21 -95
  97. package/dist/core/config/schema/improve.js +4 -42
  98. package/dist/core/config/schema/scheduler.js +12 -12
  99. package/dist/core/config/schema/search.js +6 -22
  100. package/dist/core/env-secret-ref.js +0 -1
  101. package/dist/core/errors.js +8 -9
  102. package/dist/core/file-lock.js +76 -173
  103. package/dist/core/logs-db.js +2 -2
  104. package/dist/core/paths.js +0 -27
  105. package/dist/core/redaction.js +109 -2
  106. package/dist/core/run-lock.js +2 -5
  107. package/dist/core/spawn-env.js +1 -1
  108. package/dist/core/state/migrations.js +108 -61
  109. package/dist/core/state-db-scope.js +2 -4
  110. package/dist/core/state-db.js +126 -692
  111. package/dist/core/type-presentation.js +1 -9
  112. package/dist/core/write-source.js +293 -1012
  113. package/dist/execution/input-contract.js +1 -1
  114. package/dist/execution/resolved-request.js +135 -689
  115. package/dist/execution/source.js +63 -257
  116. package/dist/execution/target-ref.js +1 -1
  117. package/dist/indexer/bundle-identity-guard.js +2 -2
  118. package/dist/indexer/db/graph-db.js +106 -46
  119. package/dist/indexer/ensure-index.js +44 -85
  120. package/dist/indexer/graph/graph-extraction.js +340 -562
  121. package/dist/indexer/graph/graph-related.js +130 -0
  122. package/dist/indexer/index-rebuild-lock.js +3 -11
  123. package/dist/indexer/index-writer-lock.js +8 -17
  124. package/dist/indexer/index-written-assets.js +139 -151
  125. package/dist/indexer/indexer.js +524 -846
  126. package/dist/indexer/materialize-embeddings.js +60 -397
  127. package/dist/indexer/passes/memory-inference.js +81 -90
  128. package/dist/indexer/passes/metadata.js +132 -200
  129. package/dist/indexer/read-preflight.js +0 -7
  130. package/dist/indexer/scan/doc-to-entry.js +1 -3
  131. package/dist/indexer/scan/drain-dir.js +1 -1
  132. package/dist/indexer/search/db-search.js +181 -590
  133. package/dist/indexer/search/fts-query.js +30 -41
  134. package/dist/indexer/search/ranking.js +28 -154
  135. package/dist/indexer/search/search-attribution.js +12 -32
  136. package/dist/indexer/search/search-fields.js +11 -15
  137. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  138. package/dist/indexer/search/search-source.js +1 -4
  139. package/dist/indexer/usage/usage-events.js +2 -7
  140. package/dist/integrations/agent/engine-fallback.js +23 -40
  141. package/dist/integrations/agent/engine-resolution.js +93 -183
  142. package/dist/integrations/agent/execution.js +507 -0
  143. package/dist/integrations/agent/model-map.js +28 -156
  144. package/dist/integrations/agent/request-lowering.js +66 -141
  145. package/dist/integrations/agent/runner-dispatch.js +143 -321
  146. package/dist/integrations/agent/runner.js +54 -14
  147. package/dist/integrations/lockfile.js +53 -101
  148. package/dist/llm/embedders/deterministic.js +2 -3
  149. package/dist/llm/embedders/profile.js +71 -0
  150. package/dist/llm/embedders/remote.js +10 -15
  151. package/dist/llm/graph-extract.js +3 -12
  152. package/dist/llm/index-passes.js +3 -5
  153. package/dist/llm/memory-infer.js +1 -2
  154. package/dist/llm/metadata-enhance.js +1 -2
  155. package/dist/llm/structured-call.js +5 -24
  156. package/dist/output/generic-render.js +23 -11
  157. package/dist/output/html-render.js +13 -10
  158. package/dist/output/render-registry.js +3 -32
  159. package/dist/output/shapes/helpers.js +2 -34
  160. package/dist/output/shapes/passthrough.js +1 -9
  161. package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
  162. package/dist/output/text/command-format.js +60 -23
  163. package/dist/output/text/helpers.js +1 -1
  164. package/dist/output/text/migrate.js +5 -14
  165. package/dist/output/text/proposal-format.js +1 -2
  166. package/dist/output/text/workflow-format.js +0 -32
  167. package/dist/output/text.js +2 -0
  168. package/dist/registry/factory.js +4 -19
  169. package/dist/registry/network.js +66 -220
  170. package/dist/registry/providers/index.js +0 -2
  171. package/dist/registry/providers/skills-sh.js +3 -14
  172. package/dist/registry/providers/static-index.js +24 -26
  173. package/dist/registry/resolve.js +55 -131
  174. package/dist/scripts/akm-migrate-node.js +43937 -93313
  175. package/dist/scripts/akm-migrate.js +43697 -93071
  176. package/dist/setup/registry-stash-loader.js +4 -13
  177. package/dist/setup/semantic-assets.js +3 -44
  178. package/dist/setup/setup.js +1 -1
  179. package/dist/setup/steps/tasks.js +25 -15
  180. package/dist/sources/provider-factory.js +17 -18
  181. package/dist/sources/providers/filesystem.js +2 -3
  182. package/dist/sources/providers/git-install.js +7 -1
  183. package/dist/sources/providers/git-provider.js +0 -3
  184. package/dist/sources/providers/git-stash.js +0 -17
  185. package/dist/sources/providers/npm.js +2 -4
  186. package/dist/sources/providers/provider-utils.js +5 -10
  187. package/dist/sources/providers/website.js +0 -2
  188. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  189. package/dist/sources/website-url.js +2 -2
  190. package/dist/storage/database.js +9 -35
  191. package/dist/storage/repositories/improve-ledger-repository.js +168 -0
  192. package/dist/storage/repositories/index-connection.js +34 -70
  193. package/dist/storage/repositories/index-entries-repository.js +69 -111
  194. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  195. package/dist/storage/repositories/index-entry-schema.js +83 -269
  196. package/dist/storage/repositories/index-fts-repository.js +86 -256
  197. package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
  198. package/dist/storage/repositories/index-meta-repository.js +6 -4
  199. package/dist/storage/repositories/index-schema.js +192 -220
  200. package/dist/storage/repositories/index-utility-repository.js +8 -29
  201. package/dist/storage/repositories/index-vec-repository.js +133 -414
  202. package/dist/storage/repositories/outcome-repository.js +2 -1
  203. package/dist/storage/repositories/proposals-repository.js +35 -0
  204. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  205. package/dist/storage/repositories/task-history-repository.js +26 -4
  206. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  207. package/dist/storage/sqlite-migrations.js +136 -0
  208. package/dist/storage/sqlite-pragmas.js +11 -9
  209. package/dist/storage/sqlite-transaction.js +170 -0
  210. package/dist/storage/state-db-integrity.js +34 -27
  211. package/dist/tasks/activation-config.js +134 -62
  212. package/dist/tasks/backends/cron.js +129 -277
  213. package/dist/tasks/backends/exec-utils.js +2 -5
  214. package/dist/tasks/backends/launchd.js +125 -745
  215. package/dist/tasks/backends/schtasks.js +101 -620
  216. package/dist/tasks/prepare/prepare-support.js +5 -15
  217. package/dist/tasks/prepare/prepare.js +0 -2
  218. package/dist/tasks/resolve-akm-bin.js +20 -79
  219. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  220. package/dist/tasks/scheduler-binding.js +18 -238
  221. package/dist/tasks/scheduler-invocation.js +52 -52
  222. package/dist/tasks/scheduler-lock.js +53 -0
  223. package/dist/tasks/scheduler-sync.js +363 -679
  224. package/dist/tasks/source/parse-task-source.js +160 -10
  225. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  226. package/dist/tasks/source/task-to-v4.js +2 -2
  227. package/dist/workflows/authoring/authoring.js +3 -12
  228. package/dist/workflows/compile.js +211 -0
  229. package/dist/workflows/concurrency-policy.js +13 -74
  230. package/dist/workflows/exec/child-invocation.js +3 -17
  231. package/dist/workflows/exec/child-workflow.js +32 -141
  232. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  233. package/dist/workflows/exec/environment.js +98 -0
  234. package/dist/workflows/exec/exec-unit.js +33 -140
  235. package/dist/workflows/exec/frozen-judge.js +7 -59
  236. package/dist/workflows/exec/native-executor.js +82 -341
  237. package/dist/workflows/exec/param-secrets.js +29 -47
  238. package/dist/workflows/exec/run-workflow.js +154 -387
  239. package/dist/workflows/exec/scheduler.js +9 -36
  240. package/dist/workflows/exec/step-work.js +127 -430
  241. package/dist/workflows/exec/unit-dispatch.js +11 -63
  242. package/dist/workflows/exec/unit-writer.js +8 -52
  243. package/dist/workflows/exec/worktree.js +39 -273
  244. package/dist/workflows/freeze/child-output-references.js +4 -15
  245. package/dist/workflows/freeze/environment.js +99 -92
  246. package/dist/workflows/freeze/freeze.js +172 -0
  247. package/dist/workflows/freeze/step-values.js +19 -21
  248. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  249. package/dist/workflows/freeze/targets/command.js +10 -33
  250. package/dist/workflows/freeze/targets/script.js +5 -12
  251. package/dist/workflows/freeze/targets/shell.js +3 -6
  252. package/dist/workflows/freeze/targets/task.js +25 -80
  253. package/dist/workflows/freeze/task-bindings.js +20 -67
  254. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  255. package/dist/workflows/ir/params.js +6 -51
  256. package/dist/workflows/ir/plan-hash.js +2 -34
  257. package/dist/workflows/parser.js +140 -43
  258. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  259. package/dist/workflows/renderer.js +36 -69
  260. package/dist/workflows/resource-limits.js +12 -120
  261. package/dist/workflows/runtime/agent-identity.js +8 -40
  262. package/dist/workflows/runtime/run-outputs.js +3 -6
  263. package/dist/workflows/runtime/run-plan.js +316 -0
  264. package/dist/workflows/runtime/runs.js +48 -200
  265. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  266. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  267. package/dist/workflows/validate-summary.js +2 -7
  268. package/docs/integration/bundling-akm.md +49 -42
  269. package/docs/migration/README.md +1 -0
  270. package/docs/migration/release-notes/0.9.17.md +41 -0
  271. package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
  272. package/docs/reference/cli.md +182 -125
  273. package/docs/reference/configuration.md +49 -56
  274. package/docs/reference/data-and-telemetry.md +19 -20
  275. package/docs/reference/tasks.md +86 -38
  276. package/docs/reference/workflow-schema.md +14 -18
  277. package/docs/reference/workflows.md +6 -9
  278. package/package.json +1 -1
  279. package/schemas/akm-config.json +87 -406
  280. package/dist/commands/health/advisories.js +0 -150
  281. package/dist/commands/health/metrics.js +0 -329
  282. package/dist/commands/health/surfaces.js +0 -102
  283. package/dist/commands/improve/anti-collapse.js +0 -83
  284. package/dist/commands/improve/collapse-detector.js +0 -432
  285. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  286. package/dist/commands/improve/consolidate/merge.js +0 -146
  287. package/dist/commands/improve/distill/promote-memory.js +0 -329
  288. package/dist/commands/improve/distill/quality-gate.js +0 -500
  289. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  290. package/dist/commands/improve/proposal-envelope.js +0 -31
  291. package/dist/commands/improve/run-context.js +0 -123
  292. package/dist/commands/improve/shared.js +0 -21
  293. package/dist/commands/improve/source-identity.js +0 -28
  294. package/dist/commands/improve/triage.js +0 -96
  295. package/dist/commands/proposal/drain-policies.js +0 -151
  296. package/dist/commands/sources/update-transaction.js +0 -220
  297. package/dist/core/action-contributors.js +0 -28
  298. package/dist/core/config/config-version-shim.js +0 -101
  299. package/dist/core/config/retired-experimental-keys-shim.js +0 -62
  300. package/dist/core/fs-txn.js +0 -405
  301. package/dist/core/lexical-score.js +0 -25
  302. package/dist/core/maintenance-barrier.js +0 -167
  303. package/dist/execution/executable-identity.js +0 -105
  304. package/dist/execution/guarded-source.js +0 -427
  305. package/dist/indexer/graph/graph-boost.js +0 -427
  306. package/dist/indexer/graph/graph-dedup.js +0 -95
  307. package/dist/indexer/search/name-match.js +0 -35
  308. package/dist/indexer/search/ranking-contributors.js +0 -515
  309. package/dist/indexer/walk/project-context.js +0 -192
  310. package/dist/integrations/agent/execution-cascade.js +0 -566
  311. package/dist/integrations/agent/execution-definitions.js +0 -202
  312. package/dist/integrations/agent/execution-lowering.js +0 -841
  313. package/dist/integrations/agent/execution-preparation.js +0 -98
  314. package/dist/integrations/agent/inline-execution.js +0 -74
  315. package/dist/registry/create-provider-registry.js +0 -29
  316. package/dist/registry/pinned-request-helper.js +0 -247
  317. package/dist/registry/pinned-transport.js +0 -717
  318. package/dist/sources/providers/index.js +0 -14
  319. package/dist/storage/engines/sqlite-migrations.js +0 -271
  320. package/dist/storage/repositories/canaries-repository.js +0 -107
  321. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  322. package/dist/storage/repositories/registry-cache.js +0 -113
  323. package/dist/tasks/scheduler-sync-preview.js +0 -52
  324. package/dist/workflows/freeze/resolve-steps.js +0 -86
  325. package/dist/workflows/freeze/source-freeze.js +0 -64
  326. package/dist/workflows/ir/compile.js +0 -321
  327. package/dist/workflows/ir/environment-v4.js +0 -330
  328. package/dist/workflows/ir/freeze-v4.js +0 -153
  329. package/dist/workflows/ir/schema-v4.js +0 -745
  330. package/dist/workflows/ir/schema.js +0 -354
  331. package/dist/workflows/program/schema.js +0 -78
  332. package/dist/workflows/runtime/checkin.js +0 -57
  333. package/dist/workflows/runtime/plan-classifier.js +0 -196
  334. package/dist/workflows/runtime/unit-checkin.js +0 -45
  335. package/dist/workflows/runtime/unit-phases.js +0 -20
  336. package/dist/workflows/schema.js +0 -4
  337. package/dist/workflows/source-ir/compile.js +0 -200
  338. package/dist/workflows/source-ir/program.js +0 -50
  339. package/dist/workflows/source-ir/result.js +0 -26
  340. package/dist/workflows/source-ir/schema.js +0 -786
  341. package/dist/workflows/source-ir/triggers.js +0 -79
  342. package/dist/workflows/source-ir/uses.js +0 -40
  343. package/dist/workflows/validator.js +0 -60
@@ -1,446 +1,147 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- /** Pure whole-set scheduler reconciliation planning. */
5
- import { createHash } from "node:crypto";
4
+ /**
5
+ * Scheduler reconciliation: compile the desired bindings from a bundle's
6
+ * task and workflow sources, then diff them against the rows the native
7
+ * scheduler holds.
8
+ *
9
+ * `compileSchedulerSources` reads each source once. One source that fails
10
+ * to parse or prepare is reported and skipped; every other source still
11
+ * compiles (#867). `planSchedulerSync` is pure: given the desired bindings,
12
+ * the installed rows, and the bundles this sync owns, it decides which rows
13
+ * to install, update, remove, or leave alone. Rows it cannot attribute to
14
+ * one of those bundles are never touched.
15
+ */
16
+ import fs from "node:fs";
6
17
  import path from "node:path";
7
- import { loadAdapterExecutionSource } from "../commands/command/execution-source-loader.js";
8
- import { makeBundleRef } from "../core/asset/asset-ref.js";
18
+ import { makeBundleRef, parseBundleRef } from "../core/asset/asset-ref.js";
9
19
  import { compareCodePoints, toPosix } from "../core/common.js";
10
20
  import { UsageError } from "../core/errors.js";
11
21
  import { canonicalizeWorkflowName, WORKFLOW_EXTENSIONS } from "../core/recognition-util.js";
12
- import { captureGuardedDirectoryManifest, captureGuardedExecutionSource, GuardedExecutionSourceCollector, } from "../execution/guarded-source.js";
13
22
  import { applyInputDefaults, validateInputs } from "../execution/input-contract.js";
14
- import { compileWorkflowPlan } from "../workflows/ir/compile.js";
15
- import { compileResolveFreezeWorkflowV4 } from "../workflows/ir/freeze-v4.js";
16
- import { canonicalJson, computePlanHash } from "../workflows/ir/plan-hash.js";
17
- import { WorkflowSourceCollisionError, WorkflowSourceNameError, WorkflowSourceRejectionError, workflowNameForSourcePath, } from "../workflows/source-files.js";
18
- import { compileWorkflowSource } from "../workflows/source-ir/compile.js";
23
+ import { checkWorkflowPlan, compileWorkflowSource } from "../workflows/compile.js";
24
+ import { WorkflowSourceCollisionError, WorkflowSourceNameError, workflowNameForSourcePath, } from "../workflows/source-files.js";
19
25
  import { prepareTaskV3Execution } from "./prepare/prepare.js";
20
26
  import { parseSchedule } from "./schedule.js";
21
- import { assertSchedulerNativeArtifactCardinality, compileTaskSchedulerBindings, compileWorkflowSchedulerBindings, schedulerBindingNativeId, schedulerBindingOrdinal, schedulerNativeArtifactKey, schedulerNativeArtifactOwner, schedulerNativeBindingId, } from "./scheduler-binding.js";
27
+ import { compileTaskSchedulerBindings, compileWorkflowSchedulerBindings, schedulerBindingNativeId, schedulerNativeBindingId, } from "./scheduler-binding.js";
22
28
  import { parseTaskSource } from "./source/parse-task-source.js";
23
29
  import { projectTaskSourceV4 } from "./source/project-v4.js";
24
30
  import { taskSourceErrorDetail } from "./source-v3.js";
25
- export function computeSchedulerExecutionEvidenceDigest(planHash, sourceReadSet) {
26
- const envelope = canonicalJson({
27
- version: 1,
28
- planHash,
29
- sourceReadSet: sourceReadSet.map((snapshot) => ({
30
- identity: snapshot.identity,
31
- containmentPhysicalIdentity: snapshot.containmentPhysicalIdentity,
32
- physicalIdentity: snapshot.physicalIdentity,
33
- size: snapshot.size,
34
- })),
35
- });
36
- return createHash("sha256").update("akm.scheduler.workflow-evidence\0v1\0").update(envelope).digest("hex");
37
- }
38
31
  const SCHEDULER_PROJECTION_CONFIG = Object.freeze({
39
32
  configVersion: "0.9.0",
40
33
  semanticSearchMode: "off",
41
34
  });
42
- export async function prepareSchedulerSyncSourceSet(input) {
43
- const collector = new SchedulerSourceCollector(input);
44
- const compiled = await compileDesiredSourceSet(input, collector);
45
- const sourceSnapshot = collector.snapshot();
46
- assertSchedulerSourceSnapshot(sourceSnapshot);
47
- return Object.freeze({
48
- desired: compiled.desired,
49
- sourceSnapshot,
50
- executableWorkflows: compiled.executableWorkflows,
51
- failures: compiled.failures,
52
- });
53
- }
54
- function schedulerActivationKey(kind, ref) {
55
- return `${kind}\0${ref}`;
56
- }
57
- export function finalizeSchedulerSyncPlan(input, prepared) {
58
- const inspection = inspectionForPlan(input);
59
- const coherentInput = {
60
- ...input,
61
- installed: inspection.installed,
62
- nativeArtifacts: inspection.artifacts,
63
- };
64
- const desired = prepared.desired;
65
- assertUniqueDesiredIds(desired);
66
- assertSchedulerBackendInspection(inspection, desired, input.inspection !== undefined);
67
- assertNoForeignIds(desired, coherentInput);
68
- const scopedInstalled = coherentInput.installed.filter((entry) => belongsToBundle(entry, coherentInput));
69
- const present = new Map(scopedInstalled.map((entry) => [entry.id, entry]));
70
- const installed = [];
71
- const updated = [];
72
- const unchanged = [];
73
- const operations = [];
74
- for (const binding of desired) {
75
- const current = present.get(binding.id);
76
- const options = installOptionsFor(coherentInput, current);
77
- const resultFingerprint = coherentInput.expectedSignature?.(binding, options);
78
- if (!current) {
79
- installed.push(binding.id);
80
- operations.push(freezeOperation({
81
- kind: "install",
82
- binding,
83
- expected: freezeMutationExpectation(expectationForBinding(binding, "absent")),
84
- ...(resultFingerprint !== undefined ? { resultFingerprint } : {}),
85
- ...(options ? { options } : {}),
86
- }));
87
- continue;
88
- }
89
- if (current.signature !== undefined && resultFingerprint !== undefined && current.signature === resultFingerprint) {
90
- unchanged.push(binding.id);
91
- continue;
92
- }
93
- const artifact = exactInstalledArtifact(binding.id, current, inspection.artifacts);
94
- const priorFingerprint = artifact?.fingerprint ?? current.signature;
95
- if (artifact === undefined || priorFingerprint === undefined) {
96
- throw new UsageError(`Installed scheduler binding ${JSON.stringify(binding.id)} has no exact native fingerprint; refusing update.`, "RESOURCE_ALREADY_EXISTS");
97
- }
98
- updated.push(binding.id);
99
- operations.push(freezeOperation({
100
- kind: "update",
101
- binding,
102
- expected: freezeMutationExpectation(expectationForBinding(binding, "present", priorFingerprint)),
103
- ...(resultFingerprint !== undefined ? { resultFingerprint } : {}),
104
- ...(options ? { options } : {}),
105
- }));
106
- }
107
- const desiredIds = new Set(desired.map(({ id }) => id));
108
- const removed = scopedInstalled
109
- .map(({ id }) => id)
110
- .filter((id) => !desiredIds.has(id))
111
- .sort(compareCodePoints);
112
- for (const id of removed) {
113
- const current = present.get(id);
114
- operations.push(buildSchedulerRemoveOperation(id, current, inspection.artifacts, coherentInput));
115
- }
116
- return Object.freeze({
117
- desired,
118
- installed: Object.freeze(installed),
119
- updated: Object.freeze(updated),
120
- removed: Object.freeze(removed),
121
- unchanged: Object.freeze(unchanged),
122
- operations: Object.freeze(operations),
123
- sourceSnapshot: prepared.sourceSnapshot,
124
- failures: prepared.failures,
125
- });
126
- }
127
- /** Validate one coherent whole-backend read before deriving any mutation plan. */
128
- export function assertSchedulerBackendInspection(inspection, desired = [], requireCompleteFingerprint = true) {
129
- assertCoherentInspection(inspection, requireCompleteFingerprint);
130
- assertUniqueInstalledIds(inspection.installed);
131
- assertSchedulerNativeArtifactOwnership(desired, inspection.artifacts);
132
- }
133
- /**
134
- * Build the exact removal operation for one installed binding: same
135
- * exact-native-fingerprint / ordinal-attribution safety checks
136
- * `finalizeSchedulerSyncPlan`'s remove loop always applied, factored out so
137
- * `akm task prune` (#851) can build removal operations for entries
138
- * `belongsToBundle` structurally can't see (unresolvable ownership) without
139
- * re-deriving — or weakening — this logic. Throws the same `UsageError`s a
140
- * sync removal would on an inexact match; callers computing prune candidates
141
- * should only pass entries they've already independently confirmed are safe
142
- * to remove.
143
- */
144
- export function buildSchedulerRemoveOperation(id, current, artifacts, input) {
145
- if (!current?.invocation) {
146
- throw nativeArtifactCollision({ nativeId: current?.nativeId ?? schedulerNativeBindingId(id), bindingId: id }, { nativeId: current?.nativeId ?? schedulerNativeBindingId(id) });
147
- }
148
- const nativeId = exactInstalledNativeId(id, current, artifacts);
149
- const artifact = artifacts.find((candidate) => candidate.nativeId === nativeId && candidate.bindingId === id);
150
- const priorFingerprint = current.signature ?? artifact?.fingerprint;
151
- if (!artifact || priorFingerprint === undefined) {
152
- throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} has no exact native fingerprint; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
153
- }
154
- const logicalSource = installedLogicalSource(current.invocation, input);
155
- const ordinal = schedulerBindingOrdinal(id, logicalSource, current.invocation);
156
- if (ordinal === undefined) {
157
- throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} cannot be attributed to an exact schedule ordinal; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
158
- }
159
- return Object.freeze({
160
- kind: "remove",
161
- id,
162
- nativeId,
163
- expected: freezeRemovalExpectation({
164
- state: "present",
165
- bindingId: id,
166
- nativeId,
167
- logicalSource,
168
- ordinal,
169
- invocation: current.invocation,
170
- fingerprint: priorFingerprint,
171
- }),
172
- ...(current.ownerBundlePath !== undefined ? { ownerBundlePath: current.ownerBundlePath } : {}),
173
- });
174
- }
175
- function exactInstalledNativeId(logicalId, current, artifacts) {
176
- const exact = artifacts.find((artifact) => artifact.bindingId === logicalId);
177
- return exact?.nativeId ?? current?.nativeId ?? schedulerNativeBindingId(logicalId);
178
- }
179
- function exactInstalledArtifact(bindingId, current, artifacts) {
180
- const nativeId = current.nativeId ?? schedulerNativeBindingId(bindingId);
181
- return artifacts.find((artifact) => artifact.nativeId === nativeId && (artifact.bindingId === bindingId || artifact.bindingId === undefined));
182
- }
183
- function nativeArtifactsForPlan(input) {
184
- return (input.nativeArtifacts ??
185
- input.installed.map((entry) => ({
186
- nativeId: entry.nativeId ?? schedulerNativeBindingId(entry.id),
187
- bindingId: entry.id,
188
- ...(entry.invocation ? { invocation: Object.freeze([...entry.invocation]) } : {}),
189
- ...(entry.signature !== undefined ? { fingerprint: entry.signature } : {}),
190
- })));
191
- }
192
- function inspectionForPlan(input) {
193
- return input.inspection ?? Object.freeze({ installed: input.installed, artifacts: nativeArtifactsForPlan(input) });
194
- }
195
- export function assertSchedulerNativeArtifactOwnership(desired, installed) {
196
- const desiredByKey = new Map();
197
- for (const binding of desired) {
198
- const nativeId = schedulerBindingNativeId(binding);
199
- const key = schedulerNativeArtifactKey(nativeId);
200
- const prior = desiredByKey.get(key);
201
- if (prior && !sameDesiredArtifact(prior, binding)) {
202
- throw nativeArtifactCollision(desiredArtifact(prior), desiredArtifact(binding));
203
- }
204
- desiredByKey.set(key, binding);
205
- }
206
- const installedByKey = new Map();
207
- for (const artifact of installed) {
208
- const key = schedulerNativeArtifactKey(artifact.nativeId);
209
- const prior = installedByKey.get(key);
210
- if (prior) {
211
- throw nativeArtifactCollision(prior, artifact);
212
- }
213
- installedByKey.set(key, artifact);
214
- const wanted = desiredByKey.get(key);
215
- if (!wanted)
216
- continue;
217
- // Re-derive ownership from the artifact's own invocation content (not the
218
- // caller-supplied `bindingId` label) so a proven owner whose invocation
219
- // no longer matches the desired shape is an UPDATE, not a refusal — that
220
- // reconciliation happens below in finalizeSchedulerSyncPlan. An artifact
221
- // whose invocation content does not actually prove it belongs to
222
- // `wanted` (unproven, malformed, or a different logical owner) is still
223
- // a genuine collision.
224
- const provenBindingId = artifact.invocation !== undefined
225
- ? schedulerNativeArtifactOwner(artifact.nativeId, artifact.invocation)?.logicalId
226
- : undefined;
227
- if (artifact.nativeId !== schedulerBindingNativeId(wanted) || provenBindingId !== wanted.id) {
228
- throw nativeArtifactCollision(desiredArtifact(wanted), artifact);
229
- }
230
- }
231
- }
232
- function assertCoherentInspection(inspection, requireCompleteFingerprint = false) {
233
- const seenNativeKeys = new Set();
234
- for (const artifact of inspection.artifacts) {
235
- const key = schedulerNativeArtifactKey(artifact.nativeId);
236
- if (seenNativeKeys.has(key)) {
237
- throw new UsageError(`Scheduler inspection has duplicate normalized native artifact ${JSON.stringify(artifact.nativeId)}; expected cardinality one.`, "RESOURCE_ALREADY_EXISTS");
238
- }
239
- seenNativeKeys.add(key);
240
- }
241
- for (const installed of inspection.installed) {
242
- const nativeId = installed.nativeId ?? schedulerNativeBindingId(installed.id);
243
- const artifact = assertSchedulerNativeArtifactCardinality(inspection.artifacts, nativeId, 1);
244
- if (!artifact ||
245
- (installed.signature !== undefined &&
246
- (artifact.fingerprint !== undefined
247
- ? installed.signature !== artifact.fingerprint
248
- : requireCompleteFingerprint)) ||
249
- (artifact.bindingId !== undefined && artifact.bindingId !== installed.id)) {
250
- throw new UsageError(`Scheduler inspection is not coherent for ${JSON.stringify(nativeId)}: installed and native fingerprints differ.`, "RESOURCE_ALREADY_EXISTS");
251
- }
252
- if (installed.invocation !== undefined &&
253
- (artifact.invocation === undefined || !sameInvocation(installed.invocation, artifact.invocation))) {
254
- throw new UsageError(`Scheduler inspection is not coherent for ${JSON.stringify(nativeId)}: installed and native owners differ.`, "RESOURCE_ALREADY_EXISTS");
255
- }
256
- }
257
- }
258
- function nativeArtifactCollision(left, right) {
259
- const owner = (value) => value.bindingId === undefined || value.invocation === undefined
260
- ? `${JSON.stringify(value.nativeId)} (unproven owner)`
261
- : `binding ${JSON.stringify(value.bindingId)} invoking ${JSON.stringify(value.invocation)}`;
262
- return new UsageError(`Native scheduler artifact collision between ${owner(left)} and ${owner(right)}; refusing to overwrite an existing or ambiguous native owner.`, "RESOURCE_ALREADY_EXISTS");
263
- }
264
- function desiredArtifact(binding) {
265
- return {
266
- nativeId: schedulerBindingNativeId(binding),
267
- bindingId: binding.id,
268
- invocation: binding.invocation,
269
- };
270
- }
271
- function sameDesiredArtifact(left, right) {
272
- return (schedulerBindingNativeId(left) === schedulerBindingNativeId(right) &&
273
- left.id === right.id &&
274
- left.logicalSource.kind === right.logicalSource.kind &&
275
- left.logicalSource.ref === right.logicalSource.ref &&
276
- left.ordinal === right.ordinal &&
277
- left.executionEvidenceDigest === right.executionEvidenceDigest &&
278
- sameInvocation(left.invocation, right.invocation));
279
- }
280
- async function compileDesiredSourceSet(input, collector) {
281
- const bindings = [];
282
- const executableWorkflows = [];
35
+ export async function compileSchedulerSources(input) {
36
+ const sourceRoot = path.resolve(input.sourceRoot);
37
+ const desired = [];
283
38
  const failures = [];
284
- await compileTaskSources(input, collector, bindings, failures);
285
- await compileWorkflowSources(input, collector, bindings, executableWorkflows, failures);
286
- // Degrade, don't reject (#867): one source that fails to parse/prepare no
287
- // longer poisons the whole desired set — it is dropped from `desired` and
288
- // reported here instead, so every OTHER task/workflow still reconciles.
289
- // Genuinely cross-cutting integrity violations (duplicate ids, native
290
- // artifact ownership conflicts, an incoherent backend inspection) are
291
- // asserted separately in `finalizeSchedulerSyncPlan` and still hard-fail
292
- // the whole sync — this only relaxes the per-source parse/prepare gate.
39
+ await compileTaskSources({ ...input, sourceRoot }, desired, failures);
40
+ compileWorkflowSources({ ...input, sourceRoot }, desired, failures);
293
41
  return Object.freeze({
294
- desired: Object.freeze(bindings),
295
- executableWorkflows: Object.freeze(executableWorkflows.sort((left, right) => compareCodePoints(left.ref, right.ref))),
42
+ desired: Object.freeze(desired),
296
43
  failures: Object.freeze(failures.sort((left, right) => compareCodePoints(left.path, right.path))),
297
44
  });
298
45
  }
299
- async function compileTaskSources(input, collector, out, failures) {
300
- if (input.adapterId !== "akm" && input.adapterId !== "akm-task")
301
- return;
302
- const physicalOwners = new Map();
303
- for (const guarded of collector.authoredTaskSources(input.adapterId)) {
304
- const sourcePath = guarded.sourcePath;
305
- const relative = guarded.relativePath;
306
- const conceptId = relative.slice(0, -4);
307
- const id = input.adapterId === "akm-task" ? conceptId : path.basename(sourcePath, ".yml");
308
- const qualifiedRefForFailure = makeBundleRef(input.bundleName, conceptId);
309
- if (input.enabledActivations &&
310
- !input.enabledActivations.has(schedulerActivationKey("task", qualifiedRefForFailure))) {
46
+ async function compileTaskSources(input, desired, failures) {
47
+ for (const file of taskSourceFiles(input.sourceRoot, input.adapterId)) {
48
+ const relative = toPosix(path.relative(input.sourceRoot, file));
49
+ const conceptId = relative.slice(0, -".yml".length);
50
+ const id = input.adapterId === "akm-task" ? conceptId : path.basename(file, ".yml");
51
+ const ref = sourceRef(input.bundleName, conceptId);
52
+ if (ref === undefined || (input.enabledRefs && !input.enabledRefs.has(ref)))
311
53
  continue;
312
- }
313
54
  try {
314
- const physicalIdentity = guarded.physicalIdentity;
315
- const priorOwner = physicalOwners.get(physicalIdentity);
316
- if (priorOwner !== undefined && priorOwner !== sourcePath) {
317
- throw new UsageError(`Task sources ${JSON.stringify(priorOwner)} and ${JSON.stringify(sourcePath)} resolve to the same physical source identity; refusing canonical task identity collision.`, "RESOURCE_ALREADY_EXISTS");
318
- }
319
- physicalOwners.set(physicalIdentity, sourcePath);
320
- // Project BEFORE prepareTaskV3Execution so projectability is checked —
321
- // but build the scheduler bindings from the ORIGINAL task source v4
322
- // document, not the projection, which deliberately drops
323
- // `schedule[i].inputs` (project-v4.ts) —
324
- // schedule-supplied inputs are delivered through the scheduler
325
- // binding's own compiled invocation tail (P2b Lane B, spec §4.4,
326
- // B-N3), not through the prepare-seam projection. A task source v4
327
55
  const parsed = parseTaskSource({
328
- yaml: guarded.content,
329
- filePath: sourcePath,
56
+ yaml: fs.readFileSync(file, "utf8"),
57
+ filePath: file,
330
58
  workspaceRoot: input.sourceRoot,
331
59
  });
332
- const document = projectTaskSourceV4(parsed.v4);
333
- const qualifiedRef = makeBundleRef(input.bundleName, conceptId);
334
- // See assertTaskScheduleInputsSatisfyContract's own docblock (below in
335
- // this file) for why this second, defaults-applied gate exists.
336
- assertTaskScheduleInputsSatisfyContract(parsed.v4, qualifiedRef);
337
- await prepareTaskV3Execution(document, {
60
+ // A task with no `schedule:` is run by hand or by a workflow step; it
61
+ // contributes no binding and is not sync's to validate.
62
+ if (parsed.v4.schedule.length === 0)
63
+ continue;
64
+ assertTaskScheduleInputsSatisfyContract(parsed.v4, ref);
65
+ assertTaskScheduleCronValid(parsed.v4, input.backend);
66
+ // Project so a target that cannot resolve (a missing workflow, an
67
+ // unconfigured engine) is reported here rather than at fire time.
68
+ // Bindings compile from the original document: the projection drops
69
+ // `schedule[i].inputs`, which are delivered through the invocation tail.
70
+ await prepareTaskV3Execution(projectTaskSourceV4(parsed.v4), {
338
71
  taskId: id,
339
- taskRef: qualifiedRef,
72
+ taskRef: ref,
340
73
  bundleName: input.bundleName,
341
74
  bundleRoot: input.sourceRoot,
342
75
  config: input.config ?? SCHEDULER_PROJECTION_CONFIG,
343
76
  ...(input.resolveAsset ? { resolveAsset: input.resolveAsset } : {}),
344
- readFile: (file, bundleRoot) => collector.readBytes(file, bundleRoot ?? input.sourceRoot),
345
- commandSourceLoader: (ref, kind, options) => {
346
- const guardedOptions = {
347
- ...options,
348
- fileContext: (root, file) => collector.fileContext(root, file),
349
- };
350
- return kind === "command"
351
- ? loadAdapterExecutionSource(ref, "command", guardedOptions)
352
- : loadAdapterExecutionSource(ref, "persona", guardedOptions);
353
- },
354
77
  });
355
- const relSource = toPosix(path.relative(input.sourceRoot, sourcePath));
356
- const sourceBindings = compileTaskSchedulerBindings({
78
+ desired.push(...compileTaskSchedulerBindings({
357
79
  id,
358
- qualifiedRef,
359
- ...(input.bundleTarget ? { bundleTarget: input.bundleTarget } : {}),
80
+ qualifiedRef: ref,
360
81
  schedules: parsed.v4.schedule.map((schedule) => ({
361
82
  cron: schedule.cron,
362
83
  ordinal: schedule.ordinal,
363
- source: `${relSource}:${schedule.source}`,
364
- // P2b Lane B (spec §4.4, B-N3): delivered through the compiled
365
- // binding's own invocation tail below — the F-B2 flip that closes
366
- // the P2a B-38 "validated but not yet delivered" gap this comment
367
- // used to describe.
84
+ source: `${relative}:${schedule.source}`,
368
85
  inputs: schedule.inputs,
369
86
  })),
370
- });
371
- assertTaskScheduleCronValid(parsed.v4, input.backend);
372
- out.push(...sourceBindings);
87
+ }));
373
88
  }
374
89
  catch (cause) {
375
- failures.push(taskFailure(sourcePath, qualifiedRefForFailure, cause));
376
- }
377
- }
378
- }
379
- /**
380
- * P2b Lane B (spec §4.4, rows B-50/F-B2): validate every v4 `schedule:`
381
- * entry's `inputs` against the task's OWN declared contract WITH DEFAULTS
382
- * APPLIED — the same `applyInputDefaults` + `validateInputs` pair
383
- * `akm task run` uses (`src/tasks/run/load-task.ts`). `parseTaskSource`'s
384
- * own parse-time check (`task-source-v4.ts`'s `parseScheduleEntry`) already
385
- * rejects an unknown/malformed entry against the RAW supplied values; this
386
- * is a deliberate second, independent gate over the DEFAULTED view — the
387
- * exact set of values a compiled invocation actually delivers — so a
388
- * violation fails HERE, recorded as a task failure at sync, rather than
389
- * surfacing for the first time when the scheduler fires the invocation.
390
- *
391
- * Extracted so `akm task validate` can run the IDENTICAL gate
392
- * over a bare file path without a second, potentially-diverging copy of
393
- * this check. `refLabel` is only the text embedded in the thrown message —
394
- * `compileTaskSources` passes its bundle-qualified ref; `akm task validate`,
395
- * which never resolves a bundle for a bare file, passes the file path.
396
- */
397
- export function assertTaskScheduleInputsSatisfyContract(v4, refLabel) {
398
- const contract = v4.inputs ?? {};
399
- for (const scheduleEntry of v4.schedule) {
400
- const defaultedInputs = applyInputDefaults(contract, { ...scheduleEntry.inputs });
401
- const errors = validateInputs(contract, defaultedInputs);
402
- if (errors.length > 0) {
403
- throw new UsageError(`Task ${JSON.stringify(refLabel)} schedule[${scheduleEntry.ordinal}].inputs does not satisfy ` +
404
- `its declared inputs once defaults are applied: ${errors.join("; ")}`, "TASK_SOURCE_INVALID");
90
+ const detail = taskSourceErrorDetail(cause);
91
+ failures.push({ path: file, ref, reason: detail === errorMessage(cause) ? `${file}: ${detail}` : detail });
405
92
  }
406
93
  }
407
94
  }
408
- /**
409
- * Validate every v4 `schedule:` entry's `cron` against the active scheduler
410
- * backend's dialect (`parseSchedule`, `./schedule.ts`) — cron is the most
411
- * permissive of the three backends, so a task authored on Linux can carry a
412
- * cron expression launchd/schtasks cannot translate; `compileTaskSources`
413
- * re-checks against the LOCAL backend on every sync rather than trusting
414
- * whatever backend the file was authored against.
415
- *
416
- * Extracted alongside {@link assertTaskScheduleInputsSatisfyContract}
417
- * so `akm task validate` shares this exact gate instead of a second copy
418
- * that could silently drift from what `akm task sync` actually enforces.
419
- */
420
- export function assertTaskScheduleCronValid(v4, backend) {
421
- for (const scheduleEntry of v4.schedule) {
422
- parseSchedule(scheduleEntry.cron, backend);
95
+ /** Task candidates: `tasks/*.yml` for the akm adapter, every `.yml` in the tree for `akm-task`. */
96
+ function taskSourceFiles(sourceRoot, adapterId) {
97
+ if (adapterId === "akm") {
98
+ const dir = path.join(sourceRoot, "tasks");
99
+ const names = listDirectory(dir);
100
+ return names
101
+ .filter((name) => name.endsWith(".yml"))
102
+ .map((name) => path.join(dir, name))
103
+ .filter((file) => statOrUndefined(file)?.isFile() === true)
104
+ .sort(compareCodePoints);
423
105
  }
106
+ if (adapterId === "akm-task")
107
+ return walkFiles(sourceRoot).filter((file) => file.endsWith(".yml"));
108
+ return [];
424
109
  }
425
- async function compileWorkflowSources(input, collector, out, evidence, failures) {
110
+ function compileWorkflowSources(input, desired, failures) {
426
111
  if (input.adapterId !== "akm" && input.adapterId !== "akm-workflow")
427
112
  return;
428
- const lookups = enumerateWorkflowLookups(input, collector, failures);
429
- for (const [canonicalName, sources] of lookups) {
430
- const failurePath = sources[0]?.sourcePath ?? canonicalName;
431
- const failureRef = makeBundleRef(input.bundleName, input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName);
432
- if (input.enabledActivations && !input.enabledActivations.has(schedulerActivationKey("workflow", failureRef))) {
113
+ const root = input.adapterId === "akm" ? path.join(input.sourceRoot, "workflows") : input.sourceRoot;
114
+ const byName = new Map();
115
+ for (const file of walkFiles(root)) {
116
+ if (path.basename(file).toLowerCase() === "readme.md")
117
+ continue;
118
+ const authoredName = workflowNameForSourcePath(input.sourceRoot, input.adapterId, file);
119
+ if (authoredName === undefined)
120
+ continue;
121
+ const extension = path.posix.extname(authoredName).toLowerCase();
122
+ const stem = authoredName.slice(0, -extension.length).toLowerCase();
123
+ const nestedSuffix = WORKFLOW_EXTENSIONS.find((suffix) => stem.endsWith(suffix));
124
+ if (nestedSuffix) {
125
+ const relative = toPosix(path.relative(input.sourceRoot, file));
126
+ failures.push({ path: file, reason: new WorkflowSourceNameError(relative, nestedSuffix).message });
433
127
  continue;
434
128
  }
129
+ const canonicalName = canonicalizeWorkflowName(authoredName);
130
+ byName.set(canonicalName, [...(byName.get(canonicalName) ?? []), file]);
131
+ }
132
+ for (const [canonicalName, sources] of [...byName].sort(([left], [right]) => compareCodePoints(left, right))) {
133
+ const conceptId = input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName;
134
+ const ref = sourceRef(input.bundleName, conceptId);
135
+ if (ref === undefined || (input.enabledRefs && !input.enabledRefs.has(ref)))
136
+ continue;
137
+ const file = sources[0];
435
138
  try {
436
139
  if (sources.length > 1) {
437
- throw new WorkflowSourceCollisionError(input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName, sources.map((source) => source.relativePath));
140
+ throw new WorkflowSourceCollisionError(conceptId, sources.map((source) => toPosix(path.relative(input.sourceRoot, source))));
438
141
  }
439
- const guarded = sources[0];
440
- if (!guarded)
441
- continue;
442
- const compiled = compileWorkflowSource(guarded.content, {
443
- path: guarded.relativePath,
142
+ const relative = toPosix(path.relative(input.sourceRoot, file));
143
+ const compiled = compileWorkflowSource(fs.readFileSync(file, "utf8"), {
144
+ path: relative,
444
145
  workspaceRoot: input.sourceRoot,
445
146
  });
446
147
  if (!compiled.ok) {
@@ -448,336 +149,319 @@ async function compileWorkflowSources(input, collector, out, evidence, failures)
448
149
  .map((error) => `${error.path}:${error.line ?? 1} [${error.code}] ${error.message}`)
449
150
  .join("; "), "WORKFLOW_SOURCE_INVALID");
450
151
  }
451
- const planDraft = compileWorkflowPlan(compiled.ir, canonicalName);
152
+ const planDraft = checkWorkflowPlan(compiled.plan);
452
153
  if (!planDraft.ok) {
453
- throw new UsageError(planDraft.errors.map((error) => `${guarded.relativePath}:${error.line} ${error.message}`).join("; "), "WORKFLOW_SOURCE_INVALID");
154
+ throw new UsageError(planDraft.errors.map((error) => `${relative}:${error.line} ${error.message}`).join("; "), "WORKFLOW_SOURCE_INVALID");
454
155
  }
455
- const conceptId = input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName;
456
- const qualifiedRef = makeBundleRef(input.bundleName, conceptId);
457
- const asset = {
458
- ref: qualifiedRef,
459
- path: guarded.sourcePath,
460
- sourcePath: input.sourceRoot,
461
- adapterId: input.adapterId,
462
- title: canonicalName,
463
- steps: [],
464
- sourceIr: compiled.ir,
465
- };
466
- const frozen = await compileResolveFreezeWorkflowV4(asset, input.config ?? schedulerProjectionConfig(input), {
467
- sourceCollector: collector.executionCollector(),
468
- });
469
- const planHash = computePlanHash(frozen.plan);
470
- const executionEvidenceDigest = computeSchedulerExecutionEvidenceDigest(planHash, frozen.plan.sourceReadSet);
471
- evidence.push(Object.freeze({
472
- ref: qualifiedRef,
473
- irVersion: 5,
474
- planHash,
475
- sourceReadSet: frozen.plan.sourceReadSet,
476
- executionEvidenceDigest,
156
+ const schedules = (compiled.plan.schedules ?? []).map((schedule) => ({
157
+ cron: schedule.cron,
158
+ source: `${relative}:${schedule.line}`,
159
+ ordinal: schedule.ordinal,
477
160
  }));
478
- const schedules = compiled.ir.triggers.flatMap((trigger) => trigger.kind === "schedule"
479
- ? [
480
- {
481
- cron: trigger.cron,
482
- source: `${trigger.source.path}:${trigger.source.start}`,
483
- ordinal: trigger.ordinal,
484
- },
485
- ]
486
- : []);
487
- for (const binding of compileWorkflowSchedulerBindings({
488
- qualifiedRef,
489
- schedules,
490
- executionEvidenceDigest,
491
- })) {
161
+ for (const binding of compileWorkflowSchedulerBindings({ qualifiedRef: ref, schedules })) {
492
162
  parseSchedule(binding.cron, input.backend);
493
- out.push(binding);
163
+ desired.push(binding);
494
164
  }
495
165
  }
496
166
  catch (cause) {
497
- failures.push(workflowFailure(failurePath, failureRef, cause));
167
+ failures.push({ path: file, ref, reason: errorMessage(cause) });
498
168
  }
499
169
  }
500
170
  }
501
- function schedulerProjectionConfig(input) {
502
- return Object.freeze({
503
- ...SCHEDULER_PROJECTION_CONFIG,
504
- defaultBundle: input.bundleName,
505
- bundles: {
506
- [input.bundleName]: {
507
- path: input.sourceRoot,
508
- components: { main: { root: ".", adapter: input.adapterId } },
509
- },
510
- },
511
- });
512
- }
513
- function enumerateWorkflowLookups(input, collector, failures) {
514
- const lookups = new Map();
515
- for (const guarded of collector.authoredWorkflowSources(input.adapterId)) {
516
- const sourcePath = guarded.sourcePath;
517
- if (path.basename(sourcePath).toLowerCase() === "readme.md")
518
- continue;
519
- const authoredName = workflowNameForSourcePath(input.sourceRoot, input.adapterId, sourcePath);
520
- if (authoredName === undefined)
521
- continue;
522
- const extension = path.posix.extname(authoredName).toLowerCase();
523
- const stem = authoredName.slice(0, -extension.length).toLowerCase();
524
- const nestedSuffix = WORKFLOW_EXTENSIONS.find((suffix) => stem.endsWith(suffix));
525
- if (nestedSuffix) {
526
- failures.push(workflowFailure(sourcePath, undefined, new WorkflowSourceNameError(guarded.relativePath, nestedSuffix)));
527
- continue;
528
- }
529
- const canonicalName = canonicalizeWorkflowName(authoredName);
530
- const owners = lookups.get(canonicalName) ?? [];
531
- owners.push(guarded);
532
- lookups.set(canonicalName, owners);
171
+ /** A file whose name cannot form a ref (a `#` in it) cannot be enabled, so it is not this sync's. */
172
+ function sourceRef(bundleName, conceptId) {
173
+ try {
174
+ return makeBundleRef(bundleName, conceptId);
533
175
  }
534
- return new Map([...lookups]
535
- .sort(([left], [right]) => compareCodePoints(left, right))
536
- .map(([name, sources]) => [name, Object.freeze(sources.sort(compareGuardedSources))]));
537
- }
538
- function installOptionsFor(input, current) {
539
- if (current && !input.rebind) {
540
- return Object.freeze({
541
- ...(input.bundleTarget ? { target: input.bundleTarget } : {}),
542
- binding: Object.freeze([...current.binding]),
543
- contextPath: current.contextPath,
544
- });
176
+ catch {
177
+ return undefined;
545
178
  }
546
- return input.installOptions ? Object.freeze({ ...input.installOptions }) : undefined;
547
179
  }
548
- function belongsToBundle(entry, input) {
549
- if (input.bundlePath !== undefined && entry.target === input.bundleName) {
550
- // Path-scoped (#846), primary/unconfigured-bundle sync only: the name
551
- // already matches, but a display name derived from a directory
552
- // basename is not an identity — two unrelated bundles can legitimately
553
- // share one. Require the entry's own scheduler-context descriptor to
554
- // additionally confirm the resolved path. An entry whose owning path
555
- // cannot be established is never assumed to be ours — that silent
556
- // assumption is exactly what let an isolated/foreign bundle's sync
557
- // reach for another bundle's real scheduler entries. (`bundlePath` is
558
- // only set for a primary sync — a `--bundle <target>` entry's
559
- // descriptor reflects the invoking process's OWN primary directory,
560
- // not the targeted bundle's, so it is not a meaningful signal there;
561
- // that case keeps relying on config-name uniqueness below.)
562
- return entry.ownerBundlePath !== undefined && entry.ownerBundlePath === input.bundlePath;
180
+ /** Directory entries, or none when the directory does not exist. Any other read error is the bundle's own failure. */
181
+ function listDirectory(dir) {
182
+ try {
183
+ return fs.readdirSync(dir);
184
+ }
185
+ catch (error) {
186
+ if (error.code === "ENOENT")
187
+ return [];
188
+ throw error;
563
189
  }
564
- if (entry.target === input.bundleName || entry.target === input.bundleTarget)
565
- return true;
566
- return false;
567
- }
568
- function assertNoForeignIds(desired, input) {
569
- const wanted = new Set(desired.map(({ id }) => id));
570
- const foreign = input.installed.find((entry) => wanted.has(entry.id) && !belongsToBundle(entry, input));
571
- if (!foreign)
572
- return;
573
- const where = foreign.ownerBundlePath
574
- ? `the bundle at ${JSON.stringify(foreign.ownerBundlePath)}`
575
- : foreign.target
576
- ? `bundle ${JSON.stringify(foreign.target)}`
577
- : "the default bundle";
578
- const mine = input.bundlePath ? ` (this sync is scoped to ${JSON.stringify(input.bundlePath)})` : "";
579
- throw new UsageError(`Scheduler id ${JSON.stringify(foreign.id)} is already scheduled from ${where}${mine}; desired source ids must not collide across bundles.`, "RESOURCE_ALREADY_EXISTS");
580
190
  }
581
- function assertUniqueDesiredIds(desired) {
582
- const seen = new Set();
583
- for (const binding of desired) {
584
- if (seen.has(binding.id)) {
585
- throw new UsageError(`Desired scheduler id collision for ${JSON.stringify(binding.id)}; no native definitions were changed.`, "RESOURCE_ALREADY_EXISTS");
191
+ /** Every regular file under `dir` (symlinks followed), sorted. */
192
+ function walkFiles(dir) {
193
+ const files = [];
194
+ const visit = (directory) => {
195
+ for (const name of listDirectory(directory)) {
196
+ const candidate = path.join(directory, name);
197
+ const stat = statOrUndefined(candidate);
198
+ if (stat?.isDirectory())
199
+ visit(candidate);
200
+ else if (stat?.isFile())
201
+ files.push(candidate);
586
202
  }
587
- seen.add(binding.id);
203
+ };
204
+ visit(dir);
205
+ return files.sort(compareCodePoints);
206
+ }
207
+ /** A dangling or looping symlink is not a source file; it is skipped, never fatal. */
208
+ function statOrUndefined(file) {
209
+ try {
210
+ return fs.statSync(file);
211
+ }
212
+ catch {
213
+ return undefined;
588
214
  }
589
215
  }
590
- function assertUniqueInstalledIds(installed) {
591
- const seen = new Set();
592
- for (const binding of installed) {
593
- if (seen.has(binding.id)) {
594
- throw new UsageError(`Installed scheduler id collision for ${JSON.stringify(binding.id)}; refusing whole-set reconciliation.`, "RESOURCE_ALREADY_EXISTS");
216
+ /**
217
+ * Validate every v4 `schedule:` entry's `inputs` against the task's own
218
+ * declared contract with defaults applied — the exact set of values a
219
+ * compiled invocation delivers — so a violation is reported at sync rather
220
+ * than when the scheduler fires. Shared with `akm task validate`.
221
+ */
222
+ export function assertTaskScheduleInputsSatisfyContract(v4, refLabel) {
223
+ const contract = v4.inputs ?? {};
224
+ for (const scheduleEntry of v4.schedule) {
225
+ const defaultedInputs = applyInputDefaults(contract, { ...scheduleEntry.inputs });
226
+ const errors = validateInputs(contract, defaultedInputs);
227
+ if (errors.length > 0) {
228
+ throw new UsageError(`Task ${JSON.stringify(refLabel)} schedule[${scheduleEntry.ordinal}].inputs does not satisfy ` +
229
+ `its declared inputs once defaults are applied: ${errors.join("; ")}`, "TASK_SOURCE_INVALID");
595
230
  }
596
- seen.add(binding.id);
597
231
  }
598
232
  }
599
- function taskFailure(file, ref, cause) {
600
- const detail = taskSourceErrorDetail(cause);
601
- const reason = detail === errorMessage(cause) ? `${file}: ${detail}` : detail;
602
- return Object.freeze({ path: file, ref, reason });
603
- }
604
- function workflowFailure(file, ref, cause) {
605
- return Object.freeze({ path: file, ...(ref ? { ref } : {}), reason: errorMessage(cause) });
606
- }
607
- function errorMessage(cause) {
608
- return cause instanceof Error ? cause.message : String(cause);
233
+ /**
234
+ * Validate every v4 `schedule:` entry's `cron` against the active backend's
235
+ * dialect: cron is the most permissive of the three, so a task authored on
236
+ * Linux can carry an expression launchd/schtasks cannot translate. Shared
237
+ * with `akm task validate`.
238
+ */
239
+ export function assertTaskScheduleCronValid(v4, backend) {
240
+ for (const scheduleEntry of v4.schedule)
241
+ parseSchedule(scheduleEntry.cron, backend);
609
242
  }
610
- export function assertSchedulerSourceSnapshot(snapshot) {
611
- try {
612
- for (const source of snapshot.files) {
613
- const current = captureGuardedExecutionSource(source.sourcePath, source.containmentRoot, {
614
- authored: source.authored,
615
- ...(source.identity ? { identity: source.identity } : {}),
616
- });
617
- if (JSON.stringify(current) !== JSON.stringify(source)) {
618
- throw new Error(`source identity changed: ${source.sourcePath}`);
619
- }
243
+ export function installedRowNativeId(row) {
244
+ return row.nativeId ?? schedulerNativeBindingId(row.id);
245
+ }
246
+ /** The bundle an installed row's own invocation names: `--bundle <x>`, or a workflow ref's bundle. */
247
+ export function scheduledInvocationBundle(invocation) {
248
+ if (!invocation)
249
+ return undefined;
250
+ const bundleIndex = invocation.indexOf("--bundle");
251
+ if (bundleIndex >= 0)
252
+ return invocation[bundleIndex + 1];
253
+ if (invocation[0] === "workflow" && invocation[1] === "run" && invocation[2]) {
254
+ try {
255
+ return parseBundleRef(invocation[2]).bundle;
620
256
  }
621
- for (const manifest of snapshot.directoryManifests) {
622
- const current = captureGuardedDirectoryManifest(manifest.directoryPath, manifest.containmentRoot);
623
- if (JSON.stringify(current) !== JSON.stringify(manifest)) {
624
- throw new Error(`directory manifest changed: ${manifest.directoryPath}`);
625
- }
257
+ catch {
258
+ return undefined;
626
259
  }
627
260
  }
628
- catch (cause) {
629
- throw new UsageError(`Scheduler desired source read set changed after projection; refusing native mutation: ${errorMessage(cause)}`, "RESOURCE_ALREADY_EXISTS");
630
- }
261
+ return undefined;
631
262
  }
632
- class SchedulerSourceCollector {
633
- #adapterId;
634
- #sourceRoot;
635
- #collector = new GuardedExecutionSourceCollector();
636
- constructor(input) {
637
- this.#adapterId = input.adapterId;
638
- this.#sourceRoot = path.resolve(input.sourceRoot);
639
- const root = this.#collector.trackDirectory(this.#sourceRoot, this.#sourceRoot);
640
- const rootDirectories = new Set(root.entries.filter((entry) => entry.kind === "directory").map((entry) => entry.name));
641
- const candidates = [];
642
- if (input.adapterId === "akm") {
643
- if (rootDirectories.has("tasks")) {
644
- const taskRoot = path.join(this.#sourceRoot, "tasks");
645
- const taskManifest = this.#collector.trackDirectory(taskRoot, this.#sourceRoot);
646
- for (const entry of taskManifest.entries) {
647
- if (entry.kind === "file" && entry.name.endsWith(".yml")) {
648
- candidates.push(path.join(taskRoot, entry.name));
649
- }
650
- }
651
- }
652
- if (rootDirectories.has("workflows")) {
653
- candidates.push(...this.#collector.enumerateTree(path.join(this.#sourceRoot, "workflows"), this.#sourceRoot));
654
- }
655
- }
656
- else if (input.adapterId === "akm-task") {
657
- candidates.push(...this.#collector.enumerateTree(this.#sourceRoot, this.#sourceRoot).filter((file) => file.endsWith(".yml")));
658
- }
659
- else if (input.adapterId === "akm-workflow") {
660
- candidates.push(...this.#collector.enumerateTree(this.#sourceRoot, this.#sourceRoot));
263
+ /**
264
+ * The scope that owns an installed row, or none. A row written before
265
+ * `--bundle` existed names no bundle and belongs to the primary scope
266
+ * unless its descriptor says otherwise; a row that names a bundle belongs
267
+ * to the scope of that name — for the primary scope only when its
268
+ * descriptor also proves the path.
269
+ */
270
+ export function installedRowScope(row, scopes) {
271
+ const bundle = row.target ?? scheduledInvocationBundle(row.invocation);
272
+ for (const scope of scopes) {
273
+ if (scope.bundlePath === undefined) {
274
+ if (bundle === scope.bundleName)
275
+ return scope;
276
+ continue;
661
277
  }
662
- for (const file of candidates.sort(compareCodePoints)) {
663
- this.#collector.capture(file, this.#sourceRoot, { authored: true });
278
+ if (bundle === undefined) {
279
+ if (row.ownerBundlePath === undefined || row.ownerBundlePath === scope.bundlePath)
280
+ return scope;
281
+ continue;
664
282
  }
283
+ if (bundle === scope.bundleName && row.ownerBundlePath === scope.bundlePath)
284
+ return scope;
665
285
  }
666
- executionCollector() {
667
- return this.#collector;
286
+ return undefined;
287
+ }
288
+ export function planSchedulerSync(input) {
289
+ const failures = [];
290
+ const installedByNative = new Map();
291
+ for (const row of input.installed)
292
+ installedByNative.set(installedRowNativeId(row), row);
293
+ // Two sources claiming one native row — compared case-insensitively, as
294
+ // Task Scheduler and a default macOS volume compare them: neither is
295
+ // installed and the row, if any, is left exactly as it is.
296
+ const desiredByNative = new Map();
297
+ for (const binding of input.desired) {
298
+ const key = schedulerBindingNativeId(binding).toLowerCase();
299
+ desiredByNative.set(key, [...(desiredByNative.get(key) ?? []), binding]);
668
300
  }
669
- authoredTaskSources(adapterId) {
670
- return this.#collector
671
- .snapshot()
672
- .sources.filter((file) => {
673
- if (!file.authored || !file.relativePath.endsWith(".yml"))
674
- return false;
675
- if (adapterId === "akm-task")
676
- return true;
677
- return path.posix.dirname(file.relativePath) === "tasks";
678
- })
679
- .sort(compareGuardedSources);
301
+ const skipped = new Set();
302
+ for (const group of desiredByNative.values()) {
303
+ if (group.length < 2)
304
+ continue;
305
+ const refs = group.map((binding) => JSON.stringify(binding.logicalSource.ref)).join(" and ");
306
+ for (const binding of group) {
307
+ const nativeId = schedulerBindingNativeId(binding);
308
+ skipped.add(nativeId);
309
+ failures.push({
310
+ path: binding.source,
311
+ ref: binding.logicalSource.ref,
312
+ reason: `Scheduler id ${JSON.stringify(nativeId)} is claimed by ${refs}. Rename one task before enabling both.`,
313
+ });
314
+ }
680
315
  }
681
- authoredWorkflowSources(adapterId) {
682
- return this.#collector
683
- .snapshot()
684
- .sources.filter((file) => {
685
- if (!file.authored)
686
- return false;
687
- if (adapterId === "akm-workflow")
688
- return true;
689
- return file.relativePath.startsWith("workflows/");
690
- })
691
- .sort(compareGuardedSources);
316
+ const installed = [];
317
+ const updated = [];
318
+ const unchanged = [];
319
+ const writes = [];
320
+ for (const binding of input.desired) {
321
+ const nativeId = schedulerBindingNativeId(binding);
322
+ if (skipped.has(nativeId))
323
+ continue;
324
+ const current = installedByNative.get(nativeId);
325
+ if (current &&
326
+ installedRowScope(current, input.scopes)?.bundleName !== parseBundleRef(binding.logicalSource.ref).bundle) {
327
+ skipped.add(nativeId);
328
+ failures.push({
329
+ path: binding.source,
330
+ ref: binding.logicalSource.ref,
331
+ reason: foreignRowMessage(nativeId, current),
332
+ });
333
+ continue;
334
+ }
335
+ const options = installOptionsFor(input, current);
336
+ const expected = input.expectedSignature?.(binding, options);
337
+ if (!current) {
338
+ installed.push(binding.id);
339
+ writes.push(Object.freeze({
340
+ kind: "install",
341
+ binding,
342
+ ...(options ? { options } : {}),
343
+ ...(expected !== undefined ? { expectedSignature: expected } : {}),
344
+ }));
345
+ continue;
346
+ }
347
+ if (expected === undefined || current.signature === expected) {
348
+ unchanged.push(binding.id);
349
+ continue;
350
+ }
351
+ updated.push(binding.id);
352
+ writes.push(Object.freeze({
353
+ kind: "update",
354
+ binding,
355
+ ...(options ? { options } : {}),
356
+ ...(current.signature !== undefined ? { installedSignature: current.signature } : {}),
357
+ expectedSignature: expected,
358
+ }));
692
359
  }
693
- readBytes(file, containmentRoot) {
694
- this.#trackAncestors(file, containmentRoot);
695
- return this.#collector.readBytes(file, containmentRoot);
360
+ const desiredNative = new Set(input.desired.map(schedulerBindingNativeId));
361
+ const removals = new Map();
362
+ for (const row of input.installed) {
363
+ const nativeId = installedRowNativeId(row);
364
+ if (desiredNative.has(nativeId) || skipped.has(nativeId))
365
+ continue;
366
+ const scope = installedRowScope(row, input.scopes);
367
+ if (!scope)
368
+ continue;
369
+ const ref = installedRowRef(row, scope);
370
+ if (ref === undefined || !input.keepRefs?.has(ref))
371
+ removals.set(nativeId, row);
696
372
  }
697
- fileContext(root, file) {
698
- this.#trackAncestors(file, root);
699
- return this.#collector.fileContext(root, file);
373
+ for (const row of input.extraRemovals ?? []) {
374
+ const nativeId = installedRowNativeId(row);
375
+ if (!desiredNative.has(nativeId) && !removals.has(nativeId))
376
+ removals.set(nativeId, row);
700
377
  }
701
- snapshot() {
702
- const guarded = this.#collector.snapshot();
703
- const root = guarded.directoryManifests.find((manifest) => manifest.directoryPath === this.#sourceRoot && manifest.containmentRoot === this.#sourceRoot);
704
- if (!root)
705
- throw new Error("scheduler source collector lost its guarded root manifest");
378
+ const removeOperations = [...removals.values()]
379
+ .sort((left, right) => compareCodePoints(left.id, right.id))
380
+ .map((row) => Object.freeze({
381
+ kind: "remove",
382
+ id: row.id,
383
+ nativeId: installedRowNativeId(row),
384
+ ...(row.ownerBundlePath !== undefined ? { ownerBundlePath: row.ownerBundlePath } : {}),
385
+ }));
386
+ return Object.freeze({
387
+ desired: input.desired,
388
+ installed: Object.freeze(installed),
389
+ updated: Object.freeze(updated),
390
+ removed: Object.freeze(removeOperations.map((operation) => operation.id)),
391
+ unchanged: Object.freeze(unchanged),
392
+ operations: Object.freeze([...removeOperations, ...writes]),
393
+ failures: Object.freeze(failures),
394
+ });
395
+ }
396
+ /** An installed row keeps its own launcher unless `rebind`; the descriptor always follows the current policy. */
397
+ function installOptionsFor(input, current) {
398
+ if (current && !input.rebind) {
706
399
  return Object.freeze({
707
- adapterId: this.#adapterId,
708
- sourceRoot: this.#sourceRoot,
709
- sourceRealPath: root.realPath,
710
- sourcePhysicalIdentity: root.physicalIdentity,
711
- sourceDirectoryVersion: root.version,
712
- files: Object.freeze([...guarded.sources].sort(compareGuardedSources)),
713
- directoryManifests: guarded.directoryManifests,
400
+ binding: Object.freeze([...current.binding]),
401
+ contextPath: input.installOptions?.contextPath ?? current.contextPath,
714
402
  });
715
403
  }
716
- #trackAncestors(file, containmentRoot) {
717
- const root = path.resolve(containmentRoot);
718
- const parent = path.dirname(path.resolve(file));
719
- const relative = path.relative(root, parent);
720
- if (relative.startsWith("..") || path.isAbsolute(relative))
721
- return;
722
- this.#collector.trackDirectory(root, root);
723
- if (relative === "")
724
- return;
725
- let current = root;
726
- for (const segment of relative.split(path.sep)) {
727
- current = path.join(current, segment);
728
- this.#collector.trackDirectory(current, root);
729
- }
730
- }
731
- }
732
- function compareGuardedSources(left, right) {
733
- return compareCodePoints(left.sourcePath, right.sourcePath);
404
+ return input.installOptions ? Object.freeze({ ...input.installOptions }) : undefined;
734
405
  }
735
- function installedLogicalSource(invocation, input) {
736
- if (invocation[0] === "workflow" && invocation[1] === "run" && invocation.length === 3) {
737
- return Object.freeze({ kind: "workflow", ref: invocation[2] });
738
- }
739
- if (invocation[0] === "task" && invocation[1] === "run" && invocation[2]) {
740
- const bundleIndex = invocation.indexOf("--bundle", 3);
741
- const bundle = bundleIndex === -1 ? input.bundleName : invocation[bundleIndex + 1];
742
- if (!bundle) {
743
- throw new UsageError("Installed task invocation has no resolvable bundle owner.", "RESOURCE_ALREADY_EXISTS");
406
+ /** The source ref an installed row runs, read from its own invocation. */
407
+ function installedRowRef(row, scope) {
408
+ const [verb, action, subject] = row.invocation ?? [];
409
+ if (action !== "run" || !subject)
410
+ return undefined;
411
+ if (verb === "workflow")
412
+ return subject;
413
+ if (verb !== "task")
414
+ return undefined;
415
+ return `${scope.bundleName}//${scope.adapterId === "akm-task" ? subject : `tasks/${subject}`}`;
416
+ }
417
+ /** Where an installed row was scheduled from, for messages. */
418
+ export function installedRowOwner(row) {
419
+ const bundle = row.target ?? scheduledInvocationBundle(row.invocation);
420
+ if (row.ownerBundlePath)
421
+ return `the bundle at ${JSON.stringify(row.ownerBundlePath)}`;
422
+ return bundle ? `bundle ${JSON.stringify(bundle)}` : "an installation this sync cannot attribute";
423
+ }
424
+ function foreignRowMessage(nativeId, row) {
425
+ return `Scheduler id ${JSON.stringify(nativeId)} is already scheduled from ${installedRowOwner(row)}; desired source ids must not collide across bundles. Leaving it out of this sync.`;
426
+ }
427
+ /** Project a set of operations into a preview report. Pure: no I/O, no backend calls. */
428
+ export function renderSchedulerPlanPreview(backend, operations, unchanged = [], failures = []) {
429
+ const adds = [];
430
+ const updates = [];
431
+ const removes = [];
432
+ for (const operation of operations) {
433
+ if (operation.kind === "remove") {
434
+ removes.push({
435
+ id: operation.id,
436
+ kind: "remove",
437
+ nativeId: operation.nativeId,
438
+ ...(operation.ownerBundlePath !== undefined ? { ownerBundlePath: operation.ownerBundlePath } : {}),
439
+ ...(operation.reason !== undefined ? { reason: operation.reason } : {}),
440
+ });
441
+ }
442
+ else if (operation.kind === "install") {
443
+ adds.push({ id: operation.binding.id, kind: "install" });
444
+ }
445
+ else {
446
+ updates.push({
447
+ id: operation.binding.id,
448
+ kind: "update",
449
+ ...(operation.installedSignature !== undefined ? { installedFingerprint: operation.installedSignature } : {}),
450
+ ...(operation.expectedSignature !== undefined ? { expectedFingerprint: operation.expectedSignature } : {}),
451
+ });
744
452
  }
745
- const conceptId = input.adapterId === "akm" ? `tasks/${invocation[2]}` : invocation[2];
746
- return Object.freeze({ kind: "task", ref: makeBundleRef(bundle, conceptId) });
747
453
  }
748
- throw new UsageError("Installed scheduler invocation has no exact canonical source owner.", "RESOURCE_ALREADY_EXISTS");
749
- }
750
- function freezeRemovalExpectation(expectation) {
751
- return Object.freeze({
752
- ...expectation,
753
- logicalSource: Object.freeze({ ...expectation.logicalSource }),
754
- invocation: Object.freeze([...expectation.invocation]),
755
- });
756
- }
757
- function expectationForBinding(binding, state, fingerprint) {
758
454
  return {
759
- state,
760
- bindingId: binding.id,
761
- nativeId: schedulerBindingNativeId(binding),
762
- logicalSource: binding.logicalSource,
763
- ordinal: binding.ordinal,
764
- invocation: binding.invocation,
765
- ...(fingerprint !== undefined ? { fingerprint } : {}),
455
+ backend,
456
+ dryRun: true,
457
+ adds,
458
+ updates,
459
+ removes,
460
+ unchanged: [...unchanged],
461
+ hasRemovals: removes.length > 0,
462
+ failures: [...failures],
766
463
  };
767
464
  }
768
- function freezeMutationExpectation(expectation) {
769
- return Object.freeze({
770
- ...expectation,
771
- logicalSource: Object.freeze({ ...expectation.logicalSource }),
772
- invocation: Object.freeze([...expectation.invocation]),
773
- });
774
- }
775
- function freezeOperation(operation) {
776
- return Object.freeze(operation);
777
- }
778
- function sameInvocation(left, right) {
779
- return left.length === right.length && left.every((value, index) => value === right[index]);
465
+ function errorMessage(cause) {
466
+ return cause instanceof Error ? cause.message : String(cause);
780
467
  }
781
- // Retain the concrete error in this module's public dependency graph so callers
782
- // can continue to identify ownership failures without importing an adapter.
783
- export { WorkflowSourceRejectionError };