@xemahq/dsl 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/LICENSE +201 -0
  2. package/dist/deliverable-spec/index.d.ts +3 -0
  3. package/dist/deliverable-spec/index.d.ts.map +1 -0
  4. package/dist/deliverable-spec/index.js +19 -0
  5. package/dist/deliverable-spec/index.js.map +1 -0
  6. package/dist/deliverable-spec/lib/schema.d.ts +151 -0
  7. package/dist/deliverable-spec/lib/schema.d.ts.map +1 -0
  8. package/dist/deliverable-spec/lib/schema.js +139 -0
  9. package/dist/deliverable-spec/lib/schema.js.map +1 -0
  10. package/dist/deliverable-spec/lib/types.d.ts +8 -0
  11. package/dist/deliverable-spec/lib/types.d.ts.map +1 -0
  12. package/dist/deliverable-spec/lib/types.js +3 -0
  13. package/dist/deliverable-spec/lib/types.js.map +1 -0
  14. package/dist/payload-codec/index.d.ts +8 -0
  15. package/dist/payload-codec/index.d.ts.map +1 -0
  16. package/dist/payload-codec/index.js +27 -0
  17. package/dist/payload-codec/index.js.map +1 -0
  18. package/dist/payload-codec/lib/blob-store.d.ts +37 -0
  19. package/dist/payload-codec/lib/blob-store.d.ts.map +1 -0
  20. package/dist/payload-codec/lib/blob-store.js +0 -0
  21. package/dist/payload-codec/lib/blob-store.js.map +1 -0
  22. package/dist/payload-codec/lib/codec-context.d.ts +6 -0
  23. package/dist/payload-codec/lib/codec-context.d.ts.map +1 -0
  24. package/dist/payload-codec/lib/codec-context.js +16 -0
  25. package/dist/payload-codec/lib/codec-context.js.map +1 -0
  26. package/dist/payload-codec/lib/codec.d.ts +51 -0
  27. package/dist/payload-codec/lib/codec.d.ts.map +1 -0
  28. package/dist/payload-codec/lib/codec.js +330 -0
  29. package/dist/payload-codec/lib/codec.js.map +1 -0
  30. package/dist/payload-codec/lib/enums.d.ts +18 -0
  31. package/dist/payload-codec/lib/enums.d.ts.map +1 -0
  32. package/dist/payload-codec/lib/enums.js +23 -0
  33. package/dist/payload-codec/lib/enums.js.map +1 -0
  34. package/dist/payload-codec/lib/errors.d.ts +18 -0
  35. package/dist/payload-codec/lib/errors.d.ts.map +1 -0
  36. package/dist/payload-codec/lib/errors.js +39 -0
  37. package/dist/payload-codec/lib/errors.js.map +1 -0
  38. package/dist/payload-codec/lib/http-blob-store.d.ts +21 -0
  39. package/dist/payload-codec/lib/http-blob-store.d.ts.map +1 -0
  40. package/dist/payload-codec/lib/http-blob-store.js +139 -0
  41. package/dist/payload-codec/lib/http-blob-store.js.map +1 -0
  42. package/dist/payload-codec/lib/lru-cache.d.ts +12 -0
  43. package/dist/payload-codec/lib/lru-cache.d.ts.map +1 -0
  44. package/dist/payload-codec/lib/lru-cache.js +59 -0
  45. package/dist/payload-codec/lib/lru-cache.js.map +1 -0
  46. package/dist/schema/action.schema.json +181 -0
  47. package/dist/schema/reusable-workflow.schema.json +46 -0
  48. package/dist/schema/workflow.schema.json +373 -0
  49. package/dist/workflow/index.d.ts +14 -0
  50. package/dist/workflow/index.d.ts.map +1 -0
  51. package/dist/workflow/index.js +49 -0
  52. package/dist/workflow/index.js.map +1 -0
  53. package/dist/workflow/lib/action-input-validator.d.ts +10 -0
  54. package/dist/workflow/lib/action-input-validator.d.ts.map +1 -0
  55. package/dist/workflow/lib/action-input-validator.js +69 -0
  56. package/dist/workflow/lib/action-input-validator.js.map +1 -0
  57. package/dist/workflow/lib/compiler/action-shape.d.ts +5 -0
  58. package/dist/workflow/lib/compiler/action-shape.d.ts.map +1 -0
  59. package/dist/workflow/lib/compiler/action-shape.js +43 -0
  60. package/dist/workflow/lib/compiler/action-shape.js.map +1 -0
  61. package/dist/workflow/lib/compiler/canonical-json.d.ts +3 -0
  62. package/dist/workflow/lib/compiler/canonical-json.d.ts.map +1 -0
  63. package/dist/workflow/lib/compiler/canonical-json.js +45 -0
  64. package/dist/workflow/lib/compiler/canonical-json.js.map +1 -0
  65. package/dist/workflow/lib/compiler/compile.d.ts +4 -0
  66. package/dist/workflow/lib/compiler/compile.d.ts.map +1 -0
  67. package/dist/workflow/lib/compiler/compile.js +794 -0
  68. package/dist/workflow/lib/compiler/compile.js.map +1 -0
  69. package/dist/workflow/lib/compiler/concurrency.d.ts +5 -0
  70. package/dist/workflow/lib/compiler/concurrency.d.ts.map +1 -0
  71. package/dist/workflow/lib/compiler/concurrency.js +104 -0
  72. package/dist/workflow/lib/compiler/concurrency.js.map +1 -0
  73. package/dist/workflow/lib/compiler/dag.d.ts +10 -0
  74. package/dist/workflow/lib/compiler/dag.d.ts.map +1 -0
  75. package/dist/workflow/lib/compiler/dag.js +74 -0
  76. package/dist/workflow/lib/compiler/dag.js.map +1 -0
  77. package/dist/workflow/lib/compiler/index.d.ts +6 -0
  78. package/dist/workflow/lib/compiler/index.d.ts.map +1 -0
  79. package/dist/workflow/lib/compiler/index.js +14 -0
  80. package/dist/workflow/lib/compiler/index.js.map +1 -0
  81. package/dist/workflow/lib/compiler/inputs.d.ts +4 -0
  82. package/dist/workflow/lib/compiler/inputs.d.ts.map +1 -0
  83. package/dist/workflow/lib/compiler/inputs.js +108 -0
  84. package/dist/workflow/lib/compiler/inputs.js.map +1 -0
  85. package/dist/workflow/lib/compiler/installation-resource-validator.d.ts +9 -0
  86. package/dist/workflow/lib/compiler/installation-resource-validator.d.ts.map +1 -0
  87. package/dist/workflow/lib/compiler/installation-resource-validator.js +76 -0
  88. package/dist/workflow/lib/compiler/installation-resource-validator.js.map +1 -0
  89. package/dist/workflow/lib/compiler/manifest-source.d.ts +4 -0
  90. package/dist/workflow/lib/compiler/manifest-source.d.ts.map +1 -0
  91. package/dist/workflow/lib/compiler/manifest-source.js +100 -0
  92. package/dist/workflow/lib/compiler/manifest-source.js.map +1 -0
  93. package/dist/workflow/lib/compiler/matrix.d.ts +4 -0
  94. package/dist/workflow/lib/compiler/matrix.d.ts.map +1 -0
  95. package/dist/workflow/lib/compiler/matrix.js +76 -0
  96. package/dist/workflow/lib/compiler/matrix.js.map +1 -0
  97. package/dist/workflow/lib/compiler/mount-plan.d.ts +4 -0
  98. package/dist/workflow/lib/compiler/mount-plan.d.ts.map +1 -0
  99. package/dist/workflow/lib/compiler/mount-plan.js +96 -0
  100. package/dist/workflow/lib/compiler/mount-plan.js.map +1 -0
  101. package/dist/workflow/lib/compiler/payload-reach-in.d.ts +14 -0
  102. package/dist/workflow/lib/compiler/payload-reach-in.d.ts.map +1 -0
  103. package/dist/workflow/lib/compiler/payload-reach-in.js +273 -0
  104. package/dist/workflow/lib/compiler/payload-reach-in.js.map +1 -0
  105. package/dist/workflow/lib/compiler/permissions.d.ts +6 -0
  106. package/dist/workflow/lib/compiler/permissions.d.ts.map +1 -0
  107. package/dist/workflow/lib/compiler/permissions.js +43 -0
  108. package/dist/workflow/lib/compiler/permissions.js.map +1 -0
  109. package/dist/workflow/lib/compiler/retry-timeout.d.ts +6 -0
  110. package/dist/workflow/lib/compiler/retry-timeout.d.ts.map +1 -0
  111. package/dist/workflow/lib/compiler/retry-timeout.js +64 -0
  112. package/dist/workflow/lib/compiler/retry-timeout.js.map +1 -0
  113. package/dist/workflow/lib/compiler/review-step.d.ts +18 -0
  114. package/dist/workflow/lib/compiler/review-step.d.ts.map +1 -0
  115. package/dist/workflow/lib/compiler/review-step.js +247 -0
  116. package/dist/workflow/lib/compiler/review-step.js.map +1 -0
  117. package/dist/workflow/lib/compiler/types.d.ts +42 -0
  118. package/dist/workflow/lib/compiler/types.d.ts.map +1 -0
  119. package/dist/workflow/lib/compiler/types.js +3 -0
  120. package/dist/workflow/lib/compiler/types.js.map +1 -0
  121. package/dist/workflow/lib/compiler/variable-requirements.d.ts +5 -0
  122. package/dist/workflow/lib/compiler/variable-requirements.d.ts.map +1 -0
  123. package/dist/workflow/lib/compiler/variable-requirements.js +119 -0
  124. package/dist/workflow/lib/compiler/variable-requirements.js.map +1 -0
  125. package/dist/workflow/lib/deliverable-spec-keys.d.ts +3 -0
  126. package/dist/workflow/lib/deliverable-spec-keys.d.ts.map +1 -0
  127. package/dist/workflow/lib/deliverable-spec-keys.js +90 -0
  128. package/dist/workflow/lib/deliverable-spec-keys.js.map +1 -0
  129. package/dist/workflow/lib/dispatch-inputs/index.d.ts +23 -0
  130. package/dist/workflow/lib/dispatch-inputs/index.d.ts.map +1 -0
  131. package/dist/workflow/lib/dispatch-inputs/index.js +106 -0
  132. package/dist/workflow/lib/dispatch-inputs/index.js.map +1 -0
  133. package/dist/workflow/lib/dispatch-inputs/to-json-schema.d.ts +3 -0
  134. package/dist/workflow/lib/dispatch-inputs/to-json-schema.d.ts.map +1 -0
  135. package/dist/workflow/lib/dispatch-inputs/to-json-schema.js +43 -0
  136. package/dist/workflow/lib/dispatch-inputs/to-json-schema.js.map +1 -0
  137. package/dist/workflow/lib/duration.d.ts +2 -0
  138. package/dist/workflow/lib/duration.d.ts.map +1 -0
  139. package/dist/workflow/lib/duration.js +26 -0
  140. package/dist/workflow/lib/duration.js.map +1 -0
  141. package/dist/workflow/lib/errors.d.ts +9 -0
  142. package/dist/workflow/lib/errors.d.ts.map +1 -0
  143. package/dist/workflow/lib/errors.js +28 -0
  144. package/dist/workflow/lib/errors.js.map +1 -0
  145. package/dist/workflow/lib/expression/ast.d.ts +61 -0
  146. package/dist/workflow/lib/expression/ast.d.ts.map +1 -0
  147. package/dist/workflow/lib/expression/ast.js +34 -0
  148. package/dist/workflow/lib/expression/ast.js.map +1 -0
  149. package/dist/workflow/lib/expression/context.d.ts +63 -0
  150. package/dist/workflow/lib/expression/context.d.ts.map +1 -0
  151. package/dist/workflow/lib/expression/context.js +32 -0
  152. package/dist/workflow/lib/expression/context.js.map +1 -0
  153. package/dist/workflow/lib/expression/evaluator.d.ts +5 -0
  154. package/dist/workflow/lib/expression/evaluator.d.ts.map +1 -0
  155. package/dist/workflow/lib/expression/evaluator.js +291 -0
  156. package/dist/workflow/lib/expression/evaluator.js.map +1 -0
  157. package/dist/workflow/lib/expression/index.d.ts +9 -0
  158. package/dist/workflow/lib/expression/index.d.ts.map +1 -0
  159. package/dist/workflow/lib/expression/index.js +26 -0
  160. package/dist/workflow/lib/expression/index.js.map +1 -0
  161. package/dist/workflow/lib/expression/interpolation.d.ts +9 -0
  162. package/dist/workflow/lib/expression/interpolation.d.ts.map +1 -0
  163. package/dist/workflow/lib/expression/interpolation.js +51 -0
  164. package/dist/workflow/lib/expression/interpolation.js.map +1 -0
  165. package/dist/workflow/lib/expression/parser.d.ts +4 -0
  166. package/dist/workflow/lib/expression/parser.d.ts.map +1 -0
  167. package/dist/workflow/lib/expression/parser.js +203 -0
  168. package/dist/workflow/lib/expression/parser.js.map +1 -0
  169. package/dist/workflow/lib/expression/template.d.ts +18 -0
  170. package/dist/workflow/lib/expression/template.d.ts.map +1 -0
  171. package/dist/workflow/lib/expression/template.js +63 -0
  172. package/dist/workflow/lib/expression/template.js.map +1 -0
  173. package/dist/workflow/lib/expression/tokenizer.d.ts +3 -0
  174. package/dist/workflow/lib/expression/tokenizer.d.ts.map +1 -0
  175. package/dist/workflow/lib/expression/tokenizer.js +153 -0
  176. package/dist/workflow/lib/expression/tokenizer.js.map +1 -0
  177. package/dist/workflow/lib/expression/tokens.d.ts +25 -0
  178. package/dist/workflow/lib/expression/tokens.d.ts.map +1 -0
  179. package/dist/workflow/lib/expression/tokens.js +24 -0
  180. package/dist/workflow/lib/expression/tokens.js.map +1 -0
  181. package/dist/workflow/lib/expression/walk-artifact-refs.d.ts +5 -0
  182. package/dist/workflow/lib/expression/walk-artifact-refs.d.ts.map +1 -0
  183. package/dist/workflow/lib/expression/walk-artifact-refs.js +138 -0
  184. package/dist/workflow/lib/expression/walk-artifact-refs.js.map +1 -0
  185. package/dist/workflow/lib/installation-resource-kind.d.ts +14 -0
  186. package/dist/workflow/lib/installation-resource-kind.d.ts.map +1 -0
  187. package/dist/workflow/lib/installation-resource-kind.js +59 -0
  188. package/dist/workflow/lib/installation-resource-kind.js.map +1 -0
  189. package/dist/workflow/lib/schemas-loader.d.ts +4 -0
  190. package/dist/workflow/lib/schemas-loader.d.ts.map +1 -0
  191. package/dist/workflow/lib/schemas-loader.js +36 -0
  192. package/dist/workflow/lib/schemas-loader.js.map +1 -0
  193. package/dist/workflow/lib/serializer.d.ts +3 -0
  194. package/dist/workflow/lib/serializer.d.ts.map +1 -0
  195. package/dist/workflow/lib/serializer.js +15 -0
  196. package/dist/workflow/lib/serializer.js.map +1 -0
  197. package/dist/workflow/lib/types.d.ts +179 -0
  198. package/dist/workflow/lib/types.d.ts.map +1 -0
  199. package/dist/workflow/lib/types.js +3 -0
  200. package/dist/workflow/lib/types.js.map +1 -0
  201. package/dist/workflow/lib/validate.d.ts +8 -0
  202. package/dist/workflow/lib/validate.d.ts.map +1 -0
  203. package/dist/workflow/lib/validate.js +119 -0
  204. package/dist/workflow/lib/validate.js.map +1 -0
  205. package/dist/workspace-manifest/index.d.ts +6 -0
  206. package/dist/workspace-manifest/index.d.ts.map +1 -0
  207. package/dist/workspace-manifest/index.js +22 -0
  208. package/dist/workspace-manifest/index.js.map +1 -0
  209. package/dist/workspace-manifest/lib/compile.d.ts +8 -0
  210. package/dist/workspace-manifest/lib/compile.d.ts.map +1 -0
  211. package/dist/workspace-manifest/lib/compile.js +439 -0
  212. package/dist/workspace-manifest/lib/compile.js.map +1 -0
  213. package/dist/workspace-manifest/lib/interpolate.d.ts +12 -0
  214. package/dist/workspace-manifest/lib/interpolate.d.ts.map +1 -0
  215. package/dist/workspace-manifest/lib/interpolate.js +81 -0
  216. package/dist/workspace-manifest/lib/interpolate.js.map +1 -0
  217. package/dist/workspace-manifest/lib/resolve-extends.d.ts +10 -0
  218. package/dist/workspace-manifest/lib/resolve-extends.d.ts.map +1 -0
  219. package/dist/workspace-manifest/lib/resolve-extends.js +108 -0
  220. package/dist/workspace-manifest/lib/resolve-extends.js.map +1 -0
  221. package/dist/workspace-manifest/lib/schema.d.ts +710 -0
  222. package/dist/workspace-manifest/lib/schema.d.ts.map +1 -0
  223. package/dist/workspace-manifest/lib/schema.js +355 -0
  224. package/dist/workspace-manifest/lib/schema.js.map +1 -0
  225. package/dist/workspace-manifest/lib/types.d.ts +153 -0
  226. package/dist/workspace-manifest/lib/types.d.ts.map +1 -0
  227. package/dist/workspace-manifest/lib/types.js +10 -0
  228. package/dist/workspace-manifest/lib/types.js.map +1 -0
  229. package/package.json +79 -0
  230. package/schema/action.schema.json +181 -0
  231. package/schema/reusable-workflow.schema.json +46 -0
  232. package/schema/workflow.schema.json +373 -0
  233. package/src/deliverable-spec/index.ts +19 -0
  234. package/src/deliverable-spec/lib/schema.ts +248 -0
  235. package/src/deliverable-spec/lib/types.ts +26 -0
  236. package/src/payload-codec/index.ts +40 -0
  237. package/src/payload-codec/lib/blob-store.ts +0 -0
  238. package/src/payload-codec/lib/codec-context.ts +38 -0
  239. package/src/payload-codec/lib/codec.ts +593 -0
  240. package/src/payload-codec/lib/enums.ts +58 -0
  241. package/src/payload-codec/lib/errors.ts +54 -0
  242. package/src/payload-codec/lib/http-blob-store.ts +257 -0
  243. package/src/payload-codec/lib/lru-cache.ts +81 -0
  244. package/src/workflow/index.ts +98 -0
  245. package/src/workflow/lib/action-input-validator.ts +160 -0
  246. package/src/workflow/lib/compiler/action-shape.ts +71 -0
  247. package/src/workflow/lib/compiler/canonical-json.ts +53 -0
  248. package/src/workflow/lib/compiler/compile.ts +1518 -0
  249. package/src/workflow/lib/compiler/concurrency.ts +223 -0
  250. package/src/workflow/lib/compiler/dag.ts +108 -0
  251. package/src/workflow/lib/compiler/index.ts +10 -0
  252. package/src/workflow/lib/compiler/inputs.ts +199 -0
  253. package/src/workflow/lib/compiler/installation-resource-validator.ts +114 -0
  254. package/src/workflow/lib/compiler/manifest-source.ts +176 -0
  255. package/src/workflow/lib/compiler/matrix.ts +135 -0
  256. package/src/workflow/lib/compiler/mount-plan.ts +202 -0
  257. package/src/workflow/lib/compiler/payload-reach-in.ts +497 -0
  258. package/src/workflow/lib/compiler/permissions.ts +64 -0
  259. package/src/workflow/lib/compiler/retry-timeout.ts +105 -0
  260. package/src/workflow/lib/compiler/review-step.ts +517 -0
  261. package/src/workflow/lib/compiler/types.ts +170 -0
  262. package/src/workflow/lib/compiler/variable-requirements.ts +208 -0
  263. package/src/workflow/lib/deliverable-spec-keys.ts +109 -0
  264. package/src/workflow/lib/dispatch-inputs/index.ts +160 -0
  265. package/src/workflow/lib/dispatch-inputs/to-json-schema.ts +60 -0
  266. package/src/workflow/lib/duration.ts +48 -0
  267. package/src/workflow/lib/errors.ts +37 -0
  268. package/src/workflow/lib/expression/ast.ts +108 -0
  269. package/src/workflow/lib/expression/context.ts +148 -0
  270. package/src/workflow/lib/expression/evaluator.ts +492 -0
  271. package/src/workflow/lib/expression/index.ts +28 -0
  272. package/src/workflow/lib/expression/interpolation.ts +84 -0
  273. package/src/workflow/lib/expression/parser.ts +264 -0
  274. package/src/workflow/lib/expression/template.ts +117 -0
  275. package/src/workflow/lib/expression/tokenizer.ts +200 -0
  276. package/src/workflow/lib/expression/tokens.ts +30 -0
  277. package/src/workflow/lib/expression/walk-artifact-refs.ts +232 -0
  278. package/src/workflow/lib/installation-resource-kind.ts +107 -0
  279. package/src/workflow/lib/schemas-loader.ts +64 -0
  280. package/src/workflow/lib/serializer.ts +30 -0
  281. package/src/workflow/lib/types.ts +361 -0
  282. package/src/workflow/lib/validate.ts +199 -0
  283. package/src/workspace-manifest/index.ts +27 -0
  284. package/src/workspace-manifest/lib/compile.ts +608 -0
  285. package/src/workspace-manifest/lib/interpolate.ts +140 -0
  286. package/src/workspace-manifest/lib/resolve-extends.ts +260 -0
  287. package/src/workspace-manifest/lib/schema.ts +612 -0
  288. package/src/workspace-manifest/lib/types.ts +392 -0
