@xemahq/dsl 0.8.0 → 0.8.2

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 (64) hide show
  1. package/dist/payload-codec/lib/blob-store.js +0 -0
  2. package/dist/payload-codec/lib/blob-store.js.map +1 -1
  3. package/package.json +3 -4
  4. package/src/deliverable-spec/index.ts +0 -19
  5. package/src/deliverable-spec/lib/schema.ts +0 -270
  6. package/src/deliverable-spec/lib/types.ts +0 -26
  7. package/src/payload-codec/index.ts +0 -44
  8. package/src/payload-codec/lib/blob-store.ts +0 -0
  9. package/src/payload-codec/lib/codec-context.ts +0 -38
  10. package/src/payload-codec/lib/codec.ts +0 -605
  11. package/src/payload-codec/lib/enums.ts +0 -58
  12. package/src/payload-codec/lib/errors.ts +0 -54
  13. package/src/payload-codec/lib/http-blob-store.ts +0 -267
  14. package/src/payload-codec/lib/lru-cache.ts +0 -81
  15. package/src/payload-codec/lib/payload.ts +0 -26
  16. package/src/payload-codec/temporal/index.ts +0 -36
  17. package/src/workflow/index.ts +0 -108
  18. package/src/workflow/lib/action-input-validator.ts +0 -160
  19. package/src/workflow/lib/compiler/action-shape.ts +0 -71
  20. package/src/workflow/lib/compiler/canonical-json.ts +0 -66
  21. package/src/workflow/lib/compiler/compile.ts +0 -1742
  22. package/src/workflow/lib/compiler/concurrency.ts +0 -223
  23. package/src/workflow/lib/compiler/dag.ts +0 -108
  24. package/src/workflow/lib/compiler/gate-defaults.ts +0 -153
  25. package/src/workflow/lib/compiler/index.ts +0 -11
  26. package/src/workflow/lib/compiler/inputs.ts +0 -254
  27. package/src/workflow/lib/compiler/installation-resource-validator.ts +0 -114
  28. package/src/workflow/lib/compiler/manifest-source.ts +0 -71
  29. package/src/workflow/lib/compiler/matrix.ts +0 -135
  30. package/src/workflow/lib/compiler/mount-plan.ts +0 -190
  31. package/src/workflow/lib/compiler/payload-reach-in.ts +0 -497
  32. package/src/workflow/lib/compiler/permissions.ts +0 -64
  33. package/src/workflow/lib/compiler/retry-timeout.ts +0 -105
  34. package/src/workflow/lib/compiler/review-step.ts +0 -548
  35. package/src/workflow/lib/compiler/types.ts +0 -172
  36. package/src/workflow/lib/compiler/variable-requirements.ts +0 -208
  37. package/src/workflow/lib/deliverable-spec-introspection-error.ts +0 -63
  38. package/src/workflow/lib/deliverable-spec-keys.ts +0 -147
  39. package/src/workflow/lib/deliverable-spec-source-scan.ts +0 -280
  40. package/src/workflow/lib/dispatch-inputs/index.ts +0 -160
  41. package/src/workflow/lib/dispatch-inputs/to-json-schema.ts +0 -60
  42. package/src/workflow/lib/duration.ts +0 -43
  43. package/src/workflow/lib/errors.ts +0 -37
  44. package/src/workflow/lib/expression/ast.ts +0 -108
  45. package/src/workflow/lib/expression/context.ts +0 -148
  46. package/src/workflow/lib/expression/evaluator.ts +0 -492
  47. package/src/workflow/lib/expression/index.ts +0 -28
  48. package/src/workflow/lib/expression/interpolation.ts +0 -84
  49. package/src/workflow/lib/expression/parser.ts +0 -264
  50. package/src/workflow/lib/expression/template.ts +0 -117
  51. package/src/workflow/lib/expression/tokenizer.ts +0 -200
  52. package/src/workflow/lib/expression/tokens.ts +0 -30
  53. package/src/workflow/lib/expression/walk-artifact-refs.ts +0 -232
  54. package/src/workflow/lib/installation-resource-kind.ts +0 -107
  55. package/src/workflow/lib/schemas-loader.ts +0 -64
  56. package/src/workflow/lib/serializer.ts +0 -30
  57. package/src/workflow/lib/types.ts +0 -417
  58. package/src/workflow/lib/validate.ts +0 -199
  59. package/src/workspace-manifest/index.ts +0 -27
  60. package/src/workspace-manifest/lib/compile.ts +0 -619
  61. package/src/workspace-manifest/lib/interpolate.ts +0 -166
  62. package/src/workspace-manifest/lib/resolve-extends.ts +0 -260
  63. package/src/workspace-manifest/lib/schema.ts +0 -692
  64. package/src/workspace-manifest/lib/types.ts +0 -446
