akm-cli 0.9.17-alpha.3 → 0.9.17-alpha.5

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 (346) hide show
  1. package/CHANGELOG.md +760 -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/lint/base-linter.js +19 -5
  53. package/dist/commands/proposal/drain.js +251 -644
  54. package/dist/commands/proposal/proposal-cli.js +3 -18
  55. package/dist/commands/proposal/proposal-types.js +20 -41
  56. package/dist/commands/proposal/proposal.js +1 -2
  57. package/dist/commands/proposal/propose.js +134 -160
  58. package/dist/commands/proposal/repository.js +502 -1487
  59. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  60. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  61. package/dist/commands/proposal/validators/proposals.js +13 -89
  62. package/dist/commands/read/curate.js +63 -413
  63. package/dist/commands/read/search-cli.js +16 -33
  64. package/dist/commands/read/search.js +17 -23
  65. package/dist/commands/read/show.js +2 -13
  66. package/dist/commands/sources/bundle-cli.js +25 -2
  67. package/dist/commands/sources/bundle-config-ops.js +4 -0
  68. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  69. package/dist/commands/sources/info.js +2 -11
  70. package/dist/commands/sources/installed-stashes.js +197 -746
  71. package/dist/commands/sources/schema-repair.js +98 -129
  72. package/dist/commands/sources/source-add.js +62 -12
  73. package/dist/commands/sources/source-manage.js +9 -2
  74. package/dist/commands/sources/stash-cli.js +1 -1
  75. package/dist/commands/tasks/explain.js +10 -13
  76. package/dist/commands/tasks/tasks-cli.js +9 -8
  77. package/dist/commands/tasks/tasks.js +326 -930
  78. package/dist/commands/tasks/validate.js +42 -21
  79. package/dist/commands/workflow/plan.js +22 -29
  80. package/dist/commands/workflow-cli.js +4 -4
  81. package/dist/core/adapter/adapters/akm-adapter.js +0 -1
  82. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  83. package/dist/core/adapter/adapters/akm-metadata.js +11 -12
  84. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  85. package/dist/core/adapter/execution-source.js +17 -29
  86. package/dist/core/asset/asset-placement.js +4 -13
  87. package/dist/core/asset/resolve-ref.js +1 -1
  88. package/dist/core/bundle-id.js +42 -5
  89. package/dist/core/bundle-rename.js +291 -0
  90. package/dist/core/config/config-io.js +1 -2
  91. package/dist/core/config/config-schema.js +1 -33
  92. package/dist/core/config/config-walker.js +1 -1
  93. package/dist/core/config/config.js +163 -68
  94. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  95. package/dist/core/config/schema/embedding.js +20 -5
  96. package/dist/core/config/schema/engines.js +5 -0
  97. package/dist/core/config/schema/execution.js +1 -1
  98. package/dist/core/config/schema/experimental.js +1 -1
  99. package/dist/core/config/schema/improve-processes.js +21 -95
  100. package/dist/core/config/schema/improve.js +4 -42
  101. package/dist/core/config/schema/scheduler.js +12 -12
  102. package/dist/core/config/schema/search.js +6 -22
  103. package/dist/core/env-secret-ref.js +0 -1
  104. package/dist/core/errors.js +8 -9
  105. package/dist/core/file-lock.js +76 -173
  106. package/dist/core/logs-db.js +2 -2
  107. package/dist/core/paths.js +0 -27
  108. package/dist/core/redaction.js +109 -2
  109. package/dist/core/run-lock.js +2 -5
  110. package/dist/core/spawn-env.js +1 -1
  111. package/dist/core/state/migrations.js +108 -61
  112. package/dist/core/state-db-scope.js +2 -4
  113. package/dist/core/state-db.js +126 -692
  114. package/dist/core/type-presentation.js +1 -9
  115. package/dist/core/write-source.js +293 -1012
  116. package/dist/execution/input-contract.js +1 -1
  117. package/dist/execution/resolved-request.js +135 -689
  118. package/dist/execution/source.js +63 -257
  119. package/dist/execution/target-ref.js +1 -1
  120. package/dist/indexer/bundle-identity-guard.js +2 -2
  121. package/dist/indexer/db/graph-db.js +106 -46
  122. package/dist/indexer/ensure-index.js +44 -85
  123. package/dist/indexer/graph/graph-extraction.js +340 -562
  124. package/dist/indexer/graph/graph-related.js +130 -0
  125. package/dist/indexer/index-rebuild-lock.js +3 -11
  126. package/dist/indexer/index-writer-lock.js +8 -17
  127. package/dist/indexer/index-written-assets.js +139 -151
  128. package/dist/indexer/indexer.js +524 -846
  129. package/dist/indexer/materialize-embeddings.js +60 -397
  130. package/dist/indexer/passes/memory-inference.js +81 -90
  131. package/dist/indexer/passes/metadata.js +132 -200
  132. package/dist/indexer/read-preflight.js +0 -7
  133. package/dist/indexer/scan/doc-to-entry.js +1 -3
  134. package/dist/indexer/scan/drain-dir.js +1 -1
  135. package/dist/indexer/search/db-search.js +181 -590
  136. package/dist/indexer/search/fts-query.js +30 -41
  137. package/dist/indexer/search/ranking.js +28 -154
  138. package/dist/indexer/search/search-attribution.js +12 -32
  139. package/dist/indexer/search/search-fields.js +11 -15
  140. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  141. package/dist/indexer/search/search-source.js +1 -4
  142. package/dist/indexer/usage/usage-events.js +2 -7
  143. package/dist/integrations/agent/engine-fallback.js +23 -40
  144. package/dist/integrations/agent/engine-resolution.js +93 -183
  145. package/dist/integrations/agent/execution.js +507 -0
  146. package/dist/integrations/agent/model-map.js +28 -156
  147. package/dist/integrations/agent/request-lowering.js +66 -141
  148. package/dist/integrations/agent/runner-dispatch.js +143 -321
  149. package/dist/integrations/agent/runner.js +54 -14
  150. package/dist/integrations/lockfile.js +53 -101
  151. package/dist/llm/embedders/deterministic.js +2 -3
  152. package/dist/llm/embedders/profile.js +71 -0
  153. package/dist/llm/embedders/remote.js +10 -15
  154. package/dist/llm/graph-extract.js +3 -12
  155. package/dist/llm/index-passes.js +3 -5
  156. package/dist/llm/memory-infer.js +1 -2
  157. package/dist/llm/metadata-enhance.js +1 -2
  158. package/dist/llm/structured-call.js +5 -24
  159. package/dist/output/generic-render.js +23 -11
  160. package/dist/output/html-render.js +13 -10
  161. package/dist/output/render-registry.js +3 -32
  162. package/dist/output/shapes/helpers.js +2 -34
  163. package/dist/output/shapes/passthrough.js +1 -9
  164. package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
  165. package/dist/output/text/command-format.js +60 -23
  166. package/dist/output/text/helpers.js +1 -1
  167. package/dist/output/text/migrate.js +5 -14
  168. package/dist/output/text/proposal-format.js +1 -2
  169. package/dist/output/text/workflow-format.js +0 -32
  170. package/dist/output/text.js +2 -0
  171. package/dist/registry/factory.js +4 -19
  172. package/dist/registry/network.js +66 -220
  173. package/dist/registry/providers/index.js +0 -2
  174. package/dist/registry/providers/skills-sh.js +3 -14
  175. package/dist/registry/providers/static-index.js +24 -26
  176. package/dist/registry/resolve.js +55 -131
  177. package/dist/scripts/akm-migrate-node.js +43940 -93320
  178. package/dist/scripts/akm-migrate.js +43700 -93078
  179. package/dist/setup/registry-stash-loader.js +4 -13
  180. package/dist/setup/semantic-assets.js +3 -44
  181. package/dist/setup/setup.js +1 -1
  182. package/dist/setup/steps/tasks.js +25 -15
  183. package/dist/sources/provider-factory.js +17 -18
  184. package/dist/sources/providers/filesystem.js +2 -3
  185. package/dist/sources/providers/git-install.js +7 -1
  186. package/dist/sources/providers/git-provider.js +0 -3
  187. package/dist/sources/providers/git-stash.js +0 -17
  188. package/dist/sources/providers/npm.js +2 -4
  189. package/dist/sources/providers/provider-utils.js +5 -10
  190. package/dist/sources/providers/website.js +0 -2
  191. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  192. package/dist/sources/website-url.js +2 -2
  193. package/dist/storage/database.js +9 -35
  194. package/dist/storage/repositories/improve-ledger-repository.js +168 -0
  195. package/dist/storage/repositories/index-connection.js +34 -70
  196. package/dist/storage/repositories/index-entries-repository.js +69 -111
  197. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  198. package/dist/storage/repositories/index-entry-schema.js +83 -269
  199. package/dist/storage/repositories/index-fts-repository.js +86 -256
  200. package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
  201. package/dist/storage/repositories/index-meta-repository.js +6 -4
  202. package/dist/storage/repositories/index-schema.js +192 -220
  203. package/dist/storage/repositories/index-utility-repository.js +8 -29
  204. package/dist/storage/repositories/index-vec-repository.js +133 -414
  205. package/dist/storage/repositories/outcome-repository.js +2 -1
  206. package/dist/storage/repositories/proposals-repository.js +35 -0
  207. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  208. package/dist/storage/repositories/task-history-repository.js +26 -4
  209. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  210. package/dist/storage/sqlite-migrations.js +136 -0
  211. package/dist/storage/sqlite-pragmas.js +11 -9
  212. package/dist/storage/sqlite-transaction.js +170 -0
  213. package/dist/storage/state-db-integrity.js +34 -27
  214. package/dist/tasks/activation-config.js +134 -62
  215. package/dist/tasks/backends/cron.js +129 -277
  216. package/dist/tasks/backends/exec-utils.js +2 -5
  217. package/dist/tasks/backends/launchd.js +125 -745
  218. package/dist/tasks/backends/schtasks.js +101 -620
  219. package/dist/tasks/prepare/prepare-support.js +5 -15
  220. package/dist/tasks/prepare/prepare.js +0 -2
  221. package/dist/tasks/resolve-akm-bin.js +20 -79
  222. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  223. package/dist/tasks/scheduler-binding.js +18 -238
  224. package/dist/tasks/scheduler-invocation.js +52 -52
  225. package/dist/tasks/scheduler-lock.js +53 -0
  226. package/dist/tasks/scheduler-sync.js +361 -751
  227. package/dist/tasks/source/parse-task-source.js +160 -10
  228. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  229. package/dist/tasks/source/task-to-v4.js +2 -2
  230. package/dist/workflows/authoring/authoring.js +3 -12
  231. package/dist/workflows/compile.js +211 -0
  232. package/dist/workflows/concurrency-policy.js +13 -74
  233. package/dist/workflows/exec/child-invocation.js +3 -17
  234. package/dist/workflows/exec/child-workflow.js +32 -141
  235. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  236. package/dist/workflows/exec/environment.js +98 -0
  237. package/dist/workflows/exec/exec-unit.js +33 -140
  238. package/dist/workflows/exec/frozen-judge.js +7 -59
  239. package/dist/workflows/exec/native-executor.js +82 -341
  240. package/dist/workflows/exec/param-secrets.js +29 -47
  241. package/dist/workflows/exec/run-workflow.js +154 -387
  242. package/dist/workflows/exec/scheduler.js +9 -36
  243. package/dist/workflows/exec/step-work.js +127 -430
  244. package/dist/workflows/exec/unit-dispatch.js +11 -63
  245. package/dist/workflows/exec/unit-writer.js +8 -52
  246. package/dist/workflows/exec/worktree.js +39 -273
  247. package/dist/workflows/freeze/child-output-references.js +4 -15
  248. package/dist/workflows/freeze/environment.js +99 -92
  249. package/dist/workflows/freeze/freeze.js +172 -0
  250. package/dist/workflows/freeze/step-values.js +19 -21
  251. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  252. package/dist/workflows/freeze/targets/command.js +10 -33
  253. package/dist/workflows/freeze/targets/script.js +5 -12
  254. package/dist/workflows/freeze/targets/shell.js +3 -6
  255. package/dist/workflows/freeze/targets/task.js +25 -80
  256. package/dist/workflows/freeze/task-bindings.js +20 -67
  257. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  258. package/dist/workflows/ir/params.js +6 -51
  259. package/dist/workflows/ir/plan-hash.js +2 -34
  260. package/dist/workflows/parser.js +140 -43
  261. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  262. package/dist/workflows/renderer.js +36 -69
  263. package/dist/workflows/resource-limits.js +12 -120
  264. package/dist/workflows/runtime/agent-identity.js +8 -40
  265. package/dist/workflows/runtime/run-outputs.js +3 -6
  266. package/dist/workflows/runtime/run-plan.js +316 -0
  267. package/dist/workflows/runtime/runs.js +48 -200
  268. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  269. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  270. package/dist/workflows/validate-summary.js +2 -7
  271. package/docs/integration/bundling-akm.md +49 -42
  272. package/docs/migration/README.md +1 -0
  273. package/docs/migration/release-notes/0.9.17.md +41 -0
  274. package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
  275. package/docs/reference/cli.md +182 -125
  276. package/docs/reference/configuration.md +49 -56
  277. package/docs/reference/data-and-telemetry.md +19 -20
  278. package/docs/reference/tasks.md +86 -38
  279. package/docs/reference/workflow-schema.md +14 -18
  280. package/docs/reference/workflows.md +6 -9
  281. package/package.json +1 -1
  282. package/schemas/akm-config.json +87 -406
  283. package/dist/commands/health/advisories.js +0 -150
  284. package/dist/commands/health/metrics.js +0 -329
  285. package/dist/commands/health/surfaces.js +0 -102
  286. package/dist/commands/improve/anti-collapse.js +0 -83
  287. package/dist/commands/improve/collapse-detector.js +0 -432
  288. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  289. package/dist/commands/improve/consolidate/merge.js +0 -146
  290. package/dist/commands/improve/distill/promote-memory.js +0 -329
  291. package/dist/commands/improve/distill/quality-gate.js +0 -500
  292. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  293. package/dist/commands/improve/proposal-envelope.js +0 -31
  294. package/dist/commands/improve/run-context.js +0 -123
  295. package/dist/commands/improve/shared.js +0 -21
  296. package/dist/commands/improve/source-identity.js +0 -28
  297. package/dist/commands/improve/triage.js +0 -96
  298. package/dist/commands/proposal/drain-policies.js +0 -151
  299. package/dist/commands/sources/update-transaction.js +0 -220
  300. package/dist/core/action-contributors.js +0 -28
  301. package/dist/core/config/config-version-shim.js +0 -101
  302. package/dist/core/config/retired-experimental-keys-shim.js +0 -62
  303. package/dist/core/fs-txn.js +0 -405
  304. package/dist/core/lexical-score.js +0 -25
  305. package/dist/core/maintenance-barrier.js +0 -167
  306. package/dist/execution/executable-identity.js +0 -105
  307. package/dist/execution/guarded-source.js +0 -441
  308. package/dist/indexer/graph/graph-boost.js +0 -427
  309. package/dist/indexer/graph/graph-dedup.js +0 -95
  310. package/dist/indexer/search/name-match.js +0 -35
  311. package/dist/indexer/search/ranking-contributors.js +0 -515
  312. package/dist/indexer/walk/project-context.js +0 -192
  313. package/dist/integrations/agent/execution-cascade.js +0 -566
  314. package/dist/integrations/agent/execution-definitions.js +0 -202
  315. package/dist/integrations/agent/execution-lowering.js +0 -841
  316. package/dist/integrations/agent/execution-preparation.js +0 -98
  317. package/dist/integrations/agent/inline-execution.js +0 -74
  318. package/dist/registry/create-provider-registry.js +0 -29
  319. package/dist/registry/pinned-request-helper.js +0 -247
  320. package/dist/registry/pinned-transport.js +0 -717
  321. package/dist/sources/providers/index.js +0 -14
  322. package/dist/storage/engines/sqlite-migrations.js +0 -271
  323. package/dist/storage/repositories/canaries-repository.js +0 -107
  324. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  325. package/dist/storage/repositories/registry-cache.js +0 -113
  326. package/dist/tasks/scheduler-sync-preview.js +0 -52
  327. package/dist/workflows/freeze/resolve-steps.js +0 -86
  328. package/dist/workflows/freeze/source-freeze.js +0 -64
  329. package/dist/workflows/ir/compile.js +0 -321
  330. package/dist/workflows/ir/environment-v4.js +0 -330
  331. package/dist/workflows/ir/freeze-v4.js +0 -153
  332. package/dist/workflows/ir/schema-v4.js +0 -745
  333. package/dist/workflows/ir/schema.js +0 -354
  334. package/dist/workflows/program/schema.js +0 -78
  335. package/dist/workflows/runtime/checkin.js +0 -57
  336. package/dist/workflows/runtime/plan-classifier.js +0 -196
  337. package/dist/workflows/runtime/unit-checkin.js +0 -45
  338. package/dist/workflows/runtime/unit-phases.js +0 -20
  339. package/dist/workflows/schema.js +0 -4
  340. package/dist/workflows/source-ir/compile.js +0 -200
  341. package/dist/workflows/source-ir/program.js +0 -50
  342. package/dist/workflows/source-ir/result.js +0 -26
  343. package/dist/workflows/source-ir/schema.js +0 -786
  344. package/dist/workflows/source-ir/triggers.js +0 -79
  345. package/dist/workflows/source-ir/uses.js +0 -40
  346. package/dist/workflows/validator.js +0 -60
