@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,264 @@
1
+ import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
2
+ import { WorkflowDslError } from '../errors';
3
+ import {
4
+ type BinaryNode,
5
+ type ExpressionNode,
6
+ ExpressionFunction,
7
+ ExpressionNodeKind,
8
+ ExpressionRoot,
9
+ type IdentifierNode,
10
+ type IndexNode,
11
+ type LiteralNode,
12
+ type MemberNode,
13
+ } from './ast';
14
+ import { type Token, TokenKind } from './tokens';
15
+
16
+ /**
17
+ * Recursive-descent parser for the Xema expression subset. Grammar is a
18
+ * closed subset inspired by GitHub Actions' expression language:
19
+ *
20
+ * expression := logicalOr
21
+ * logicalOr := logicalAnd ('||' logicalAnd)*
22
+ * logicalAnd := equality ('&&' equality)*
23
+ * equality := unary (('==' | '!=') unary)*
24
+ * unary := '!' unary | postfix
25
+ * postfix := primary ('.' IDENT | '[' expression ']')*
26
+ * primary := '(' expression ')'
27
+ * | literal
28
+ * | identifierOrCall
29
+ * identifierOrCall := IDENT ('(' argList? ')')?
30
+ * argList := expression (',' expression)*
31
+ *
32
+ * Root identifiers must be one of {needs, matrix, inputs, vars, trigger, job}.
33
+ * Call callees must be one of {fromJSON, toJSON}.
34
+ */
35
+ export function parseExpression(source: string, tokens: readonly Token[]): ExpressionNode {
36
+ const parser = new Parser(source, tokens);
37
+ const node = parser.parseExpression();
38
+ parser.expect(TokenKind.EOF);
39
+ return node;
40
+ }
41
+
42
+ class Parser {
43
+ private index = 0;
44
+
45
+ constructor(
46
+ private readonly source: string,
47
+ private readonly tokens: readonly Token[],
48
+ ) {}
49
+
50
+ parseExpression(): ExpressionNode {
51
+ return this.parseOr();
52
+ }
53
+
54
+ expect(kind: TokenKind): Token {
55
+ const token = this.peek();
56
+ if (token.kind !== kind) {
57
+ throw this.error(
58
+ `Expected ${kind} but found ${token.kind === TokenKind.EOF ? 'end of expression' : token.kind}`,
59
+ token.column,
60
+ );
61
+ }
62
+ this.index++;
63
+ return token;
64
+ }
65
+
66
+ private peek(): Token {
67
+ const token = this.tokens[this.index];
68
+ if (!token) {
69
+ throw this.error('Parser consumed past end of token stream', this.source.length + 1);
70
+ }
71
+ return token;
72
+ }
73
+
74
+ private consume(): Token {
75
+ const token = this.peek();
76
+ this.index++;
77
+ return token;
78
+ }
79
+
80
+ private match(kind: TokenKind): boolean {
81
+ return this.peek().kind === kind;
82
+ }
83
+
84
+ private parseOr(): ExpressionNode {
85
+ let left = this.parseAnd();
86
+ while (this.match(TokenKind.OR)) {
87
+ this.consume();
88
+ const right = this.parseAnd();
89
+ left = binary(ExpressionNodeKind.BINARY_OR, left, right);
90
+ }
91
+ return left;
92
+ }
93
+
94
+ private parseAnd(): ExpressionNode {
95
+ let left = this.parseEquality();
96
+ while (this.match(TokenKind.AND)) {
97
+ this.consume();
98
+ const right = this.parseEquality();
99
+ left = binary(ExpressionNodeKind.BINARY_AND, left, right);
100
+ }
101
+ return left;
102
+ }
103
+
104
+ private parseEquality(): ExpressionNode {
105
+ let left = this.parseUnary();
106
+ for (;;) {
107
+ if (this.match(TokenKind.EQ)) {
108
+ this.consume();
109
+ const right = this.parseUnary();
110
+ left = binary(ExpressionNodeKind.BINARY_EQ, left, right);
111
+ } else if (this.match(TokenKind.NEQ)) {
112
+ this.consume();
113
+ const right = this.parseUnary();
114
+ left = binary(ExpressionNodeKind.BINARY_NEQ, left, right);
115
+ } else {
116
+ break;
117
+ }
118
+ }
119
+ return left;
120
+ }
121
+
122
+ private parseUnary(): ExpressionNode {
123
+ if (this.match(TokenKind.BANG)) {
124
+ this.consume();
125
+ const operand = this.parseUnary();
126
+ return { kind: ExpressionNodeKind.UNARY_NOT, operand };
127
+ }
128
+ return this.parsePostfix();
129
+ }
130
+
131
+ private parsePostfix(): ExpressionNode {
132
+ let node = this.parsePrimary();
133
+ for (;;) {
134
+ if (this.match(TokenKind.DOT)) {
135
+ this.consume();
136
+ const propertyToken = this.expect(TokenKind.IDENTIFIER);
137
+ const member: MemberNode = {
138
+ kind: ExpressionNodeKind.MEMBER,
139
+ target: node,
140
+ property: propertyToken.value,
141
+ };
142
+ node = member;
143
+ } else if (this.match(TokenKind.LBRACKET)) {
144
+ this.consume();
145
+ const indexExpr = this.parseExpression();
146
+ this.expect(TokenKind.RBRACKET);
147
+ const indexNode: IndexNode = {
148
+ kind: ExpressionNodeKind.INDEX,
149
+ target: node,
150
+ index: indexExpr,
151
+ };
152
+ node = indexNode;
153
+ } else {
154
+ break;
155
+ }
156
+ }
157
+ return node;
158
+ }
159
+
160
+ private parsePrimary(): ExpressionNode {
161
+ const token = this.peek();
162
+ switch (token.kind) {
163
+ case TokenKind.LPAREN: {
164
+ this.consume();
165
+ const expr = this.parseExpression();
166
+ this.expect(TokenKind.RPAREN);
167
+ return expr;
168
+ }
169
+ case TokenKind.NUMBER: {
170
+ this.consume();
171
+ const literal: LiteralNode = { kind: ExpressionNodeKind.LITERAL, value: Number(token.value) };
172
+ return literal;
173
+ }
174
+ case TokenKind.STRING: {
175
+ this.consume();
176
+ const literal: LiteralNode = { kind: ExpressionNodeKind.LITERAL, value: token.value };
177
+ return literal;
178
+ }
179
+ case TokenKind.BOOLEAN: {
180
+ this.consume();
181
+ const literal: LiteralNode = { kind: ExpressionNodeKind.LITERAL, value: token.value === 'true' };
182
+ return literal;
183
+ }
184
+ case TokenKind.NULL: {
185
+ this.consume();
186
+ const literal: LiteralNode = { kind: ExpressionNodeKind.LITERAL, value: null };
187
+ return literal;
188
+ }
189
+ case TokenKind.IDENTIFIER: {
190
+ return this.parseIdentifierOrCall();
191
+ }
192
+ default:
193
+ throw this.error(
194
+ `Unexpected token '${token.value}' at column ${token.column}`,
195
+ token.column,
196
+ );
197
+ }
198
+ }
199
+
200
+ private parseIdentifierOrCall(): ExpressionNode {
201
+ const identifierToken = this.expect(TokenKind.IDENTIFIER);
202
+ const name = identifierToken.value;
203
+
204
+ if (this.match(TokenKind.LPAREN)) {
205
+ this.consume();
206
+ if (!isExpressionFunction(name)) {
207
+ throw this.error(
208
+ `Unknown function '${name}' at column ${identifierToken.column}. Allowed: ${Object.values(ExpressionFunction).join(', ')}.`,
209
+ identifierToken.column,
210
+ );
211
+ }
212
+ const args: ExpressionNode[] = [];
213
+ if (!this.match(TokenKind.RPAREN)) {
214
+ args.push(this.parseExpression());
215
+ while (this.match(TokenKind.COMMA)) {
216
+ this.consume();
217
+ args.push(this.parseExpression());
218
+ }
219
+ }
220
+ this.expect(TokenKind.RPAREN);
221
+ return {
222
+ kind: ExpressionNodeKind.CALL,
223
+ callee: name,
224
+ args,
225
+ };
226
+ }
227
+
228
+ // Bare identifier: must be one of the allowed roots.
229
+ if (!isExpressionRoot(name)) {
230
+ throw this.error(
231
+ `Unknown identifier '${name}' at column ${identifierToken.column}. Allowed roots: ${Object.values(ExpressionRoot).join(', ')}.`,
232
+ identifierToken.column,
233
+ );
234
+ }
235
+ const ident: IdentifierNode = { kind: ExpressionNodeKind.IDENTIFIER, name };
236
+ return ident;
237
+ }
238
+
239
+ private error(message: string, column: number): WorkflowDslError {
240
+ return new WorkflowDslError(
241
+ WorkflowErrorCode.DSL_EXPRESSION_INVALID,
242
+ `${message} [expression: ${this.source}]`,
243
+ { expression: this.source, column },
244
+ );
245
+ }
246
+ }
247
+
248
+ function binary(
249
+ kind: BinaryNode['kind'],
250
+ left: ExpressionNode,
251
+ right: ExpressionNode,
252
+ ): BinaryNode {
253
+ return { kind, left, right };
254
+ }
255
+
256
+ function isExpressionRoot(name: string): name is ExpressionRoot {
257
+ const roots: readonly string[] = Object.values(ExpressionRoot);
258
+ return roots.includes(name);
259
+ }
260
+
261
+ function isExpressionFunction(name: string): name is ExpressionFunction {
262
+ const fns: readonly string[] = Object.values(ExpressionFunction);
263
+ return fns.includes(name);
264
+ }
@@ -0,0 +1,117 @@
1
+ import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
2
+ import { WorkflowDslError } from '../errors';
3
+
4
+ /**
5
+ * Template parsing for fields that explicitly accept partial interpolation
6
+ * (`literal-text-${{ expression }}-more-text`). Most authored expressions in
7
+ * the DSL must be fully wrapped — see `interpolation.ts` and the policy on
8
+ * `with`, `outputs`, `if`, `strategy.dynamic.from`. Concurrency `group` is
9
+ * the documented exception, so it composes from this primitive instead.
10
+ *
11
+ * The parser is deterministic and total: every input string maps to one
12
+ * sequence of segments. Authoring errors (an open `${{` without a matching
13
+ * `}}`, an empty body) raise `DSL_EXPRESSION_INVALID` at the position they
14
+ * occur — never at <root>, never silently coerced.
15
+ */
16
+ export enum TemplateSegmentKind {
17
+ Literal = 'literal',
18
+ Expression = 'expression',
19
+ }
20
+
21
+ export interface TemplateLiteralSegment {
22
+ readonly kind: TemplateSegmentKind.Literal;
23
+ readonly value: string;
24
+ }
25
+
26
+ export interface TemplateExpressionSegment {
27
+ readonly kind: TemplateSegmentKind.Expression;
28
+ readonly source: string;
29
+ /** Byte offset of the opening `${{` within the input source. */
30
+ readonly start: number;
31
+ /** Byte offset just past the closing `}}` within the input source. */
32
+ readonly end: number;
33
+ }
34
+
35
+ export type TemplateSegment = TemplateLiteralSegment | TemplateExpressionSegment;
36
+
37
+ const OPEN_DELIM = '${{';
38
+ const CLOSE_DELIM = '}}';
39
+
40
+ /**
41
+ * Parse a string into a sequence of literal + expression segments. Pure
42
+ * literals (no interpolation) return a single literal segment; an empty
43
+ * input returns no segments. Unbalanced delimiters fail fast with a typed
44
+ * `WorkflowDslError`.
45
+ */
46
+ export function parseTemplate(
47
+ source: string,
48
+ authoredPath: readonly string[] = [],
49
+ ): readonly TemplateSegment[] {
50
+ const segments: TemplateSegment[] = [];
51
+ let cursor = 0;
52
+
53
+ while (cursor < source.length) {
54
+ const openIdx = source.indexOf(OPEN_DELIM, cursor);
55
+ if (openIdx === -1) {
56
+ segments.push({
57
+ kind: TemplateSegmentKind.Literal,
58
+ value: source.slice(cursor),
59
+ });
60
+ return segments;
61
+ }
62
+
63
+ if (openIdx > cursor) {
64
+ segments.push({
65
+ kind: TemplateSegmentKind.Literal,
66
+ value: source.slice(cursor, openIdx),
67
+ });
68
+ }
69
+
70
+ const bodyStart = openIdx + OPEN_DELIM.length;
71
+ const closeIdx = source.indexOf(CLOSE_DELIM, bodyStart);
72
+ if (closeIdx === -1) {
73
+ throw new WorkflowDslError(
74
+ WorkflowErrorCode.DSL_EXPRESSION_INVALID,
75
+ `Unterminated template interpolation at ${pathLabel(authoredPath)} (offset ${openIdx}): missing closing '}}'.`,
76
+ { authoredPath: [...authoredPath], offset: openIdx },
77
+ );
78
+ }
79
+
80
+ const rawBody = source.slice(bodyStart, closeIdx);
81
+ const trimmed = rawBody.trim();
82
+ if (trimmed.length === 0) {
83
+ throw new WorkflowDslError(
84
+ WorkflowErrorCode.DSL_EXPRESSION_INVALID,
85
+ `Empty template interpolation at ${pathLabel(authoredPath)} (offset ${openIdx}): '\${{ }}' must contain an expression.`,
86
+ { authoredPath: [...authoredPath], offset: openIdx },
87
+ );
88
+ }
89
+
90
+ segments.push({
91
+ kind: TemplateSegmentKind.Expression,
92
+ source: trimmed,
93
+ start: openIdx,
94
+ end: closeIdx + CLOSE_DELIM.length,
95
+ });
96
+
97
+ cursor = closeIdx + CLOSE_DELIM.length;
98
+ }
99
+
100
+ return segments;
101
+ }
102
+
103
+ /**
104
+ * True iff the parsed template has at least one expression segment. Cheap
105
+ * shortcut for callers that need to distinguish "static literal" from
106
+ * "needs evaluation" without re-parsing.
107
+ */
108
+ export function templateHasExpressions(segments: readonly TemplateSegment[]): boolean {
109
+ for (const segment of segments) {
110
+ if (segment.kind === TemplateSegmentKind.Expression) return true;
111
+ }
112
+ return false;
113
+ }
114
+
115
+ function pathLabel(path: readonly string[]): string {
116
+ return path.length === 0 ? '<root>' : path.join('.');
117
+ }
@@ -0,0 +1,200 @@
1
+ import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
2
+ import { WorkflowDslError } from '../errors';
3
+ import { type Token, TokenKind } from './tokens';
4
+
5
+ /**
6
+ * Hand-rolled tokenizer for the expression subset. Deterministic: same
7
+ * input always produces the same token stream. Fails fast on any character
8
+ * outside the grammar.
9
+ *
10
+ * Grammar (surface):
11
+ * identifiers: [A-Za-z_][A-Za-z0-9_-]*
12
+ * numbers: [0-9]+(\.[0-9]+)?
13
+ * strings: '...' (single-quoted; no escapes — keep grammar simple
14
+ * and deterministic. If escapes are needed later, add once
15
+ * with full test coverage; do not retrofit ad-hoc.)
16
+ * operators: == != && || ! ( ) [ ] , .
17
+ * booleans: true | false
18
+ * null: null
19
+ * whitespace: spaces and tabs (ignored)
20
+ */
21
+ export function tokenize(source: string): readonly Token[] {
22
+ const tokens: Token[] = [];
23
+ const length = source.length;
24
+ let index = 0;
25
+
26
+ while (index < length) {
27
+ const ch = source.charAt(index);
28
+
29
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
30
+ index++;
31
+ continue;
32
+ }
33
+
34
+ const column = index + 1;
35
+
36
+ if (ch === '(') {
37
+ tokens.push({ kind: TokenKind.LPAREN, value: '(', column });
38
+ index++;
39
+ continue;
40
+ }
41
+ if (ch === ')') {
42
+ tokens.push({ kind: TokenKind.RPAREN, value: ')', column });
43
+ index++;
44
+ continue;
45
+ }
46
+ if (ch === '[') {
47
+ tokens.push({ kind: TokenKind.LBRACKET, value: '[', column });
48
+ index++;
49
+ continue;
50
+ }
51
+ if (ch === ']') {
52
+ tokens.push({ kind: TokenKind.RBRACKET, value: ']', column });
53
+ index++;
54
+ continue;
55
+ }
56
+ if (ch === ',') {
57
+ tokens.push({ kind: TokenKind.COMMA, value: ',', column });
58
+ index++;
59
+ continue;
60
+ }
61
+ if (ch === '.') {
62
+ tokens.push({ kind: TokenKind.DOT, value: '.', column });
63
+ index++;
64
+ continue;
65
+ }
66
+
67
+ if (ch === '=' && source.charAt(index + 1) === '=') {
68
+ tokens.push({ kind: TokenKind.EQ, value: '==', column });
69
+ index += 2;
70
+ continue;
71
+ }
72
+
73
+ if (ch === '!' && source.charAt(index + 1) === '=') {
74
+ tokens.push({ kind: TokenKind.NEQ, value: '!=', column });
75
+ index += 2;
76
+ continue;
77
+ }
78
+
79
+ if (ch === '!') {
80
+ tokens.push({ kind: TokenKind.BANG, value: '!', column });
81
+ index++;
82
+ continue;
83
+ }
84
+
85
+ if (ch === '&' && source.charAt(index + 1) === '&') {
86
+ tokens.push({ kind: TokenKind.AND, value: '&&', column });
87
+ index += 2;
88
+ continue;
89
+ }
90
+
91
+ if (ch === '|' && source.charAt(index + 1) === '|') {
92
+ tokens.push({ kind: TokenKind.OR, value: '||', column });
93
+ index += 2;
94
+ continue;
95
+ }
96
+
97
+ if (ch === "'") {
98
+ const { value, next } = readString(source, index);
99
+ tokens.push({ kind: TokenKind.STRING, value, column });
100
+ index = next;
101
+ continue;
102
+ }
103
+
104
+ if (isDigit(ch)) {
105
+ const { value, next } = readNumber(source, index);
106
+ tokens.push({ kind: TokenKind.NUMBER, value, column });
107
+ index = next;
108
+ continue;
109
+ }
110
+
111
+ if (isIdentStart(ch)) {
112
+ const { value, next } = readIdentifier(source, index);
113
+ if (value === 'true' || value === 'false') {
114
+ tokens.push({ kind: TokenKind.BOOLEAN, value, column });
115
+ } else if (value === 'null') {
116
+ tokens.push({ kind: TokenKind.NULL, value, column });
117
+ } else {
118
+ tokens.push({ kind: TokenKind.IDENTIFIER, value, column });
119
+ }
120
+ index = next;
121
+ continue;
122
+ }
123
+
124
+ throw new WorkflowDslError(
125
+ WorkflowErrorCode.DSL_EXPRESSION_INVALID,
126
+ `Unexpected character '${ch}' at column ${column}`,
127
+ { source, column },
128
+ );
129
+ }
130
+
131
+ tokens.push({ kind: TokenKind.EOF, value: '', column: length + 1 });
132
+ return tokens;
133
+ }
134
+
135
+ function readString(source: string, start: number): { value: string; next: number } {
136
+ const length = source.length;
137
+ let end = start + 1;
138
+ while (end < length && source.charAt(end) !== "'") {
139
+ if (source.charAt(end) === '\\') {
140
+ throw new WorkflowDslError(
141
+ WorkflowErrorCode.DSL_EXPRESSION_INVALID,
142
+ `String escapes are not supported in expression literals (column ${end + 1}).`,
143
+ { source, column: end + 1 },
144
+ );
145
+ }
146
+ end++;
147
+ }
148
+ if (end >= length) {
149
+ throw new WorkflowDslError(
150
+ WorkflowErrorCode.DSL_EXPRESSION_INVALID,
151
+ `Unterminated string literal starting at column ${start + 1}`,
152
+ { source, column: start + 1 },
153
+ );
154
+ }
155
+ return { value: source.slice(start + 1, end), next: end + 1 };
156
+ }
157
+
158
+ function readNumber(source: string, start: number): { value: string; next: number } {
159
+ const length = source.length;
160
+ let end = start;
161
+ while (end < length && isDigit(source.charAt(end))) {
162
+ end++;
163
+ }
164
+ if (end < length && source.charAt(end) === '.') {
165
+ end++;
166
+ const fractionStart = end;
167
+ while (end < length && isDigit(source.charAt(end))) {
168
+ end++;
169
+ }
170
+ if (end === fractionStart) {
171
+ throw new WorkflowDslError(
172
+ WorkflowErrorCode.DSL_EXPRESSION_INVALID,
173
+ `Number literal with trailing dot at column ${start + 1}`,
174
+ { source, column: start + 1 },
175
+ );
176
+ }
177
+ }
178
+ return { value: source.slice(start, end), next: end };
179
+ }
180
+
181
+ function readIdentifier(source: string, start: number): { value: string; next: number } {
182
+ const length = source.length;
183
+ let end = start;
184
+ while (end < length && isIdentPart(source.charAt(end))) {
185
+ end++;
186
+ }
187
+ return { value: source.slice(start, end), next: end };
188
+ }
189
+
190
+ function isDigit(ch: string): boolean {
191
+ return ch >= '0' && ch <= '9';
192
+ }
193
+
194
+ function isIdentStart(ch: string): boolean {
195
+ return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch === '_';
196
+ }
197
+
198
+ function isIdentPart(ch: string): boolean {
199
+ return isIdentStart(ch) || isDigit(ch) || ch === '-';
200
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Token kinds for the Xema expression subset. Closed set; every token
3
+ * produced by the tokenizer picks one of these. Unknown input is rejected.
4
+ */
5
+ export enum TokenKind {
6
+ IDENTIFIER = 'identifier',
7
+ NUMBER = 'number',
8
+ STRING = 'string',
9
+ BOOLEAN = 'boolean',
10
+ NULL = 'null',
11
+ LPAREN = '(',
12
+ RPAREN = ')',
13
+ LBRACKET = '[',
14
+ RBRACKET = ']',
15
+ COMMA = ',',
16
+ DOT = '.',
17
+ BANG = '!',
18
+ EQ = '==',
19
+ NEQ = '!=',
20
+ AND = '&&',
21
+ OR = '||',
22
+ EOF = 'eof',
23
+ }
24
+
25
+ export interface Token {
26
+ readonly kind: TokenKind;
27
+ readonly value: string;
28
+ /** 1-based column in the source expression for diagnostic output. */
29
+ readonly column: number;
30
+ }