@@ -0,0 +1,497 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // ── Payload reach-in validation (plan §C.1) ──
3
+ //
4
+ // Workflow YAML supports two flavours of `needs.<X>.outputs.<name>.<field>`:
5
+ //
6
+ // 1. Envelope-field access — `<field>` is one of
7
+ // {artifactId, versionId, version, hash, type, title}. Free; the
8
+ // runtime reads it off the ArtifactRef without fetching bytes.
9
+ // Already works structurally via `readProperty` on the ref object.
10
+ //
11
+ // 2. Payload reach-in — `<field>` is a property on the bound
12
+ // deliverable spec's JSON Schema. Requires the runtime to
13
+ // pre-fetch the artifact's parsed payload into the per-step
14
+ // `payloadCache`. The compiler validates the path against the
15
+ // bound spec at compile time so authors get a clean DSL error
16
+ // instead of a runtime "Unknown property" surfacing as
17
+ // `DSL_EXPRESSION_INVALID`.
18
+ //
19
+ // This module owns family #2's compile-time checks. The actual runtime
20
+ // pre-fetch is the worker's responsibility — see the plan section on
21
+ // "Compile-time resolution (chosen approach)" — and is gated by the
22
+ // engine wiring spec metadata into `ResolvedDeliverableSpec`. Until
23
+ // that wiring lands for every spec, this validator gracefully skips
24
+ // when it cannot prove typing (the runtime then fails fast via
25
+ // `DSL_PAYLOAD_PARSE_ERROR` if the author insisted on a payload field
26
+ // without a typed binding).
27
+ // ═══════════════════════════════════════════════════════════════════════════
28
+
29
+ import {
30
+ isArtifactRefEnvelopeField,
31
+ WorkflowErrorCode,
32
+ type PayloadReachInPin,
33
+ } from '@xemahq/kernel-contracts/workflow';
34
+ import { WorkflowDslError } from '../errors';
35
+ import {
36
+ compileExpression,
37
+ ExpressionNodeKind,
38
+ stripInterpolation,
39
+ type ExpressionNode,
40
+ extractInterpolations,
41
+ } from '../expression';
42
+ import type { WorkflowDocument } from '../types';
43
+ import type { ResolvedDeliverableSpec, ResolvedRef } from './types';
44
+
45
+ /**
46
+ * Lookup helper: given an upstream job (by key) and an output name,
47
+ * return the deliverable-spec ref the upstream activity manifest pins
48
+ * for that output, or `null` when the manifest declares no per-output
49
+ * binding for it. The compiler prefers this over `with.deliverableSpecRef`
50
+ * — per-output bindings are activity-declared contracts; the job-level
51
+ * `with.deliverableSpecRef` is the workflow-author chosen runtime spec
52
+ * (dynamic, e.g. for the agent activity). They can coexist on the same
53
+ * job; the per-output binding wins for outputs it covers.
54
+ */
55
+ function lookupOutputBindingSpecRef(
56
+ upstream: WorkflowDocument['jobs'][string],
57
+ outputName: string,
58
+ resolvedRefs: Readonly<Record<string, ResolvedRef>>,
59
+ ): string | null {
60
+ const ref = resolvedRefs[upstream.uses];
61
+ const bindings = ref?.actionManifest?.spec.outputBindings;
62
+ if (!bindings) return null;
63
+ const binding = bindings[outputName];
64
+ if (!binding) return null;
65
+ const specRef = binding.deliverableSpecRef;
66
+ if (typeof specRef !== 'string' || specRef.length === 0) return null;
67
+ return specRef;
68
+ }
69
+
70
+ /**
71
+ * Walk every authored expression and field-check
72
+ * `needs.<X>.outputs.<name>.<field>` reach-ins.
73
+ *
74
+ * Decision table for each `<name>.<field>`:
75
+ * • `<field>` is an ArtifactRef envelope field → OK (no payload fetch).
76
+ * • Upstream activity manifest declares `outputBindings.<name>.deliverableSpecRef`
77
+ * (plan §B per-output binding) → use that spec; otherwise fall back
78
+ * to the upstream job's `with.deliverableSpecRef`. Then:
79
+ * - resolved spec has `topLevelKeys` and `<field>` is one → OK.
80
+ * - resolved spec has `topLevelKeys` and `<field>` is NOT one →
81
+ * `DSL_FIELD_NOT_IN_SCHEMA`.
82
+ * • Neither per-output binding NOR `with.deliverableSpecRef` is set,
83
+ * upstream has no `outputs:` projection, and no expression-shaped
84
+ * ref → `DSL_FIELD_NOT_TYPED`.
85
+ * • Otherwise (custom `outputs:` projection, expression-shaped ref,
86
+ * unknown spec, non-introspectable kind) → skip; the runtime
87
+ * evaluator + `payloadCache` will fail fast via
88
+ * `DSL_PAYLOAD_PARSE_ERROR` if the path is ultimately invalid.
89
+ *
90
+ * The validator runs only when `resolvedSpecs` is non-empty — same
91
+ * opt-out convention as the rest of the compiler (preview-raw passes
92
+ * an empty map).
93
+ */
94
+ export function validatePayloadReachIns(
95
+ doc: WorkflowDocument,
96
+ resolvedSpecs: Readonly<Record<string, ResolvedDeliverableSpec>>,
97
+ resolvedRefs: Readonly<Record<string, ResolvedRef>>,
98
+ ): void {
99
+ if (Object.keys(resolvedSpecs).length === 0) return;
100
+
101
+ for (const [jobKey, job] of Object.entries(doc.jobs)) {
102
+ const expressions = collectJobExpressions(jobKey, job);
103
+ for (const { source, fieldPath } of expressions) {
104
+ const ast = compileExpression(source);
105
+ walkPayloadReachIns(ast, (access) => {
106
+ validateReachIn(
107
+ doc,
108
+ jobKey,
109
+ fieldPath,
110
+ access,
111
+ resolvedSpecs,
112
+ resolvedRefs,
113
+ );
114
+ });
115
+ }
116
+ }
117
+
118
+ const callOutputs = doc.on.workflow_call?.outputs;
119
+ if (callOutputs) {
120
+ for (const [outputName, expr] of Object.entries(callOutputs)) {
121
+ const ast = compileExpression(stripInterpolation(expr));
122
+ walkPayloadReachIns(ast, (access) => {
123
+ validateReachIn(
124
+ doc,
125
+ '<workflow_call.outputs>',
126
+ `on.workflow_call.outputs.${outputName}`,
127
+ access,
128
+ resolvedSpecs,
129
+ resolvedRefs,
130
+ );
131
+ });
132
+ }
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Walk every reach-in for a single job and emit one pin per
138
+ * `(upstreamJobKey, specRef)` pair. Pins drive the runtime spec-version
139
+ * assertion (plan §C.1 risks-and-mitigations) — the worker pre-fetches
140
+ * the artifact and refuses to proceed when its `schemaVersion` doesn't
141
+ * match the pin's `specVersion`.
142
+ *
143
+ * Returns `[]` when the job has no resolvable reach-ins (no upstream
144
+ * spec, expression-shaped ref, or projected upstream `outputs:`).
145
+ */
146
+ export function collectPayloadReachInsForJob(
147
+ doc: WorkflowDocument,
148
+ jobKey: string,
149
+ job: WorkflowDocument['jobs'][string],
150
+ resolvedSpecs: Readonly<Record<string, ResolvedDeliverableSpec>>,
151
+ resolvedRefs: Readonly<Record<string, ResolvedRef>>,
152
+ ): readonly PayloadReachInPin[] {
153
+ if (Object.keys(resolvedSpecs).length === 0) return [];
154
+
155
+ const pins: PayloadReachInPin[] = [];
156
+ const seen = new Set<string>();
157
+
158
+ const expressions = collectJobExpressions(jobKey, job);
159
+ for (const { source, fieldPath } of expressions) {
160
+ let ast;
161
+ try {
162
+ ast = compileExpression(source);
163
+ } catch {
164
+ continue;
165
+ }
166
+ walkPayloadReachIns(ast, (access) => {
167
+ const pin = resolveReachInPin(
168
+ doc,
169
+ fieldPath,
170
+ access,
171
+ resolvedSpecs,
172
+ resolvedRefs,
173
+ );
174
+ if (pin === null) return;
175
+ const dedupeKey = `${pin.upstreamJobKey}|${pin.outputName}|${pin.specRef}`;
176
+ if (seen.has(dedupeKey)) return;
177
+ seen.add(dedupeKey);
178
+ pins.push(pin);
179
+ });
180
+ }
181
+
182
+ return pins;
183
+ }
184
+
185
+ /**
186
+ * Resolve one reach-in to a pin entry, returning `null` when the pin
187
+ * cannot be statically materialized (e.g. expression-shaped specRef,
188
+ * upstream has a custom `outputs:` projection, spec not in the resolved
189
+ * map). The validator emits the user-facing error path; this collector
190
+ * is silent on un-resolvable reach-ins because the validator already
191
+ * surfaced them.
192
+ */
193
+ function resolveReachInPin(
194
+ doc: WorkflowDocument,
195
+ fieldPath: string,
196
+ access: PayloadReachIn,
197
+ resolvedSpecs: Readonly<Record<string, ResolvedDeliverableSpec>>,
198
+ resolvedRefs: Readonly<Record<string, ResolvedRef>>,
199
+ ): PayloadReachInPin | null {
200
+ const upstream = doc.jobs[access.upstreamJobKey];
201
+ if (!upstream) return null;
202
+ if (upstream.outputs && Object.keys(upstream.outputs).length > 0) {
203
+ return null;
204
+ }
205
+
206
+ // Prefer per-output binding from the upstream activity manifest
207
+ // (plan §B). Falls back to the job-level `with.deliverableSpecRef`
208
+ // when no per-output binding is declared OR when the reach-in's
209
+ // outputName is one of the indexed sentinels.
210
+ let specRef: string | null = null;
211
+ let outputName: string | null = null;
212
+ if (
213
+ access.outputName !== '<indexed>' &&
214
+ access.outputName !== '<byKey>'
215
+ ) {
216
+ const perOutputRef = lookupOutputBindingSpecRef(
217
+ upstream,
218
+ access.outputName,
219
+ resolvedRefs,
220
+ );
221
+ if (perOutputRef !== null) {
222
+ specRef = perOutputRef;
223
+ outputName = access.outputName;
224
+ }
225
+ }
226
+ if (specRef === null) {
227
+ const withMap = upstream.with as Record<string, unknown> | undefined;
228
+ const fromWith = withMap?.['deliverableSpecRef'];
229
+ if (typeof fromWith !== 'string' || fromWith.length === 0) return null;
230
+ if (fromWith.includes('${{')) return null;
231
+ specRef = fromWith;
232
+ }
233
+
234
+ const spec = resolvedSpecs[specRef];
235
+ if (!spec) return null;
236
+ const at = specRef.lastIndexOf('@');
237
+ const specVersion = at >= 0 ? specRef.slice(at + 1) : '';
238
+ if (specVersion.length === 0) return null;
239
+ return {
240
+ fieldPath,
241
+ upstreamJobKey: access.upstreamJobKey,
242
+ outputName,
243
+ specRef,
244
+ specVersion,
245
+ };
246
+ }
247
+
248
+ interface CollectedExpression {
249
+ readonly source: string;
250
+ readonly fieldPath: string;
251
+ }
252
+
253
+ function collectJobExpressions(
254
+ jobKey: string,
255
+ job: WorkflowDocument['jobs'][string],
256
+ ): readonly CollectedExpression[] {
257
+ const out: CollectedExpression[] = [];
258
+ if (job.if !== undefined) {
259
+ out.push({ source: stripInterpolation(job.if), fieldPath: `${jobKey}.if` });
260
+ }
261
+ if (job.with) {
262
+ for (const ext of extractInterpolations(job.with, [jobKey, 'with'])) {
263
+ out.push({ source: ext.source, fieldPath: ext.path.join('.') });
264
+ }
265
+ }
266
+ if (job.outputs) {
267
+ for (const [name, expr] of Object.entries(job.outputs)) {
268
+ out.push({
269
+ source: stripInterpolation(expr),
270
+ fieldPath: `${jobKey}.outputs.${name}`,
271
+ });
272
+ }
273
+ }
274
+ if (job.strategy && 'dynamic' in job.strategy) {
275
+ out.push({
276
+ source: stripInterpolation(job.strategy.dynamic.from),
277
+ fieldPath: `${jobKey}.strategy.dynamic.from`,
278
+ });
279
+ }
280
+ return out;
281
+ }
282
+
283
+ interface PayloadReachIn {
284
+ readonly upstreamJobKey: string;
285
+ readonly outputName: string;
286
+ readonly field: string;
287
+ }
288
+
289
+ export function walkPayloadReachIns(
290
+ node: ExpressionNode,
291
+ visit: (access: PayloadReachIn) => void,
292
+ ): void {
293
+ const match = matchPayloadReachIn(node);
294
+ if (match !== null) {
295
+ visit(match);
296
+ // Continue recursion so nested expressions inside index nodes are
297
+ // also walked — e.g. byKey[needs.Y.outputs.foo.bar] would carry
298
+ // its own reach-in.
299
+ }
300
+ switch (node.kind) {
301
+ case ExpressionNodeKind.MEMBER:
302
+ walkPayloadReachIns(node.target, visit);
303
+ return;
304
+ case ExpressionNodeKind.INDEX:
305
+ walkPayloadReachIns(node.target, visit);
306
+ walkPayloadReachIns(node.index, visit);
307
+ return;
308
+ case ExpressionNodeKind.CALL:
309
+ // Skip `fromJSON(...)` and `toJSON(...)` arguments — those are
310
+ // the documented escape hatch; the author opted out of typed
311
+ // reach-in validation by wrapping the ref.
312
+ for (const arg of node.args) walkPayloadReachIns(arg, visit);
313
+ return;
314
+ case ExpressionNodeKind.UNARY_NOT:
315
+ walkPayloadReachIns(node.operand, visit);
316
+ return;
317
+ case ExpressionNodeKind.BINARY_EQ:
318
+ case ExpressionNodeKind.BINARY_NEQ:
319
+ case ExpressionNodeKind.BINARY_AND:
320
+ case ExpressionNodeKind.BINARY_OR:
321
+ walkPayloadReachIns(node.left, visit);
322
+ walkPayloadReachIns(node.right, visit);
323
+ return;
324
+ case ExpressionNodeKind.LITERAL:
325
+ case ExpressionNodeKind.IDENTIFIER:
326
+ return;
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Match the outermost `needs.<X>.outputs.<name>.<field>` shape. Returns
332
+ * null if the chain doesn't match, OR if `<field>` is an envelope
333
+ * field (the runtime reads those off the ArtifactRef without a fetch).
334
+ *
335
+ * Also matches:
336
+ * needs.<X>.outputs[N].<field>
337
+ * needs.<X>.outputs.byKey['k'].<field>
338
+ *
339
+ * For these, `<name>` is set to a sentinel so the validator still
340
+ * resolves the upstream job's spec but doesn't attempt per-output
341
+ * name resolution (today's data model declares the spec at the
342
+ * job level, not per-output — see the plan's "B." section).
343
+ */
344
+ function matchPayloadReachIn(node: ExpressionNode): PayloadReachIn | null {
345
+ if (node.kind !== ExpressionNodeKind.MEMBER) return null;
346
+ const field = node.property;
347
+ if (isArtifactRefEnvelopeField(field)) return null;
348
+
349
+ // Walk one level up: `<base>.<field>` where `<base>` must resolve to
350
+ // `needs.<X>.outputs.<name>` (or an indexed/keyed variant).
351
+ const base = node.target;
352
+
353
+ // Form A: needs.<X>.outputs.<name>.<field>
354
+ if (base.kind === ExpressionNodeKind.MEMBER) {
355
+ const outputName = base.property;
356
+ if (outputName === 'byKey') return null; // handled below
357
+ const outputsNode = base.target;
358
+ const upstreamKey = matchNeedsOutputsRoot(outputsNode);
359
+ if (upstreamKey !== null) {
360
+ return { upstreamJobKey: upstreamKey, outputName, field };
361
+ }
362
+ }
363
+
364
+ // Form B: needs.<X>.outputs[N].<field> OR needs.<X>.outputs.byKey['k'].<field>
365
+ if (base.kind === ExpressionNodeKind.INDEX) {
366
+ const indexed = base.target;
367
+ // outputs[N]
368
+ const upstreamFromOutputs = matchNeedsOutputsRoot(indexed);
369
+ if (upstreamFromOutputs !== null) {
370
+ return {
371
+ upstreamJobKey: upstreamFromOutputs,
372
+ outputName: '<indexed>',
373
+ field,
374
+ };
375
+ }
376
+ // outputs.byKey['k']
377
+ if (
378
+ indexed.kind === ExpressionNodeKind.MEMBER &&
379
+ indexed.property === 'byKey'
380
+ ) {
381
+ const upstreamFromByKey = matchNeedsOutputsRoot(indexed.target);
382
+ if (upstreamFromByKey !== null) {
383
+ return {
384
+ upstreamJobKey: upstreamFromByKey,
385
+ outputName: '<byKey>',
386
+ field,
387
+ };
388
+ }
389
+ }
390
+ }
391
+
392
+ return null;
393
+ }
394
+
395
+ /**
396
+ * Match the chain `needs.<X>.outputs`. Returns the upstream job key, or
397
+ * null if the node isn't that shape.
398
+ */
399
+ function matchNeedsOutputsRoot(node: ExpressionNode): string | null {
400
+ if (node.kind !== ExpressionNodeKind.MEMBER) return null;
401
+ if (node.property !== 'outputs') return null;
402
+ const needsRoot = node.target;
403
+ if (needsRoot.kind !== ExpressionNodeKind.MEMBER) return null;
404
+ const upstreamKey = needsRoot.property;
405
+ const ident = needsRoot.target;
406
+ if (ident.kind !== ExpressionNodeKind.IDENTIFIER) return null;
407
+ if (ident.name !== 'needs') return null;
408
+ return upstreamKey;
409
+ }
410
+
411
+ function validateReachIn(
412
+ doc: WorkflowDocument,
413
+ consumerJobKey: string,
414
+ fieldPath: string,
415
+ access: PayloadReachIn,
416
+ resolvedSpecs: Readonly<Record<string, ResolvedDeliverableSpec>>,
417
+ resolvedRefs: Readonly<Record<string, ResolvedRef>>,
418
+ ): void {
419
+ const upstream = doc.jobs[access.upstreamJobKey];
420
+ if (!upstream) {
421
+ // Topological-sort step already raises a clearer error for unknown
422
+ // upstreams; don't double-report here.
423
+ return;
424
+ }
425
+
426
+ // If the upstream declares an `outputs:` projection, it re-shapes the
427
+ // exposed namespace. We can't statically introspect arbitrary author
428
+ // projections, so skip — the projection itself is expression-validated
429
+ // upstream.
430
+ if (upstream.outputs && Object.keys(upstream.outputs).length > 0) {
431
+ return;
432
+ }
433
+
434
+ // Per-output binding from the upstream activity manifest (plan §B)
435
+ // wins over the job-level `with.deliverableSpecRef` when present.
436
+ const perOutputBinding =
437
+ access.outputName === '<indexed>' || access.outputName === '<byKey>'
438
+ ? null
439
+ : lookupOutputBindingSpecRef(upstream, access.outputName, resolvedRefs);
440
+
441
+ const withMap = upstream.with as Record<string, unknown> | undefined;
442
+ const specRef = perOutputBinding ?? withMap?.['deliverableSpecRef'];
443
+
444
+ // Expression-shaped spec → unknowable at compile time. Runtime
445
+ // surfaces `DSL_PAYLOAD_PARSE_ERROR` if reach-in is ultimately wrong.
446
+ if (typeof specRef === 'string' && specRef.includes('${{')) {
447
+ return;
448
+ }
449
+
450
+ if (typeof specRef !== 'string' || specRef.length === 0) {
451
+ throw new WorkflowDslError(
452
+ WorkflowErrorCode.DSL_FIELD_NOT_TYPED,
453
+ `${fieldPath}: 'needs.${access.upstreamJobKey}.outputs.${access.outputName}.${access.field}' reaches into the payload of an output that has no schema-bearing deliverable spec. Either bind a typed 'with.deliverableSpecRef' on '${access.upstreamJobKey}' or use the 'fromJSON(...)' escape hatch.`,
454
+ {
455
+ consumerJobKey,
456
+ upstreamJobKey: access.upstreamJobKey,
457
+ outputName: access.outputName,
458
+ field: access.field,
459
+ fieldPath,
460
+ },
461
+ );
462
+ }
463
+
464
+ const spec = resolvedSpecs[specRef];
465
+ if (!spec) {
466
+ // Engine couldn't resolve the spec (e.g. registry unreachable) —
467
+ // the existing literal-reference validator already emits
468
+ // `DSL_UNKNOWN_DELIVERABLE_SPEC` for this case; don't double-report.
469
+ return;
470
+ }
471
+ if (!spec.topLevelKeys || spec.topLevelKeys.length === 0) {
472
+ // Spec kind doesn't support static field introspection
473
+ // (DOCUMENT_TEMPLATE, STRUCTURED_JSON, CUSTOM, …). Defer to runtime.
474
+ return;
475
+ }
476
+ if (spec.topLevelKeys.includes(access.field)) {
477
+ // Valid reach-in. The runtime is responsible for pre-fetching this
478
+ // artifact's payload into the per-step `payloadCache` before the
479
+ // expression evaluates — see the plan's runtime-resolver scaffold.
480
+ return;
481
+ }
482
+
483
+ const knownPreview = spec.topLevelKeys.slice(0, 12).join(', ');
484
+ throw new WorkflowDslError(
485
+ WorkflowErrorCode.DSL_FIELD_NOT_IN_SCHEMA,
486
+ `${fieldPath}: 'needs.${access.upstreamJobKey}.outputs.${access.outputName}.${access.field}' is not a top-level field on deliverable spec '${spec.ref}' (bound by '${access.upstreamJobKey}.with.deliverableSpecRef'). Known fields: [${knownPreview}].`,
487
+ {
488
+ consumerJobKey,
489
+ upstreamJobKey: access.upstreamJobKey,
490
+ outputName: access.outputName,
491
+ field: access.field,
492
+ fieldPath,
493
+ specRef: spec.ref,
494
+ knownFields: [...spec.topLevelKeys],
495
+ },
496
+ );
497
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ PermissionResource,
3
+ PermissionScope,
4
+ WorkflowErrorCode,
5
+ } from '@xemahq/kernel-contracts/workflow';
6
+ import { WorkflowDslError } from '../errors';
7
+ import type { WorkflowPermissions } from '../types';
8
+
9
+ /** Scope strength ordering: stronger scopes dominate weaker ones. */
10
+ const SCOPE_RANK: Readonly<Record<PermissionScope, number>> = Object.freeze({
11
+ [PermissionScope.NONE]: 0,
12
+ [PermissionScope.READ]: 1,
13
+ [PermissionScope.LIMITED]: 2,
14
+ [PermissionScope.WRITE]: 3,
15
+ });
16
+
17
+ /** Zero permissions — the platform-safe default when nothing is authored. */
18
+ export const ZERO_PERMISSIONS: Readonly<Record<PermissionResource, PermissionScope>> =
19
+ Object.freeze({
20
+ [PermissionResource.REPOS]: PermissionScope.NONE,
21
+ [PermissionResource.KB]: PermissionScope.NONE,
22
+ [PermissionResource.BACKLOG]: PermissionScope.NONE,
23
+ [PermissionResource.INTEGRATIONS]: PermissionScope.NONE,
24
+ [PermissionResource.ARTIFACTS]: PermissionScope.NONE,
25
+ [PermissionResource.MEMORY]: PermissionScope.NONE,
26
+ });
27
+
28
+ /** Fill an authored permission map with explicit NONE values for unset resources. */
29
+ export function normalizePermissions(
30
+ authored: WorkflowPermissions | undefined,
31
+ ): Readonly<Record<PermissionResource, PermissionScope>> {
32
+ const base: Record<PermissionResource, PermissionScope> = { ...ZERO_PERMISSIONS };
33
+ if (!authored) return base;
34
+ for (const resource of Object.values(PermissionResource)) {
35
+ const scope = authored[resource];
36
+ if (scope !== undefined) {
37
+ base[resource] = scope;
38
+ }
39
+ }
40
+ return base;
41
+ }
42
+
43
+ /**
44
+ * Verify that `job` does not request stronger permissions than `workflow`.
45
+ * Fails fast with DSL_PERMISSION_ESCALATION on any escalation — rule 9
46
+ * (clean architecture), rule 2 (fail-fast).
47
+ */
48
+ export function assertJobPermissionsFit(
49
+ jobPermissions: Readonly<Record<PermissionResource, PermissionScope>>,
50
+ workflowPermissions: Readonly<Record<PermissionResource, PermissionScope>>,
51
+ jobKey: string,
52
+ ): void {
53
+ for (const resource of Object.values(PermissionResource)) {
54
+ const jobScope = jobPermissions[resource];
55
+ const workflowScope = workflowPermissions[resource];
56
+ if (SCOPE_RANK[jobScope] > SCOPE_RANK[workflowScope]) {
57
+ throw new WorkflowDslError(
58
+ WorkflowErrorCode.DSL_PERMISSION_ESCALATION,
59
+ `Job '${jobKey}' requests ${jobScope} on ${resource} but workflow grants only ${workflowScope}.`,
60
+ { jobKey, resource, jobScope, workflowScope },
61
+ );
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,105 @@
1
+ import type {
2
+ CompiledDefaults,
3
+ CompiledRetryPolicy,
4
+ } from '@xemahq/kernel-contracts/workflow';
5
+ import type {
6
+ ActionManifestRetryDefaults,
7
+ ActionManifestTimeoutDefaults,
8
+ WorkflowDefaults,
9
+ WorkflowRetryDeclaration,
10
+ } from '../types';
11
+ import { parseDurationMs } from '../duration';
12
+
13
+ // Default retry applied when neither the workflow nor the action declares
14
+ // one. Deliberately conservative — one attempt, no backoff — so authors
15
+ // opt into retry explicitly.
16
+ const PLATFORM_DEFAULT_RETRY: CompiledRetryPolicy = {
17
+ maxAttempts: 1,
18
+ initialIntervalMs: 1_000,
19
+ backoffCoefficient: 2.0,
20
+ maximumIntervalMs: 60_000,
21
+ nonRetryableErrorTypes: [],
22
+ };
23
+
24
+ // Default job timeout: 1 hour. Agent activities that need more declare it
25
+ // explicitly in their action manifest or in the job's timeout field.
26
+ const PLATFORM_DEFAULT_TIMEOUT_MS = 3_600_000;
27
+
28
+ /** Merge workflow-level defaults against platform baseline. */
29
+ export function resolveWorkflowDefaults(
30
+ authored: WorkflowDefaults | undefined,
31
+ ): CompiledDefaults {
32
+ return {
33
+ retry: resolveRetry(authored?.retry, PLATFORM_DEFAULT_RETRY),
34
+ timeoutMs: authored?.timeout !== undefined
35
+ ? parseDurationMs(authored.timeout)
36
+ : PLATFORM_DEFAULT_TIMEOUT_MS,
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Merge job-level retry against workflow-level + action-manifest-level
42
+ * defaults. Merge order: action manifest → workflow → job (most specific
43
+ * wins). Explicit; no "if null, skip" silent fallbacks.
44
+ */
45
+ export function resolveJobRetry(
46
+ workflowDefault: CompiledRetryPolicy,
47
+ actionDefault: ActionManifestRetryDefaults | null,
48
+ jobOverride: WorkflowRetryDeclaration | undefined,
49
+ ): CompiledRetryPolicy {
50
+ const actionBaseline = actionDefault
51
+ ? fromActionManifestRetry(actionDefault)
52
+ : workflowDefault;
53
+
54
+ return resolveRetry(jobOverride, actionBaseline);
55
+ }
56
+
57
+ /**
58
+ * Resolve job-level timeout. Merge order: action manifest → workflow → job.
59
+ */
60
+ export function resolveJobTimeout(
61
+ workflowDefaultMs: number,
62
+ actionDefault: ActionManifestTimeoutDefaults | null,
63
+ jobOverride: string | undefined,
64
+ ): number {
65
+ if (jobOverride !== undefined) {
66
+ return parseDurationMs(jobOverride);
67
+ }
68
+ if (actionDefault) {
69
+ return parseDurationMs(actionDefault.startToClose);
70
+ }
71
+ return workflowDefaultMs;
72
+ }
73
+
74
+ function resolveRetry(
75
+ override: WorkflowRetryDeclaration | undefined,
76
+ baseline: CompiledRetryPolicy,
77
+ ): CompiledRetryPolicy {
78
+ if (!override) return baseline;
79
+ return {
80
+ maxAttempts: override.maxAttempts ?? baseline.maxAttempts,
81
+ initialIntervalMs:
82
+ override.initialInterval !== undefined
83
+ ? parseDurationMs(override.initialInterval)
84
+ : baseline.initialIntervalMs,
85
+ backoffCoefficient:
86
+ override.backoffCoefficient ?? baseline.backoffCoefficient,
87
+ maximumIntervalMs:
88
+ override.maximumInterval !== undefined
89
+ ? parseDurationMs(override.maximumInterval)
90
+ : baseline.maximumIntervalMs,
91
+ nonRetryableErrorTypes: override.nonRetryableErrorTypes !== undefined
92
+ ? [...override.nonRetryableErrorTypes]
93
+ : [...baseline.nonRetryableErrorTypes],
94
+ };
95
+ }
96
+
97
+ function fromActionManifestRetry(retry: ActionManifestRetryDefaults): CompiledRetryPolicy {
98
+ return {
99
+ maxAttempts: retry.maxAttempts,
100
+ initialIntervalMs: parseDurationMs(retry.initialInterval),
101
+ backoffCoefficient: retry.backoffCoefficient,
102
+ maximumIntervalMs: parseDurationMs(retry.maximumInterval),
103
+ nonRetryableErrorTypes: retry.nonRetryableErrorTypes !== undefined ? [...retry.nonRetryableErrorTypes] : [],
104
+ };
105
+ }