@@ -1,455 +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
- for (const symlink of collector.symlinkSources()) {
303
- if (!isAuthoredTaskRelativePath(input.adapterId, symlink.relativePath))
304
- continue;
305
- const qualifiedRef = makeBundleRef(input.bundleName, symlink.relativePath.slice(0, -4));
306
- if (input.enabledActivations && !input.enabledActivations.has(schedulerActivationKey("task", qualifiedRef))) {
307
- continue;
308
- }
309
- failures.push(taskFailure(symlink.sourcePath, qualifiedRef, symbolicSourceError(symlink.sourcePath)));
310
- }
311
- const physicalOwners = new Map();
312
- for (const guarded of collector.authoredTaskSources(input.adapterId)) {
313
- const sourcePath = guarded.sourcePath;
314
- const relative = guarded.relativePath;
315
- const conceptId = relative.slice(0, -4);
316
- const id = input.adapterId === "akm-task" ? conceptId : path.basename(sourcePath, ".yml");
317
- const qualifiedRefForFailure = makeBundleRef(input.bundleName, conceptId);
318
- if (input.enabledActivations &&
319
- !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)))
320
53
  continue;
321
- }
322
54
  try {
323
- const physicalIdentity = guarded.physicalIdentity;
324
- const priorOwner = physicalOwners.get(physicalIdentity);
325
- if (priorOwner !== undefined && priorOwner !== sourcePath) {
326
- 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");
327
- }
328
- physicalOwners.set(physicalIdentity, sourcePath);
329
- // Project BEFORE prepareTaskV3Execution so projectability is checked —
330
- // but build the scheduler bindings from the ORIGINAL task source v4
331
- // document, not the projection, which deliberately drops
332
- // `schedule[i].inputs` (project-v4.ts) —
333
- // schedule-supplied inputs are delivered through the scheduler
334
- // binding's own compiled invocation tail (P2b Lane B, spec §4.4,
335
- // B-N3), not through the prepare-seam projection. A task source v4
336
55
  const parsed = parseTaskSource({
337
- yaml: guarded.content,
338
- filePath: sourcePath,
56
+ yaml: fs.readFileSync(file, "utf8"),
57
+ filePath: file,
339
58
  workspaceRoot: input.sourceRoot,
340
59
  });
341
- const document = projectTaskSourceV4(parsed.v4);
342
- const qualifiedRef = makeBundleRef(input.bundleName, conceptId);
343
- // See assertTaskScheduleInputsSatisfyContract's own docblock (below in
344
- // this file) for why this second, defaults-applied gate exists.
345
- assertTaskScheduleInputsSatisfyContract(parsed.v4, qualifiedRef);
346
- 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), {
347
71
  taskId: id,
348
- taskRef: qualifiedRef,
72
+ taskRef: ref,
349
73
  bundleName: input.bundleName,
350
74
  bundleRoot: input.sourceRoot,
351
75
  config: input.config ?? SCHEDULER_PROJECTION_CONFIG,
352
76
  ...(input.resolveAsset ? { resolveAsset: input.resolveAsset } : {}),
353
- readFile: (file, bundleRoot) => collector.readBytes(file, bundleRoot ?? input.sourceRoot),
354
- commandSourceLoader: (ref, kind, options) => {
355
- const guardedOptions = {
356
- ...options,
357
- fileContext: (root, file) => collector.fileContext(root, file),
358
- };
359
- return kind === "command"
360
- ? loadAdapterExecutionSource(ref, "command", guardedOptions)
361
- : loadAdapterExecutionSource(ref, "persona", guardedOptions);
362
- },
363
77
  });