@@ -1,148 +0,0 @@
1
- import type { TriggerPayload } from '@xemahq/kernel-contracts/workflow';
2
-
3
- /**
4
- * Per-run, read-only cache of pre-fetched artifact payloads keyed by
5
- * `versionId`. Populated by the workflow runtime BEFORE each step's
6
- * expressions are evaluated, which owns the fetch + cache plumbing.
7
- * The evaluator only ever reads from this map;
8
- * concurrent reads are safe by construction (no writes once frozen for a
9
- * step). Empty when no expression in scope reaches into a typed
10
- * payload or invokes `fromJSON(ref)`.
11
- *
12
- * Values are the parsed payload (any JSON-shaped value). The runtime is
13
- * responsible for parse-time validation; the evaluator surfaces a
14
- * missing-key as `DSL_PAYLOAD_PARSE_ERROR` so authors get a clear
15
- * "this ref was never resolved" signal rather than a generic null
16
- * dereference.
17
- */
18
- export type PayloadCache = ReadonlyMap<string, unknown>;
19
-
20
- /**
21
- * Runtime binding context passed to the evaluator. Every entry is a plain
22
- * JSON value — no closures, no functions — so evaluation is pure and
23
- * deterministic for any given input.
24
- */
25
- export interface ExpressionContext {
26
- readonly needs: Readonly<Record<string, NeedsEntry>>;
27
- readonly matrix: Readonly<Record<string, unknown>>;
28
- readonly inputs: Readonly<Record<string, unknown>>;
29
- readonly vars: Readonly<Record<string, unknown>>;
30
- /**
31
- * Project / org secret variables, populated at dispatch from
32
- * `requires.variables[*].secret: true` entries. Always present; empty
33
- * when the workflow declares no secret requirements.
34
- */
35
- readonly secrets: Readonly<Record<string, string>>;
36
- /**
37
- * The trigger that started this run. Readers access via `trigger.kind`,
38
- * `trigger.inputs`, etc. as defined by {@link TriggerPayload}.
39
- */
40
- readonly trigger: TriggerPayload;
41
- /**
42
- * Outputs of the current job (if an expression is evaluated at job-level,
43
- * e.g. inside `job.outputs`). Empty when evaluating matrix/needs/if.
44
- */
45
- readonly job: { readonly outputs: Readonly<Record<string, unknown>> };
46
- /**
47
- * System-managed canonical context for the run — see {@link XemaContext}.
48
- * Always present. Authors reach for `${{ xema.project.id }}` instead of
49
- * `${{ trigger.projectId }}` because project id is run-level state, not
50
- * trigger metadata.
51
- */
52
- readonly xema: XemaContext;
53
- /**
54
- * Pre-fetched artifact payloads for the current step, keyed by
55
- * `ArtifactRef.versionId`. Used by `fromJSON(ref)` to return the
56
- * already-parsed payload without a runtime fetch. Empty map when the
57
- * step has no payload-reaching expressions. Always present so the
58
- * evaluator can distinguish "ref not resolved" (missing key →
59
- * `DSL_PAYLOAD_PARSE_ERROR`) from "no cache at all".
60
- */
61
- readonly payloadCache: PayloadCache;
62
- }
63
-
64
- export interface NeedsEntry {
65
- readonly outputs: Readonly<Record<string, unknown>>;
66
- }
67
-
68
- /**
69
- * Canonical system-managed context. Modelled after GitHub Actions'
70
- * `github.*` so authors reach for an obvious, stable namespace rather
71
- * than digging into `trigger.*` metadata for run-level state.
72
- *
73
- * Every nested record is a plain JSON object — evaluator-safe.
74
- */
75
- export interface XemaContext {
76
- readonly org: { readonly id: string };
77
- readonly project: { readonly id: string };
78
- readonly run: {
79
- readonly id: string;
80
- readonly attempt: number;
81
- /** ISO-8601 wall-clock at trigger dispatch (mirrors `trigger.triggeredAt`). */
82
- readonly startedAtIso: string;
83
- readonly correlationId: string;
84
- readonly actor: XemaActor;
85
- };
86
- readonly workflow: {
87
- readonly key: string;
88
- readonly revisionNumber: number;
89
- };
90
- readonly job: {
91
- readonly key: string;
92
- readonly attempt: number;
93
- };
94
- }
95
-
96
- export interface XemaActor {
97
- /** Subject id of the user who initiated the run; null for scheduled / webhook triggers. */
98
- readonly subject: string | null;
99
- readonly kind: XemaActorKind;
100
- }
101
-
102
- /**
103
- * Closed set of run-actor kinds. `user` is set whenever a subject id is
104
- * present; `webhook` / `schedule` come from the trigger discriminator
105
- * for unattended runs; `system` is the fallback for engine-initiated
106
- * dispatches with no actor (e.g. internal recovery).
107
- */
108
- export enum XemaActorKind {
109
- User = 'user',
110
- System = 'system',
111
- Webhook = 'webhook',
112
- Schedule = 'schedule',
113
- }
114
-
115
- /** Empty-but-typed seeds for contexts where an axis doesn't apply yet. */
116
- export const EMPTY_CONTEXT_SEEDS = {
117
- needs: Object.freeze({}) as Readonly<Record<string, NeedsEntry>>,
118
- matrix: Object.freeze({}) as Readonly<Record<string, unknown>>,
119
- vars: Object.freeze({}) as Readonly<Record<string, unknown>>,
120
- secrets: Object.freeze({}) as Readonly<Record<string, string>>,
121
- job: Object.freeze({ outputs: Object.freeze({}) }) as {
122
- readonly outputs: Readonly<Record<string, unknown>>;
123
- },
124
- /**
125
- * Empty payload cache for compile-time / static contexts where no
126
- * artifact resolution has been performed. The runtime replaces this
127
- * with the per-step populated map.
128
- */
129
- payloadCache: new Map<string, unknown>() as PayloadCache,
130
- /**
131
- * Seed `xema` for compile-time evaluation contexts (concurrency,
132
- * static workflow checks) where the canonical fields aren't available
133
- * yet. Runtime callers always replace this with the populated shape.
134
- */
135
- xema: Object.freeze({
136
- org: Object.freeze({ id: '' }),
137
- project: Object.freeze({ id: '' }),
138
- run: Object.freeze({
139
- id: '',
140
- attempt: 0,
141
- startedAtIso: '',
142
- correlationId: '',
143
- actor: Object.freeze({ subject: null, kind: XemaActorKind.System }),
144
- }),
145
- workflow: Object.freeze({ key: '', revisionNumber: 0 }),
146
- job: Object.freeze({ key: '', attempt: 0 }),
147
- }) as XemaContext,
148
- } as const;
@@ -1,492 +0,0 @@
1
- import {
2
- isArtifactRef,
3
- isArtifactRefEnvelopeField,
4
- WorkflowErrorCode,
5
- type ArtifactRef,
6
- } from '@xemahq/kernel-contracts/workflow';
7
- import { WorkflowDslError } from '../errors';
8
- import {
9
- type BinaryNode,
10
- type CallNode,
11
- type ExpressionNode,
12
- ExpressionFunction,
13
- ExpressionNodeKind,
14
- ExpressionRoot,
15
- type IdentifierNode,
16
- type IndexNode,
17
- type LiteralNode,
18
- type MemberNode,
19
- } from './ast';
20
- import type { ExpressionContext } from './context';
21
- import { parseExpression } from './parser';
22
- import { tokenize } from './tokenizer';
23
-
24
- /**
25
- * Compile a source expression into an {@link ExpressionNode} tree. Used by
26
- * the workflow compiler to pre-validate every expression at compile time
27
- * (so runtime evaluation can only fail on unknown bindings, not on
28
- * syntax). Reject unknown identifiers, roots, and function calls.
29
- */
30
- export function compileExpression(source: string): ExpressionNode {
31
- if (source.length === 0) {
32
- throw new WorkflowDslError(
33
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
34
- 'Expression source must not be empty',
35
- { source },
36
- );
37
- }
38
- const tokens = tokenize(source);
39
- return parseExpression(source, tokens);
40
- }
41
-
42
- /**
43
- * Evaluate a pre-compiled {@link ExpressionNode} against a bound context.
44
- * Deterministic: same (node, context) always produces the same value, no
45
- * implicit time/random/env access, no silent coercion.
46
- *
47
- * Throws {@link WorkflowDslError} with code DSL_EXPRESSION_INVALID on any
48
- * unknown property lookup or type mismatch — the caller must surface the
49
- * error; silent substitution is forbidden.
50
- */
51
- export function evaluateExpression(node: ExpressionNode, ctx: ExpressionContext): unknown {
52
- switch (node.kind) {
53
- case ExpressionNodeKind.LITERAL:
54
- return evalLiteral(node);
55
- case ExpressionNodeKind.IDENTIFIER:
56
- return evalIdentifier(node, ctx);
57
- case ExpressionNodeKind.MEMBER:
58
- return evalMember(node, ctx);
59
- case ExpressionNodeKind.INDEX:
60
- return evalIndex(node, ctx);
61
- case ExpressionNodeKind.CALL:
62
- return evalCall(node, ctx);
63
- case ExpressionNodeKind.UNARY_NOT: {
64
- const operand = evaluateExpression(node.operand, ctx);
65
- return !toBool(operand);
66
- }
67
- case ExpressionNodeKind.BINARY_EQ:
68
- case ExpressionNodeKind.BINARY_NEQ:
69
- case ExpressionNodeKind.BINARY_AND:
70
- case ExpressionNodeKind.BINARY_OR:
71
- return evalBinary(node, ctx);
72
- }
73
- }
74
-
75
- function evalLiteral(node: LiteralNode): unknown {
76
- return node.value;
77
- }
78
-
79
- function evalIdentifier(node: IdentifierNode, ctx: ExpressionContext): unknown {
80
- switch (node.name as ExpressionRoot) {
81
- case ExpressionRoot.NEEDS:
82
- return ctx.needs;
83
- case ExpressionRoot.MATRIX:
84
- return ctx.matrix;
85
- case ExpressionRoot.INPUTS:
86
- return ctx.inputs;
87
- case ExpressionRoot.VARS:
88
- return ctx.vars;
89
- case ExpressionRoot.SECRETS:
90
- return ctx.secrets;
91
- case ExpressionRoot.TRIGGER:
92
- return ctx.trigger;
93
- case ExpressionRoot.JOB:
94
- return ctx.job;
95
- case ExpressionRoot.XEMA:
96
- return ctx.xema;
97
- default:
98
- throw new WorkflowDslError(
99
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
100
- `Unknown identifier root '${node.name}'`,
101
- { name: node.name },
102
- );
103
- }
104
- }
105
-
106
- function evalMember(node: MemberNode, ctx: ExpressionContext): unknown {
107
- const target = evaluateExpression(node.target, ctx);
108
- return readPropertyWithPayloadCache(target, node.property, ctx);
109
- }
110
-
111
- function evalIndex(node: IndexNode, ctx: ExpressionContext): unknown {
112
- const target = evaluateExpression(node.target, ctx);
113
- const key = evaluateExpression(node.index, ctx);
114
-
115
- if (Array.isArray(target)) {
116
- if (typeof key !== 'number' || !Number.isInteger(key)) {
117
- throw new WorkflowDslError(
118
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
119
- `Array index must be an integer, received ${typeof key}`,
120
- { key, targetKind: 'array' },
121
- );
122
- }
123
- if (key < 0 || key >= target.length) {
124
- throw new WorkflowDslError(
125
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
126
- `Array index out of range: ${key} (length ${target.length})`,
127
- { key, length: target.length },
128
- );
129
- }
130
- return target[key];
131
- }
132
-
133
- if (typeof target === 'object' && target !== null) {
134
- if (typeof key !== 'string') {
135
- throw new WorkflowDslError(
136
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
137
- `Object index must be a string, received ${typeof key}`,
138
- { key, targetKind: 'object' },
139
- );
140
- }
141
- return readPropertyWithPayloadCache(target, key, ctx);
142
- }
143
-
144
- throw new WorkflowDslError(
145
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
146
- `Indexing is only supported on arrays or objects. Target kind: ${typeof target}`,
147
- { targetKind: typeof target },
148
- );
149
- }
150
-
151
- function evalCall(node: CallNode, ctx: ExpressionContext): unknown {
152
- switch (node.callee) {
153
- case ExpressionFunction.FROM_JSON:
154
- return evalFromJson(node, ctx);
155
- case ExpressionFunction.TO_JSON:
156
- return evalToJson(node, ctx);
157
- case ExpressionFunction.FORMAT:
158
- return evalFormat(node, ctx);
159
- }
160
- }
161
-
162
- /**
163
- * `fromJSON` accepts two argument shapes:
164
- *
165
- * 1. `string` — JSON-parse the string (matches GitHub Actions semantics).
166
- * Parse failure → `DSL_EXPRESSION_INVALID` (the source is authored
167
- * literal text; we treat it as a syntax-level error).
168
- * 2. `ArtifactRef` — look up the pre-fetched parsed payload in the
169
- * `payloadCache` (keyed on `versionId`). Missing entry →
170
- * `DSL_PAYLOAD_PARSE_ERROR`: the runtime is responsible for
171
- * pre-resolving every ref referenced by an expression before the
172
- * evaluator runs. A cache miss is a runtime infra bug, not an
173
- * author bug, so it carries a runtime-coded error.
174
- *
175
- * This is the untyped escape hatch. Authors who want compile-time field
176
- * validation use the typed reach-in (`outputs.foo.field`).
177
- */
178
- function evalFromJson(node: CallNode, ctx: ExpressionContext): unknown {
179
- const argNode = singleArg(node, 'fromJSON');
180
- const arg = evaluateExpression(argNode, ctx);
181
- if (typeof arg === 'string') {
182
- try {
183
- return JSON.parse(arg);
184
- } catch (err) {
185
- throw new WorkflowDslError(
186
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
187
- `fromJSON failed to parse argument as JSON: ${(err as Error).message}`,
188
- { callee: node.callee },
189
- );
190
- }
191
- }
192
- if (isArtifactRef(arg)) {
193
- const cached = ctx.payloadCache.get(arg.versionId);
194
- if (cached === undefined) {
195
- throw new WorkflowDslError(
196
- WorkflowErrorCode.DSL_PAYLOAD_PARSE_ERROR,
197
- `fromJSON(<ArtifactRef>) called against versionId='${arg.versionId}' but the runtime did not pre-resolve its payload into the per-step cache. This is a runtime infra failure — the worker is expected to fetch and parse every referenced artifact before evaluating expressions.`,
198
- {
199
- callee: node.callee,
200
- artifactId: arg.artifactId,
201
- versionId: arg.versionId,
202
- type: arg.type,
203
- },
204
- );
205
- }
206
- return cached;
207
- }
208
- throw new WorkflowDslError(
209
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
210
- `fromJSON requires a string or ArtifactRef argument, received ${typeof arg}`,
211
- { callee: node.callee, argKind: typeof arg },
212
- );
213
- }
214
-
215
- function evalToJson(node: CallNode, ctx: ExpressionContext): string {
216
- const argNode = singleArg(node, 'toJSON');
217
- return JSON.stringify(evaluateExpression(argNode, ctx));
218
- }
219
-
220
- function evalFormat(node: CallNode, ctx: ExpressionContext): string {
221
- if (node.args.length < 1) {
222
- throw new WorkflowDslError(
223
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
224
- `format expects at least one argument (the template string)`,
225
- { callee: node.callee, arity: node.args.length },
226
- );
227
- }
228
- const [templateNode, ...argNodes] = node.args;
229
- if (!templateNode) {
230
- throw new WorkflowDslError(
231
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
232
- `format template is missing from AST`,
233
- { callee: node.callee },
234
- );
235
- }
236
- const template = evaluateExpression(templateNode, ctx);
237
- if (typeof template !== 'string') {
238
- throw new WorkflowDslError(
239
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
240
- `format template must be a string, received ${typeof template}`,
241
- { callee: node.callee, templateKind: typeof template },
242
- );
243
- }
244
- const args = argNodes.map((argNode) => evaluateExpression(argNode, ctx));
245
- return formatTemplate(template, args);
246
- }
247
-
248
- function singleArg(node: CallNode, fnName: string): ExpressionNode {
249
- if (node.args.length !== 1) {
250
- throw new WorkflowDslError(
251
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
252
- `${fnName} expects exactly one argument, received ${node.args.length}`,
253
- { callee: node.callee, arity: node.args.length },
254
- );
255
- }
256
- const [argNode] = node.args;
257
- if (!argNode) {
258
- throw new WorkflowDslError(
259
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
260
- `${fnName} argument is missing from AST`,
261
- { callee: node.callee },
262
- );
263
- }
264
- return argNode;
265
- }
266
-
267
- /**
268
- * Replace `{N}` placeholders in `template` with the stringified arg at
269
- * index N. Escape `{{` / `}}` for literal braces. Out-of-range indices
270
- * fail fast — silent emptiness would be a footgun for authors.
271
- */
272
- function formatTemplate(template: string, args: readonly unknown[]): string {
273
- let out = '';
274
- let i = 0;
275
- while (i < template.length) {
276
- const ch = template[i];
277
- if (ch === '{') {
278
- const advance = consumeOpenBrace(template, i, args);
279
- out += advance.text;
280
- i = advance.next;
281
- continue;
282
- }
283
- if (ch === '}') {
284
- const advance = consumeCloseBrace(template, i);
285
- out += advance.text;
286
- i = advance.next;
287
- continue;
288
- }
289
- out += ch;
290
- i++;
291
- }
292
- return out;
293
- }
294
-
295
- interface FormatAdvance {
296
- readonly text: string;
297
- readonly next: number;
298
- }
299
-
300
- function consumeOpenBrace(
301
- template: string,
302
- i: number,
303
- args: readonly unknown[],
304
- ): FormatAdvance {
305
- if (template[i + 1] === '{') {
306
- return { text: '{', next: i + 2 };
307
- }
308
- const close = template.indexOf('}', i + 1);
309
- if (close === -1) {
310
- throw new WorkflowDslError(
311
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
312
- `format template has unclosed '{' starting at index ${i}: ${template}`,
313
- { template },
314
- );
315
- }
316
- const indexLiteral = template.slice(i + 1, close);
317
- const idx = parseFormatIndex(template, indexLiteral, args.length);
318
- return { text: stringifyFormatArg(args[idx]), next: close + 1 };
319
- }
320
-
321
- function consumeCloseBrace(template: string, i: number): FormatAdvance {
322
- if (template[i + 1] === '}') {
323
- return { text: '}', next: i + 2 };
324
- }
325
- throw new WorkflowDslError(
326
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
327
- `format template has unmatched '}' at index ${i}: ${template}`,
328
- { template },
329
- );
330
- }
331
-
332
- function parseFormatIndex(template: string, indexLiteral: string, argCount: number): number {
333
- if (!/^\d+$/.test(indexLiteral)) {
334
- throw new WorkflowDslError(
335
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
336
- `format placeholder '{${indexLiteral}}' is not a non-negative integer`,
337
- { template, placeholder: indexLiteral },
338
- );
339
- }
340
- const idx = Number(indexLiteral);
341
- if (idx >= argCount) {
342
- throw new WorkflowDslError(
343
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
344
- `format placeholder '{${idx}}' has no matching argument (received ${argCount} arg(s))`,
345
- { template, placeholder: idx, argCount },
346
- );
347
- }
348
- return idx;
349
- }
350
-
351
- function stringifyFormatArg(value: unknown): string {
352
- if (typeof value === 'string') return value;
353
- if (typeof value === 'number' || typeof value === 'boolean') return String(value);
354
- if (value === null) return 'null';
355
- if (value === undefined) {
356
- throw new WorkflowDslError(
357
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
358
- `format argument resolved to undefined; expressions must produce a defined value`,
359
- {},
360
- );
361
- }
362
- // Objects + arrays serialize as JSON so authors can build deterministic
363
- // strings without risk of `[object Object]` slipping through.
364
- return JSON.stringify(value);
365
- }
366
-
367
- function evalBinary(node: BinaryNode, ctx: ExpressionContext): unknown {
368
- switch (node.kind) {
369
- case ExpressionNodeKind.BINARY_EQ: {
370
- const left = evaluateExpression(node.left, ctx);
371
- const right = evaluateExpression(node.right, ctx);
372
- return strictEquals(left, right);
373
- }
374
- case ExpressionNodeKind.BINARY_NEQ: {
375
- const left = evaluateExpression(node.left, ctx);
376
- const right = evaluateExpression(node.right, ctx);
377
- return !strictEquals(left, right);
378
- }
379
- case ExpressionNodeKind.BINARY_AND: {
380
- const left = evaluateExpression(node.left, ctx);
381
- if (!toBool(left)) return false;
382
- return toBool(evaluateExpression(node.right, ctx));
383
- }
384
- case ExpressionNodeKind.BINARY_OR: {
385
- const left = evaluateExpression(node.left, ctx);
386
- if (toBool(left)) return true;
387
- return toBool(evaluateExpression(node.right, ctx));
388
- }
389
- }
390
- }
391
-
392
- /**
393
- * Property-read wrapper that transparently redirects payload reach-in
394
- * (`outputs.foo.<payload-field>`) through the per-step `payloadCache`.
395
- *
396
- * When `target` is an `ArtifactRef`:
397
- * • envelope-field property (artifactId/versionId/version/hash/type/title)
398
- * → read directly off the ref (no fetch).
399
- * • any other property → look up the artifact's parsed payload in
400
- * `ctx.payloadCache` (populated by the worker's reach-in pre-fetch)
401
- * and read the field off that. Cache miss surfaces as
402
- * `DSL_PAYLOAD_PARSE_ERROR` so the author sees a clean DSL error
403
- * instead of a misleading "Unknown property" on the ref.
404
- *
405
- * Anything else delegates to the plain `readProperty` path.
406
- */
407
- function readPropertyWithPayloadCache(
408
- target: unknown,
409
- property: string,
410
- ctx: ExpressionContext,
411
- ): unknown {
412
- if (isArtifactRef(target) && !isArtifactRefEnvelopeField(property)) {
413
- return readPayloadField(target, property, ctx);
414
- }
415
- return readProperty(target, property);
416
- }
417
-
418
- function readPayloadField(
419
- ref: ArtifactRef,
420
- property: string,
421
- ctx: ExpressionContext,
422
- ): unknown {
423
- if (!ctx.payloadCache.has(ref.versionId)) {
424
- throw new WorkflowDslError(
425
- WorkflowErrorCode.DSL_PAYLOAD_PARSE_ERROR,
426
- `Payload reach-in '${property}' on artifact '${ref.artifactId}' v${ref.version} ` +
427
- `(versionId '${ref.versionId}') is not available — the per-step payloadCache ` +
428
- `has no entry for this version. The runtime pre-fetch should have populated ` +
429
- `the cache before this expression evaluated; this typically means the ref ` +
430
- `surfaced from an indirection the pre-fetch walker can't trace (e.g. a ` +
431
- `dynamically computed ref). Use 'fromJSON(<ref>)' as the documented ` +
432
- `escape hatch when the runtime cannot statically pre-fetch.`,
433
- { artifactId: ref.artifactId, versionId: ref.versionId, version: ref.version, property },
434
- );
435
- }
436
- const payload = ctx.payloadCache.get(ref.versionId);
437
- if (payload === null || payload === undefined) {
438
- throw new WorkflowDslError(
439
- WorkflowErrorCode.DSL_PAYLOAD_PARSE_ERROR,
440
- `Payload reach-in '${property}' on artifact '${ref.artifactId}' v${ref.version} ` +
441
- `failed: cached payload is ${payload === null ? 'null' : 'undefined'}.`,
442
- { artifactId: ref.artifactId, versionId: ref.versionId, property },
443
- );
444
- }
445
- return readProperty(payload, property);
446
- }
447
-
448
- function readProperty(target: unknown, property: string): unknown {
449
- if (target === null || target === undefined) {
450
- throw new WorkflowDslError(
451
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
452
- `Cannot read property '${property}' of ${target === null ? 'null' : 'undefined'}`,
453
- { property },
454
- );
455
- }
456
- if (typeof target !== 'object' || Array.isArray(target)) {
457
- throw new WorkflowDslError(
458
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
459
- `Member access '${property}' is not supported on ${Array.isArray(target) ? 'arrays' : typeof target}`,
460
- { property, targetKind: Array.isArray(target) ? 'array' : typeof target },
461
- );
462
- }
463
- const record = target as Record<string, unknown>;
464
- if (!Object.prototype.hasOwnProperty.call(record, property)) {
465
- throw new WorkflowDslError(
466
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
467
- `Unknown property '${property}'. Available keys: ${Object.keys(record).sort().join(', ') || '(none)'}.`,
468
- { property, availableKeys: Object.keys(record) },
469
- );
470
- }
471
- return record[property];
472
- }
473
-
474
- function toBool(value: unknown): boolean {
475
- // Deterministic boolean coercion — closed set of accepted types. Anything
476
- // else is a typed error. No JS truthiness footguns in expressions.
477
- if (typeof value === 'boolean') return value;
478
- if (value === null) return false;
479
- throw new WorkflowDslError(
480
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
481
- `Expected boolean, received ${typeof value}. Xema expressions do not implicitly coerce truthiness.`,
482
- { valueKind: value === null ? 'null' : typeof value },
483
- );
484
- }
485
-
486
- function strictEquals(a: unknown, b: unknown): boolean {
487
- // Primitive equality only. Arrays/objects fall through to reference
488
- // equality — expression authors should compare primitives.
489
- if (typeof a !== typeof b) return false;
490
- if (a === b) return true;
491
- return false;
492
- }
@@ -1,28 +0,0 @@
1
- export {
2
- type ExpressionContext,
3
- type NeedsEntry,
4
- type PayloadCache,
5
- type XemaActor,
6
- type XemaContext,
7
- XemaActorKind,
8
- EMPTY_CONTEXT_SEEDS,
9
- } from './context';
10
- export { compileExpression, evaluateExpression } from './evaluator';
11
- export { ExpressionFunction, ExpressionNodeKind, ExpressionRoot } from './ast';
12
- export type { ExpressionNode } from './ast';
13
- export { extractInterpolations, isInterpolationWrapped, stripInterpolation } from './interpolation';
14
- export {
15
- extractUpstreamJobKey,
16
- extractUpstreamOutputName,
17
- walkRuntimeArtifactRefTargets,
18
- } from './walk-artifact-refs';
19
- export {
20
- parseTemplate,
21
- templateHasExpressions,
22
- TemplateSegmentKind,
23
- } from './template';
24
- export type {
25
- TemplateExpressionSegment,
26
- TemplateLiteralSegment,
27
- TemplateSegment,
28
- } from './template';