364
- const relSource = toPosix(path.relative(input.sourceRoot, sourcePath));
365
- const sourceBindings = compileTaskSchedulerBindings({
78
+ desired.push(...compileTaskSchedulerBindings({
366
79
  id,
367
- qualifiedRef,
368
- ...(input.bundleTarget ? { bundleTarget: input.bundleTarget } : {}),
80
+ qualifiedRef: ref,
369
81
  schedules: parsed.v4.schedule.map((schedule) => ({
370
82
  cron: schedule.cron,
371
83
  ordinal: schedule.ordinal,
372
- source: `${relSource}:${schedule.source}`,
373
- // P2b Lane B (spec §4.4, B-N3): delivered through the compiled
374
- // binding's own invocation tail below — the F-B2 flip that closes
375
- // the P2a B-38 "validated but not yet delivered" gap this comment
376
- // used to describe.
84
+ source: `${relative}:${schedule.source}`,
377
85
  inputs: schedule.inputs,
378
86
  })),
379
- });
380
- assertTaskScheduleCronValid(parsed.v4, input.backend);
381
- out.push(...sourceBindings);
87
+ }));
382
88
  }
383
89
  catch (cause) {
384
- failures.push(taskFailure(sourcePath, qualifiedRefForFailure, cause));
385
- }
386
- }
387
- }
388
- /**
389
- * P2b Lane B (spec §4.4, rows B-50/F-B2): validate every v4 `schedule:`
390
- * entry's `inputs` against the task's OWN declared contract WITH DEFAULTS
391
- * APPLIED — the same `applyInputDefaults` + `validateInputs` pair
392
- * `akm task run` uses (`src/tasks/run/load-task.ts`). `parseTaskSource`'s
393
- * own parse-time check (`task-source-v4.ts`'s `parseScheduleEntry`) already
394
- * rejects an unknown/malformed entry against the RAW supplied values; this
395
- * is a deliberate second, independent gate over the DEFAULTED view — the
396
- * exact set of values a compiled invocation actually delivers — so a
397
- * violation fails HERE, recorded as a task failure at sync, rather than
398
- * surfacing for the first time when the scheduler fires the invocation.
399
- *
400
- * Extracted so `akm task validate` can run the IDENTICAL gate
401
- * over a bare file path without a second, potentially-diverging copy of
402
- * this check. `refLabel` is only the text embedded in the thrown message —
403
- * `compileTaskSources` passes its bundle-qualified ref; `akm task validate`,
404
- * which never resolves a bundle for a bare file, passes the file path.
405
- */
406
- export function assertTaskScheduleInputsSatisfyContract(v4, refLabel) {
407
- const contract = v4.inputs ?? {};
408
- for (const scheduleEntry of v4.schedule) {
409
- const defaultedInputs = applyInputDefaults(contract, { ...scheduleEntry.inputs });
410
- const errors = validateInputs(contract, defaultedInputs);
411
- if (errors.length > 0) {
412
- throw new UsageError(`Task ${JSON.stringify(refLabel)} schedule[${scheduleEntry.ordinal}].inputs does not satisfy ` +
413
- `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 });
414
92
  }
415
93
  }
416
94
  }
417
- /**
418
- * Validate every v4 `schedule:` entry's `cron` against the active scheduler
419
- * backend's dialect (`parseSchedule`, `./schedule.ts`) — cron is the most
420
- * permissive of the three backends, so a task authored on Linux can carry a
421
- * cron expression launchd/schtasks cannot translate; `compileTaskSources`
422
- * re-checks against the LOCAL backend on every sync rather than trusting
423
- * whatever backend the file was authored against.
424
- *
425
- * Extracted alongside {@link assertTaskScheduleInputsSatisfyContract}
426
- * so `akm task validate` shares this exact gate instead of a second copy
427
- * that could silently drift from what `akm task sync` actually enforces.
428
- */
429
- export function assertTaskScheduleCronValid(v4, backend) {
430
- for (const scheduleEntry of v4.schedule) {
431
- 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);
432
105
  }
106
+ if (adapterId === "akm-task")
107
+ return walkFiles(sourceRoot).filter((file) => file.endsWith(".yml"));
108
+ return [];
433
109
  }
434
- async function compileWorkflowSources(input, collector, out, evidence, failures) {
110
+ function compileWorkflowSources(input, desired, failures) {
435
111
  if (input.adapterId !== "akm" && input.adapterId !== "akm-workflow")
436
112
  return;
437
- const lookups = enumerateWorkflowLookups(input, collector, failures);
438
- for (const [canonicalName, sources] of lookups) {
439
- const failurePath = sources[0]?.sourcePath ?? canonicalName;
440
- const failureRef = makeBundleRef(input.bundleName, input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName);
441
- 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 });
442
127
  continue;
443
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];
444
138
  try {
445
139
  if (sources.length > 1) {
446
- 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))));
447
141
  }
448
- const guarded = sources[0];
449
- if (!guarded)
450
- continue;
451
- const compiled = compileWorkflowSource(guarded.content, {
452
- path: guarded.relativePath,
142
+ const relative = toPosix(path.relative(input.sourceRoot, file));
143
+ const compiled = compileWorkflowSource(fs.readFileSync(file, "utf8"), {
144
+ path: relative,
453
145
  workspaceRoot: input.sourceRoot,
454
146
  });
455
147
  if (!compiled.ok) {
@@ -457,401 +149,319 @@ async function compileWorkflowSources(input, collector, out, evidence, failures)
457
149
  .map((error) => `${error.path}:${error.line ?? 1} [${error.code}] ${error.message}`)
458
150
  .join("; "), "WORKFLOW_SOURCE_INVALID");
459
151
  }
460
- const planDraft = compileWorkflowPlan(compiled.ir, canonicalName);
152
+ const planDraft = checkWorkflowPlan(compiled.plan);
461
153
  if (!planDraft.ok) {
462
- 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");
463
155
  }
464
- const conceptId = input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName;
465
- const qualifiedRef = makeBundleRef(input.bundleName, conceptId);
466
- const asset = {
467
- ref: qualifiedRef,
468
- path: guarded.sourcePath,
469
- sourcePath: input.sourceRoot,
470
- adapterId: input.adapterId,
471
- title: canonicalName,
472
- steps: [],
473
- sourceIr: compiled.ir,
474
- };
475
- const frozen = await compileResolveFreezeWorkflowV4(asset, input.config ?? schedulerProjectionConfig(input), {
476
- sourceCollector: collector.executionCollector(),
477
- });
478
- const planHash = computePlanHash(frozen.plan);
479
- const executionEvidenceDigest = computeSchedulerExecutionEvidenceDigest(planHash, frozen.plan.sourceReadSet);
480
- evidence.push(Object.freeze({
481
- ref: qualifiedRef,
482
- irVersion: 5,
483
- planHash,
484
- sourceReadSet: frozen.plan.sourceReadSet,
485
- executionEvidenceDigest,
156
+ const schedules = (compiled.plan.schedules ?? []).map((schedule) => ({
157
+ cron: schedule.cron,
158
+ source: `${relative}:${schedule.line}`,
159
+ ordinal: schedule.ordinal,
486
160
  }));
487
- const schedules = compiled.ir.triggers.flatMap((trigger) => trigger.kind === "schedule"
488
- ? [
489
- {
490
- cron: trigger.cron,
491
- source: `${trigger.source.path}:${trigger.source.start}`,
492
- ordinal: trigger.ordinal,
493
- },
494
- ]
495
- : []);
496
- for (const binding of compileWorkflowSchedulerBindings({
497
- qualifiedRef,
498
- schedules,
499
- executionEvidenceDigest,
500
- })) {
161
+ for (const binding of compileWorkflowSchedulerBindings({ qualifiedRef: ref, schedules })) {
501
162
  parseSchedule(binding.cron, input.backend);
502
- out.push(binding);
163
+ desired.push(binding);
503
164
  }
504
165
  }
505
166
  catch (cause) {
506
- failures.push(workflowFailure(failurePath, failureRef, cause));
167
+ failures.push({ path: file, ref, reason: errorMessage(cause) });
507
168
  }
508
169
  }
509
170
  }
510
- function schedulerProjectionConfig(input) {
511
- return Object.freeze({
512
- ...SCHEDULER_PROJECTION_CONFIG,
513
- defaultBundle: input.bundleName,
514
- bundles: {
515
- [input.bundleName]: {
516
- path: input.sourceRoot,
517
- components: { main: { root: ".", adapter: input.adapterId } },
518
- },
519
- },
520
- });
521
- }
522
- function enumerateWorkflowLookups(input, collector, failures) {
523
- const lookups = new Map();
524
- for (const guarded of collector.authoredWorkflowSources(input.adapterId)) {
525
- const sourcePath = guarded.sourcePath;
526
- if (path.basename(sourcePath).toLowerCase() === "readme.md")
527
- continue;
528
- const authoredName = workflowNameForSourcePath(input.sourceRoot, input.adapterId, sourcePath);
529
- if (authoredName === undefined)
530
- continue;
531
- const extension = path.posix.extname(authoredName).toLowerCase();
532
- const stem = authoredName.slice(0, -extension.length).toLowerCase();
533
- const nestedSuffix = WORKFLOW_EXTENSIONS.find((suffix) => stem.endsWith(suffix));
534
- if (nestedSuffix) {
535
- failures.push(workflowFailure(sourcePath, undefined, new WorkflowSourceNameError(guarded.relativePath, nestedSuffix)));
536
- continue;
537
- }
538
- const canonicalName = canonicalizeWorkflowName(authoredName);
539
- const owners = lookups.get(canonicalName) ?? [];
540
- owners.push(guarded);
541
- 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);
542
175
  }
543
- for (const symlink of collector.symlinkSources()) {
544
- if (!isAuthoredWorkflowRelativePath(input.adapterId, symlink.relativePath))
545
- continue;
546
- if (path.basename(symlink.sourcePath).toLowerCase() === "readme.md")
547
- continue;
548
- const authoredName = workflowNameForSourcePath(input.sourceRoot, input.adapterId, symlink.sourcePath);
549
- if (authoredName === undefined)
550
- continue;
551
- const canonicalName = canonicalizeWorkflowName(authoredName);
552
- // A real sibling sharing this name must not compile either: runtime
553
- // resolution follows the symlink and may pick it over the file the
554
- // binding was compiled from. The symbolic failure below is the ref's
555
- // only report.
556
- lookups.delete(canonicalName);
557
- const failureRef = makeBundleRef(input.bundleName, input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName);
558
- if (input.enabledActivations && !input.enabledActivations.has(schedulerActivationKey("workflow", failureRef))) {
559
- continue;
560
- }
561
- failures.push(workflowFailure(symlink.sourcePath, failureRef, symbolicSourceError(symlink.sourcePath)));
176
+ catch {
177
+ return undefined;
562
178
  }
563
- return new Map([...lookups]
564
- .sort(([left], [right]) => compareCodePoints(left, right))
565
- .map(([name, sources]) => [name, Object.freeze(sources.sort(compareGuardedSources))]));
566
179
  }
567
- function installOptionsFor(input, current) {
568
- if (current && !input.rebind) {
569
- return Object.freeze({
570
- ...(input.bundleTarget ? { target: input.bundleTarget } : {}),
571
- binding: Object.freeze([...current.binding]),
572
- contextPath: current.contextPath,
573
- });
574
- }
575
- return input.installOptions ? Object.freeze({ ...input.installOptions }) : undefined;
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;
189
+ }
190
+ }
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);
202
+ }
203
+ };
204
+ visit(dir);
205
+ return files.sort(compareCodePoints);
576
206
  }
577
- function belongsToBundle(entry, input) {
578
- if (input.bundlePath !== undefined && entry.target === input.bundleName) {
579
- // Path-scoped (#846), primary/unconfigured-bundle sync only: the name
580
- // already matches, but a display name derived from a directory
581
- // basename is not an identity — two unrelated bundles can legitimately
582
- // share one. Require the entry's own scheduler-context descriptor to
583
- // additionally confirm the resolved path. An entry whose owning path
584
- // cannot be established is never assumed to be ours — that silent
585
- // assumption is exactly what let an isolated/foreign bundle's sync
586
- // reach for another bundle's real scheduler entries. (`bundlePath` is
587
- // only set for a primary sync — a `--bundle <target>` entry's
588
- // descriptor reflects the invoking process's OWN primary directory,
589
- // not the targeted bundle's, so it is not a meaningful signal there;
590
- // that case keeps relying on config-name uniqueness below.)
591
- return entry.ownerBundlePath !== undefined && entry.ownerBundlePath === input.bundlePath;
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);
592
211
  }
593
- if (entry.target === input.bundleName || entry.target === input.bundleTarget)
594
- return true;
595
- return false;
596
- }
597
- function assertNoForeignIds(desired, input) {
598
- const wanted = new Set(desired.map(({ id }) => id));
599
- const foreign = input.installed.find((entry) => wanted.has(entry.id) && !belongsToBundle(entry, input));
600
- if (!foreign)
601
- return;
602
- const where = foreign.ownerBundlePath
603
- ? `the bundle at ${JSON.stringify(foreign.ownerBundlePath)}`
604
- : foreign.target
605
- ? `bundle ${JSON.stringify(foreign.target)}`
606
- : "the default bundle";
607
- const mine = input.bundlePath ? ` (this sync is scoped to ${JSON.stringify(input.bundlePath)})` : "";
608
- 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");
609
- }
610
- function assertUniqueDesiredIds(desired) {
611
- const seen = new Set();
612
- for (const binding of desired) {
613
- if (seen.has(binding.id)) {
614
- throw new UsageError(`Desired scheduler id collision for ${JSON.stringify(binding.id)}; no native definitions were changed.`, "RESOURCE_ALREADY_EXISTS");
615
- }
616
- seen.add(binding.id);
212
+ catch {
213
+ return undefined;
617
214
  }
618
215
  }
619
- function assertUniqueInstalledIds(installed) {
620
- const seen = new Set();
621
- for (const binding of installed) {
622
- if (seen.has(binding.id)) {
623
- 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");
624
230
  }
625
- seen.add(binding.id);
626
231
  }
627
232
  }
628
233
  /**
629
- * The reason recorded for a symlinked task/workflow source: it stays a
630
- * per-source failure (like the read boundary it replaces), never a silent
631
- * drop and never a follow.
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`.
632
238
  */
633
- function symbolicSourceError(sourcePath) {
634
- return new UsageError(`${sourcePath} is a symbolic source; guarded reads require a regular no-follow owner.`, "RESOURCE_ALREADY_EXISTS");
635
- }
636
- function taskFailure(file, ref, cause) {
637
- const detail = taskSourceErrorDetail(cause);
638
- const reason = detail === errorMessage(cause) ? `${file}: ${detail}` : detail;
639
- return Object.freeze({ path: file, ref, reason });
640
- }
641
- function workflowFailure(file, ref, cause) {
642
- return Object.freeze({ path: file, ...(ref ? { ref } : {}), reason: errorMessage(cause) });
643
- }
644
- function errorMessage(cause) {
645
- return cause instanceof Error ? cause.message : String(cause);
239
+ export function assertTaskScheduleCronValid(v4, backend) {
240
+ for (const scheduleEntry of v4.schedule)
241
+ parseSchedule(scheduleEntry.cron, backend);
646
242
  }
647
- export function assertSchedulerSourceSnapshot(snapshot) {
648
- try {
649
- for (const source of snapshot.files) {
650
- const current = captureGuardedExecutionSource(source.sourcePath, source.containmentRoot, {
651
- authored: source.authored,
652
- ...(source.identity ? { identity: source.identity } : {}),
653
- });
654
- if (JSON.stringify(current) !== JSON.stringify(source)) {
655
- throw new Error(`source identity changed: ${source.sourcePath}`);
656
- }
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;
657
256
  }
658
- for (const manifest of snapshot.directoryManifests) {
659
- const current = captureGuardedDirectoryManifest(manifest.directoryPath, manifest.containmentRoot);
660
- if (JSON.stringify(current) !== JSON.stringify(manifest)) {
661
- throw new Error(`directory manifest changed: ${manifest.directoryPath}`);
662
- }
257
+ catch {
258
+ return undefined;
663
259
  }
664
260
  }
665
- catch (cause) {
666
- throw new UsageError(`Scheduler desired source read set changed after projection; refusing native mutation: ${errorMessage(cause)}`, "RESOURCE_ALREADY_EXISTS");
667
- }
668
- }
669
- /**
670
- * Shared with the symlink classification in {@link compileTaskSources}: a
671
- * `.yml` under `tasks/` (or, for `akm-task`, anywhere) is a task candidate —
672
- * one classifier for both a captured file and an uncaptured symlink entry.
673
- */
674
- function isAuthoredTaskRelativePath(adapterId, relativePath) {
675
- if (!relativePath.endsWith(".yml"))
676
- return false;
677
- if (adapterId === "akm-task")
678
- return true;
679
- return path.posix.dirname(relativePath) === "tasks";
261
+ return undefined;
680
262
  }
681
263
  /**
682
- * Shared with the symlink classification in {@link enumerateWorkflowLookups}:
683
- * anything under `workflows/` (or, for `akm-workflow`, anywhere) is a
684
- * workflow candidate — one classifier for both a captured file and an
685
- * uncaptured symlink entry.
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.
686
269
  */
687
- function isAuthoredWorkflowRelativePath(adapterId, relativePath) {
688
- if (adapterId === "akm-workflow")
689
- return true;
690
- return relativePath.startsWith("workflows/");
691
- }
692
- class SchedulerSourceCollector {
693
- #adapterId;
694
- #sourceRoot;
695
- #collector = new GuardedExecutionSourceCollector();
696
- constructor(input) {
697
- this.#adapterId = input.adapterId;
698
- this.#sourceRoot = path.resolve(input.sourceRoot);
699
- const root = this.#collector.trackDirectory(this.#sourceRoot, this.#sourceRoot);
700
- if (input.adapterId === "akm") {
701
- for (const scheduledName of ["tasks", "workflows"]) {
702
- const entry = root.entries.find((candidate) => candidate.name === scheduledName);
703
- if (entry?.kind === "symlink") {
704
- throw new UsageError(`${path.join(this.#sourceRoot, scheduledName)} is a symbolic source with a physical source identity collision; guarded reads require one no-follow owner.`, "RESOURCE_ALREADY_EXISTS");
705
- }
706
- }
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;
707
277
  }
708
- const rootDirectories = new Set(root.entries.filter((entry) => entry.kind === "directory").map((entry) => entry.name));
709
- const candidates = [];
710
- if (input.adapterId === "akm") {
711
- if (rootDirectories.has("tasks")) {
712
- const taskRoot = path.join(this.#sourceRoot, "tasks");
713
- const taskManifest = this.#collector.trackDirectory(taskRoot, this.#sourceRoot);
714
- for (const entry of taskManifest.entries) {
715
- if (entry.kind === "file" && entry.name.endsWith(".yml")) {
716
- candidates.push(path.join(taskRoot, entry.name));
717
- }
718
- }
719
- }
720
- if (rootDirectories.has("workflows")) {
721
- candidates.push(...this.#collector.enumerateTree(path.join(this.#sourceRoot, "workflows"), this.#sourceRoot));
722
- }
278
+ if (bundle === undefined) {
279
+ if (row.ownerBundlePath === undefined || row.ownerBundlePath === scope.bundlePath)
280
+ return scope;
281
+ continue;
723
282
  }
724
- else if (input.adapterId === "akm-task") {
725
- candidates.push(...this.#collector.enumerateTree(this.#sourceRoot, this.#sourceRoot).filter((file) => file.endsWith(".yml")));
283
+ if (bundle === scope.bundleName && row.ownerBundlePath === scope.bundlePath)
284
+ return scope;
285
+ }
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]);
300
+ }
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
+ });
726
314
  }
727
- else if (input.adapterId === "akm-workflow") {
728
- candidates.push(...this.#collector.enumerateTree(this.#sourceRoot, this.#sourceRoot));
315
+ }
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;
729
334
  }
730
- for (const file of candidates.sort(compareCodePoints)) {
731
- this.#collector.capture(file, this.#sourceRoot, { authored: true });
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;
732
346
  }
733
- }
734
- executionCollector() {
735
- return this.#collector;
736
- }
737
- authoredTaskSources(adapterId) {
738
- return this.#collector
739
- .snapshot()
740
- .sources.filter((file) => file.authored && isAuthoredTaskRelativePath(adapterId, file.relativePath))
741
- .sort(compareGuardedSources);
742
- }
743
- authoredWorkflowSources(adapterId) {
744
- return this.#collector
745
- .snapshot()
746
- .sources.filter((file) => file.authored && isAuthoredWorkflowRelativePath(adapterId, file.relativePath))
747
- .sort(compareGuardedSources);
748
- }
749
- /**
750
- * Every `kind: "symlink"` entry across the directory manifests captured so
751
- * far (never read, never followed — see `captureGuardedDirectoryManifest`),
752
- * with its path relative to the source root so callers can classify it
753
- * with the same predicates as a real, captured file.
754
- */
755
- symlinkSources() {
756
- const entries = [];
757
- for (const manifest of this.#collector.snapshot().directoryManifests) {
758
- for (const entry of manifest.entries) {
759
- if (entry.kind !== "symlink")
760
- continue;
761
- const sourcePath = path.join(manifest.directoryPath, entry.name);
762
- entries.push({ sourcePath, relativePath: toPosix(path.relative(this.#sourceRoot, sourcePath)) });
763
- }
347
+ if (expected === undefined || current.signature === expected) {
348
+ unchanged.push(binding.id);
349
+ continue;
764
350
  }
765
- return entries;
766
- }
767
- readBytes(file, containmentRoot) {
768
- this.#trackAncestors(file, containmentRoot);
769
- return this.#collector.readBytes(file, containmentRoot);
770
- }
771
- fileContext(root, file) {
772
- this.#trackAncestors(file, root);
773
- return this.#collector.fileContext(root, file);
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
+ }));
774
359
  }
775
- snapshot() {
776
- const guarded = this.#collector.snapshot();
777
- const root = guarded.directoryManifests.find((manifest) => manifest.directoryPath === this.#sourceRoot && manifest.containmentRoot === this.#sourceRoot);
778
- if (!root)
779
- throw new Error("scheduler source collector lost its guarded root manifest");
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);
372
+ }
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);
377
+ }
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) {
780
399
  return Object.freeze({
781
- adapterId: this.#adapterId,
782
- sourceRoot: this.#sourceRoot,
783
- sourceRealPath: root.realPath,
784
- sourcePhysicalIdentity: root.physicalIdentity,
785
- sourceDirectoryVersion: root.version,
786
- files: Object.freeze([...guarded.sources].sort(compareGuardedSources)),
787
- directoryManifests: guarded.directoryManifests,
400
+ binding: Object.freeze([...current.binding]),
401
+ contextPath: input.installOptions?.contextPath ?? current.contextPath,
788
402
  });
789
403
  }
790
- #trackAncestors(file, containmentRoot) {
791
- const root = path.resolve(containmentRoot);
792
- const parent = path.dirname(path.resolve(file));
793
- const relative = path.relative(root, parent);
794
- if (relative.startsWith("..") || path.isAbsolute(relative))
795
- return;
796
- this.#collector.trackDirectory(root, root);
797
- if (relative === "")
798
- return;
799
- let current = root;
800
- for (const segment of relative.split(path.sep)) {
801
- current = path.join(current, segment);
802
- this.#collector.trackDirectory(current, root);
803
- }
804
- }
805
- }
806
- function compareGuardedSources(left, right) {
807
- return compareCodePoints(left.sourcePath, right.sourcePath);
404
+ return input.installOptions ? Object.freeze({ ...input.installOptions }) : undefined;
808
405
  }
809
- function installedLogicalSource(invocation, input) {
810
- if (invocation[0] === "workflow" && invocation[1] === "run" && invocation.length === 3) {
811
- return Object.freeze({ kind: "workflow", ref: invocation[2] });
812
- }
813
- if (invocation[0] === "task" && invocation[1] === "run" && invocation[2]) {
814
- const bundleIndex = invocation.indexOf("--bundle", 3);
815
- const bundle = bundleIndex === -1 ? input.bundleName : invocation[bundleIndex + 1];
816
- if (!bundle) {
817
- 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
+ });
818
452
  }
819
- const conceptId = input.adapterId === "akm" ? `tasks/${invocation[2]}` : invocation[2];
820
- return Object.freeze({ kind: "task", ref: makeBundleRef(bundle, conceptId) });
821
453
  }
822
- throw new UsageError("Installed scheduler invocation has no exact canonical source owner.", "RESOURCE_ALREADY_EXISTS");
823
- }
824
- function freezeRemovalExpectation(expectation) {
825
- return Object.freeze({
826
- ...expectation,
827
- logicalSource: Object.freeze({ ...expectation.logicalSource }),
828
- invocation: Object.freeze([...expectation.invocation]),
829
- });
830
- }
831
- function expectationForBinding(binding, state, fingerprint) {
832
454
  return {
833
- state,
834
- bindingId: binding.id,
835
- nativeId: schedulerBindingNativeId(binding),
836
- logicalSource: binding.logicalSource,
837
- ordinal: binding.ordinal,
838
- invocation: binding.invocation,
839
- ...(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],
840
463
  };
841
464
  }
842
- function freezeMutationExpectation(expectation) {
843
- return Object.freeze({
844
- ...expectation,
845
- logicalSource: Object.freeze({ ...expectation.logicalSource }),
846
- invocation: Object.freeze([...expectation.invocation]),
847
- });
848
- }
849
- function freezeOperation(operation) {
850
- return Object.freeze(operation);
851
- }
852
- function sameInvocation(left, right) {
853
- 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);
854
467
  }
855
- // Retain the concrete error in this module's public dependency graph so callers
856
- // can continue to identify ownership failures without importing an adapter.
857
- export { WorkflowSourceRejectionError };