@xemahq/dsl 0.8.1 → 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 (62) hide show
  1. package/package.json +2 -3
  2. package/src/deliverable-spec/index.ts +0 -19
  3. package/src/deliverable-spec/lib/schema.ts +0 -270
  4. package/src/deliverable-spec/lib/types.ts +0 -26
  5. package/src/payload-codec/index.ts +0 -44
  6. package/src/payload-codec/lib/blob-store.ts +0 -176
  7. package/src/payload-codec/lib/codec-context.ts +0 -38
  8. package/src/payload-codec/lib/codec.ts +0 -605
  9. package/src/payload-codec/lib/enums.ts +0 -58
  10. package/src/payload-codec/lib/errors.ts +0 -54
  11. package/src/payload-codec/lib/http-blob-store.ts +0 -267
  12. package/src/payload-codec/lib/lru-cache.ts +0 -81
  13. package/src/payload-codec/lib/payload.ts +0 -26
  14. package/src/payload-codec/temporal/index.ts +0 -36
  15. package/src/workflow/index.ts +0 -108
  16. package/src/workflow/lib/action-input-validator.ts +0 -160
  17. package/src/workflow/lib/compiler/action-shape.ts +0 -71
  18. package/src/workflow/lib/compiler/canonical-json.ts +0 -66
  19. package/src/workflow/lib/compiler/compile.ts +0 -1742
  20. package/src/workflow/lib/compiler/concurrency.ts +0 -223
  21. package/src/workflow/lib/compiler/dag.ts +0 -108
  22. package/src/workflow/lib/compiler/gate-defaults.ts +0 -153
  23. package/src/workflow/lib/compiler/index.ts +0 -11
  24. package/src/workflow/lib/compiler/inputs.ts +0 -254
  25. package/src/workflow/lib/compiler/installation-resource-validator.ts +0 -114
  26. package/src/workflow/lib/compiler/manifest-source.ts +0 -71
  27. package/src/workflow/lib/compiler/matrix.ts +0 -135
  28. package/src/workflow/lib/compiler/mount-plan.ts +0 -190
  29. package/src/workflow/lib/compiler/payload-reach-in.ts +0 -497
  30. package/src/workflow/lib/compiler/permissions.ts +0 -64
  31. package/src/workflow/lib/compiler/retry-timeout.ts +0 -105
  32. package/src/workflow/lib/compiler/review-step.ts +0 -548
  33. package/src/workflow/lib/compiler/types.ts +0 -172
  34. package/src/workflow/lib/compiler/variable-requirements.ts +0 -208
  35. package/src/workflow/lib/deliverable-spec-introspection-error.ts +0 -63
  36. package/src/workflow/lib/deliverable-spec-keys.ts +0 -147
  37. package/src/workflow/lib/deliverable-spec-source-scan.ts +0 -280
  38. package/src/workflow/lib/dispatch-inputs/index.ts +0 -160
  39. package/src/workflow/lib/dispatch-inputs/to-json-schema.ts +0 -60
  40. package/src/workflow/lib/duration.ts +0 -43
  41. package/src/workflow/lib/errors.ts +0 -37
  42. package/src/workflow/lib/expression/ast.ts +0 -108
  43. package/src/workflow/lib/expression/context.ts +0 -148
  44. package/src/workflow/lib/expression/evaluator.ts +0 -492
  45. package/src/workflow/lib/expression/index.ts +0 -28
  46. package/src/workflow/lib/expression/interpolation.ts +0 -84
  47. package/src/workflow/lib/expression/parser.ts +0 -264
  48. package/src/workflow/lib/expression/template.ts +0 -117
  49. package/src/workflow/lib/expression/tokenizer.ts +0 -200
  50. package/src/workflow/lib/expression/tokens.ts +0 -30
  51. package/src/workflow/lib/expression/walk-artifact-refs.ts +0 -232
  52. package/src/workflow/lib/installation-resource-kind.ts +0 -107
  53. package/src/workflow/lib/schemas-loader.ts +0 -64
  54. package/src/workflow/lib/serializer.ts +0 -30
  55. package/src/workflow/lib/types.ts +0 -417
  56. package/src/workflow/lib/validate.ts +0 -199
  57. package/src/workspace-manifest/index.ts +0 -27
  58. package/src/workspace-manifest/lib/compile.ts +0 -619
  59. package/src/workspace-manifest/lib/interpolate.ts +0 -166
  60. package/src/workspace-manifest/lib/resolve-extends.ts +0 -260
  61. package/src/workspace-manifest/lib/schema.ts +0 -692
  62. package/src/workspace-manifest/lib/types.ts +0 -446
@@ -1,254 +0,0 @@
1
- import {
2
- WorkflowErrorCode,
3
- WorkflowTriggerKind,
4
- type TriggerPayload,
5
- } from '@xemahq/kernel-contracts/workflow';
6
- import { WorkflowDslError } from '../errors';
7
- import type { WorkflowDocument, WorkflowInputDeclaration } from '../types';
8
-
9
- /**
10
- * Bind trigger inputs against the workflow's declared input schema(s),
11
- * applying declared defaults. Missing-required / type-mismatch is a
12
- * compile error — never a silent zero/null substitution (rule 2).
13
- *
14
- * We use the trigger kind to pick which declaration block governs:
15
- * - workflow_dispatch → on.workflow_dispatch.inputs
16
- * - schedule → on.schedule[*].inputs (pre-bound on the schedule itself)
17
- * - workflow_call → on.workflow_call.inputs
18
- * - webhook → no input declaration; payload is the whole envelope
19
- *
20
- * Scheduled triggers carry their pre-bound inputs in `trigger.inputs`; we
21
- * validate against the workflow_dispatch declaration if present, or pass
22
- * through un-validated (schedule-authored inputs are trusted) — the engine
23
- * validates schedule-level inputs at schedule-creation time instead.
24
- */
25
- export function bindTriggerInputs(
26
- workflow: WorkflowDocument,
27
- trigger: TriggerPayload,
28
- previewMode = false,
29
- ): Readonly<Record<string, unknown>> {
30
- switch (trigger.kind) {
31
- case WorkflowTriggerKind.WORKFLOW_DISPATCH: {
32
- if (workflow.on.workflow_dispatch === undefined) {
33
- throw new WorkflowDslError(
34
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
35
- `Workflow does not declare an 'on.workflow_dispatch' trigger and cannot be dispatched. Declared triggers: [${Object.keys(workflow.on).sort((a, b) => a.localeCompare(b)).join(', ') || '(none)'}].`,
36
- { triggerKind: 'workflow_dispatch', declaredTriggers: Object.keys(workflow.on) },
37
- );
38
- }
39
- const decl = workflow.on.workflow_dispatch.inputs ?? {};
40
- return bindInputs(decl, trigger.inputs, 'workflow_dispatch', previewMode);
41
- }
42
- case WorkflowTriggerKind.WORKFLOW_CALL: {
43
- if (workflow.on.workflow_call === undefined) {
44
- throw new WorkflowDslError(
45
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
46
- `Workflow does not declare an 'on.workflow_call' trigger and cannot be invoked from another workflow. Declared triggers: [${Object.keys(workflow.on).sort((a, b) => a.localeCompare(b)).join(', ') || '(none)'}].`,
47
- { triggerKind: 'workflow_call', declaredTriggers: Object.keys(workflow.on) },
48
- );
49
- }
50
- const decl = workflow.on.workflow_call.inputs ?? {};
51
- return bindInputs(decl, trigger.inputs, 'workflow_call', previewMode);
52
- }
53
- case WorkflowTriggerKind.SCHEDULE: {
54
- // Schedule inputs were validated against workflow_dispatch.inputs at
55
- // schedule-create time by workflow-engine-api. Here we just validate
56
- // the resulting bound payload against the same schema (defense in
57
- // depth; cheap).
58
- const decl = workflow.on.workflow_dispatch?.inputs ?? {};
59
- return bindInputs(decl, trigger.inputs, 'schedule', previewMode);
60
- }
61
- case WorkflowTriggerKind.WEBHOOK: {
62
- // Webhook does not declare typed inputs at the workflow level —
63
- // validation of the canonical envelope happens at connector-gateway-api.
64
- return trigger.payload;
65
- }
66
- }
67
- }
68
-
69
- /**
70
- * Engine-reserved trigger inputs. The workflow engine injects these onto a
71
- * CHILD run's trigger inputs at dispatch time — they carry dispatch lineage,
72
- * the inherited responder chain, and the triggering inquiry context. They are
73
- * NEVER author-declared, so they cannot appear in `on.<trigger>.inputs`, yet
74
- * they ride the same `inputs` map. Without an explicit allowlist the
75
- * unknown-input guard below rejects every child dispatch made by
76
- * `xema/dispatch-workflow` / `workflow-decider`.
77
- *
78
- * Closed set, mirrored by the producers (a bare `__` prefix is intentionally
79
- * NOT trusted — an unknown `__`-prefixed key is still a hard error, so a
80
- * caller cannot smuggle lineage/responder-chain values past the gate):
81
- * - `dispatch-workflow.activity.ts` → __dispatchReason,
82
- * __concurrencyGroupOverride, __parentRunId, __parentJobRunId
83
- * - `workflow-decider.activity.ts` → __responderChain,
84
- * __triggeredByInquiryId, __triggeredByRecipientId
85
- *
86
- * Reserved values pass through to the bound payload untouched (consumers read
87
- * them off the run's inputs) and are exempt from type/declaration validation.
88
- */
89
- export const RESERVED_TRIGGER_INPUT_KEYS: ReadonlySet<string> = new Set([
90
- '__dispatchReason',
91
- '__concurrencyGroupOverride',
92
- '__parentRunId',
93
- '__parentJobRunId',
94
- '__responderChain',
95
- '__triggeredByInquiryId',
96
- '__triggeredByRecipientId',
97
- ]);
98
-
99
- function bindInputs(
100
- declarations: Readonly<Record<string, WorkflowInputDeclaration>>,
101
- received: Readonly<Record<string, unknown>>,
102
- label: string,
103
- previewMode = false,
104
- ): Readonly<Record<string, unknown>> {
105
- const out: Record<string, unknown> = {};
106
- const seen = new Set<string>();
107
-
108
- for (const [name, decl] of Object.entries(declarations)) {
109
- seen.add(name);
110
- const hasValue = Object.hasOwn(received, name);
111
- if (!hasValue) {
112
- handleMissingInput(out, name, decl, label, previewMode);
113
- continue;
114
- }
115
- const value = received[name];
116
- assertInputType(value, decl, name, label);
117
- out[name] = value;
118
- }
119
-
120
- // Reject unknown inputs — fail-fast beats silent pass-through. Engine-
121
- // reserved keys are the sole exception: they are injected by the engine on
122
- // child dispatches, never author-declared, so they pass through verbatim
123
- // instead of tripping the guard.
124
- for (const name of Object.keys(received)) {
125
- if (seen.has(name)) {
126
- continue;
127
- }
128
- if (RESERVED_TRIGGER_INPUT_KEYS.has(name)) {
129
- out[name] = received[name];
130
- continue;
131
- }
132
- throw new WorkflowDslError(
133
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
134
- `${label} trigger received unknown input '${name}'. Declared inputs: [${Object.keys(declarations).sort((a, b) => a.localeCompare(b)).join(', ') || '(none)'}].`,
135
- { label, inputName: name },
136
- );
137
- }
138
-
139
- return out;
140
- }
141
-
142
- function handleMissingInput(
143
- out: Record<string, unknown>,
144
- name: string,
145
- decl: WorkflowInputDeclaration,
146
- label: string,
147
- previewMode: boolean,
148
- ): void {
149
- if (decl.required !== true) {
150
- // Optional inputs are always materialized in the bound payload so
151
- // `${{ inputs.<name> }}` resolves deterministically — declared default
152
- // when present, otherwise null. Without this, expressions referencing
153
- // a not-provided optional input throw `Unknown property` at runtime.
154
- out[name] = decl.default === undefined ? null : decl.default;
155
- return;
156
- }
157
- if (previewMode) {
158
- // In preview mode, substitute a type-appropriate sentinel so the
159
- // compiler can validate structure (DAG, expressions, matrix)
160
- // without requiring real input values. Never used on real runs.
161
- out[name] = previewSentinel(decl.type);
162
- return;
163
- }
164
- throw new WorkflowDslError(
165
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
166
- `${label} trigger is missing required input '${name}'.`,
167
- { label, inputName: name },
168
- );
169
- }
170
-
171
- /**
172
- * Returns a type-appropriate sentinel value for use in preview mode when
173
- * a required input is missing. The sentinel must pass `assertInputType`
174
- * for its declared type — it is never used outside of preview compilation.
175
- */
176
- function previewSentinel(type: WorkflowInputDeclaration['type']): unknown {
177
- switch (type) {
178
- case 'string':
179
- return '';
180
- case 'number':
181
- return 0;
182
- case 'integer':
183
- return 0;
184
- case 'boolean':
185
- return false;
186
- case 'object':
187
- return {};
188
- case 'array':
189
- return [];
190
- }
191
- }
192
-
193
- function assertInputType(
194
- value: unknown,
195
- decl: WorkflowInputDeclaration,
196
- name: string,
197
- label: string,
198
- ): void {
199
- const received = classify(value);
200
- if (!isAssignable(received, decl.type)) {
201
- throw new WorkflowDslError(
202
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
203
- `${label} trigger input '${name}' expected ${decl.type}, received ${received}.`,
204
- { label, inputName: name, expected: decl.type, received },
205
- );
206
- }
207
- if (decl.enum !== undefined) {
208
- if (!decl.enum.includes(value)) {
209
- throw new WorkflowDslError(
210
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
211
- `${label} trigger input '${name}' value not in enum.`,
212
- { label, inputName: name, enum: decl.enum },
213
- );
214
- }
215
- }
216
- }
217
-
218
- /**
219
- * Whether a value classified as `received` satisfies a declared `expected`
220
- * type. Identity always holds. The one widening: an `integer` value is a
221
- * valid `number` (JSON Schema treats integer as a subtype of number), so a
222
- * whole-valued JS number like `5` — which `classify` reports as `'integer'`
223
- * — must bind to a `number`-typed input. The reverse never holds: a
224
- * `number`-declared field accepts integers, but an `integer`-declared field
225
- * still rejects fractional numbers. No other type pair is assignable.
226
- */
227
- function isAssignable(
228
- received: WorkflowInputDeclaration['type'],
229
- expected: WorkflowInputDeclaration['type'],
230
- ): boolean {
231
- if (received === expected) return true;
232
- return expected === 'number' && received === 'integer';
233
- }
234
-
235
- function classify(value: unknown): WorkflowInputDeclaration['type'] {
236
- if (Array.isArray(value)) return 'array';
237
- if (value === null) return 'object'; // null treated as absence; Ajv doesn't emit null type for us
238
- switch (typeof value) {
239
- case 'string':
240
- return 'string';
241
- case 'boolean':
242
- return 'boolean';
243
- case 'number':
244
- return Number.isInteger(value) ? 'integer' : 'number';
245
- case 'object':
246
- return 'object';
247
- default:
248
- throw new WorkflowDslError(
249
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
250
- `Unsupported input value kind: ${typeof value}`,
251
- { valueKind: typeof value },
252
- );
253
- }
254
- }
@@ -1,114 +0,0 @@
1
- import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
2
-
3
- import { WorkflowDslError } from '../errors';
4
- import {
5
- collectInstallationResourceHints,
6
- type InstallationResourceFieldHint,
7
- } from '../installation-resource-kind';
8
-
9
- import type { InstallationCompileScope } from './types';
10
-
11
- /**
12
- * Compile-time check that every `x-installation-resource` field on the
13
- * action manifest's `inputs:` schema points at a resource bound to the
14
- * calling biome installation.
15
- *
16
- * Cases handled:
17
- * - Expression-shaped value (`${{ ... }}`): the literal isn't known
18
- * yet, so we skip the binding check. Runtime auth lives at the
19
- * activity layer (`AuthFailed` from credentials/resolve).
20
- * - Literal string: must be in `boundResources[kind]`. Missing →
21
- * DSL error with `inputs.<path>` pointer.
22
- * - Array of strings (rare): every element must be bound.
23
- * - Undefined + optional: no-op.
24
- *
25
- * Skipped entirely when `installationScope` is undefined (system /
26
- * org-wide dispatch) — only biome-installed runs get this gate.
27
- */
28
- export function validateInstallationResourceBindings(input: {
29
- jobKey: string;
30
- actionId: string;
31
- inputsSchema: unknown;
32
- withValue: Readonly<Record<string, unknown>>;
33
- scope: InstallationCompileScope | undefined;
34
- }): void {
35
- if (!input.scope) {return;}
36
- if (!input.inputsSchema) {return;}
37
- const hints = collectInstallationResourceHints(input.inputsSchema);
38
- if (hints.length === 0) {return;}
39
- for (const hint of hints) {
40
- const value = readPath(input.withValue, hint.path);
41
- if (value === undefined || value === null || value === '') {
42
- if (hint.optional) {continue;}
43
- throw new WorkflowDslError(
44
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
45
- `jobs.${input.jobKey}.with.${hint.path} is required (action ${input.actionId} declares x-installation-resource kind="${hint.kind}") but the with-block omitted it`,
46
- );
47
- }
48
- const ctx = {
49
- jobKey: input.jobKey,
50
- actionId: input.actionId,
51
- scope: input.scope,
52
- };
53
- if (typeof value === 'string') {
54
- assertBound(ctx, hint, value);
55
- continue;
56
- }
57
- if (Array.isArray(value)) {
58
- for (const item of value) {
59
- if (typeof item === 'string') {
60
- assertBound(ctx, hint, item);
61
- }
62
- }
63
- }
64
- }
65
- }
66
-
67
- function assertBound(
68
- ctx: {
69
- jobKey: string;
70
- actionId: string;
71
- scope: InstallationCompileScope;
72
- },
73
- hint: InstallationResourceFieldHint,
74
- literal: string,
75
- ): void {
76
- // Expression-shaped values are deferred to runtime — the literal
77
- // string the compiler sees here is the expression body itself
78
- // (`${{ inputs.x }}`), not a resolvable id.
79
- if (literal.startsWith('${{') || literal.includes('${{')) {return;}
80
- const bound = ctx.scope.boundResources[hint.kind] ?? [];
81
- if (bound.length === 0) {
82
- throw new WorkflowDslError(
83
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
84
- `jobs.${ctx.jobKey}.with.${hint.path} references ${hint.kind}="${literal}" but installation ${ctx.scope.installationId} has no ${hint.kind} resources bound — connect one before dispatching`,
85
- );
86
- }
87
- if (!bound.includes(literal)) {
88
- throw new WorkflowDslError(
89
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
90
- `jobs.${ctx.jobKey}.with.${hint.path} references ${hint.kind}="${literal}" which is NOT bound to installation ${ctx.scope.installationId} (action ${ctx.actionId} declares x-installation-resource kind="${hint.kind}"). Bound: [${bound.join(', ')}]`,
91
- );
92
- }
93
- }
94
-
95
- function readPath(
96
- obj: Readonly<Record<string, unknown>>,
97
- path: string,
98
- ): unknown {
99
- if (path === '') {return obj;}
100
- const segments = path.split('.');
101
- let cursor: unknown = obj;
102
- for (const seg of segments) {
103
- if (!cursor || typeof cursor !== 'object') {return undefined;}
104
- // Array-element path uses `[*]` — treat as "any element", returning
105
- // the array itself so the caller iterates. The hint walker emits
106
- // these for `items.x-installation-resource` shapes.
107
- if (seg === '[*]') {
108
- if (!Array.isArray(cursor)) {return undefined;}
109
- return cursor;
110
- }
111
- cursor = (cursor as Record<string, unknown>)[seg];
112
- }
113
- return cursor;
114
- }
@@ -1,71 +0,0 @@
1
- import {
2
- WorkflowErrorCode,
3
- type CompiledManifestSource,
4
- } from '@xemahq/kernel-contracts/workflow';
5
-
6
- import { WorkflowDslError } from '../errors';
7
- import { ANY_INTERPOLATION_RE } from '../expression/interpolation';
8
- import type { ActionManifest } from '../types';
9
- import type { ResolvedAgentMeta } from './types';
10
- import { isAgentShapedAction } from './action-shape';
11
-
12
- /**
13
- * Compute the compile-time `manifestSource` discriminator for one job.
14
- *
15
- * `agentRef` is the SOLE way a workflow names its agent. Terminal shapes:
16
- * • `null` — non-agent action (the action manifest doesn't expose
17
- * `agentRef` on its `inputs:` schema).
18
- * • `{ kind: 'revision', ...pin }` — `agentRef` is a literal string
19
- * already resolved by the engine to an immutable Agent revision.
20
- * • `{ kind: 'deferred' }` — `agentRef` is a `${{ … }}` expression.
21
- * The worker records one immutable resolution in Temporal history after
22
- * expression evaluation and before scheduling the Agent activity.
23
- *
24
- * An agent-shaped action with no `agentRef` fails fast — there is no
25
- * inline `mounts` short-form anymore.
26
- */
27
- export function compileManifestSource(
28
- jobKey: string,
29
- withBlock: Readonly<Record<string, unknown>> | undefined,
30
- actionManifest: ActionManifest | null,
31
- resolvedAgents: Readonly<Record<string, ResolvedAgentMeta>>,
32
- ): CompiledManifestSource | null {
33
- if (!isAgentShapedAction(actionManifest)) return null;
34
- const block = withBlock ?? {};
35
-
36
- const ref = block['agentRef'];
37
- if (typeof ref === 'string' && ref.length > 0) {
38
- if (isLiteralString(ref)) {
39
- const resolved = resolvedAgents[ref];
40
- if (resolved === undefined) {
41
- throw new WorkflowDslError(
42
- WorkflowErrorCode.DSL_UNKNOWN_AGENT,
43
- `Job '${jobKey}' with.agentRef = '${ref}' was not resolved to an immutable Agent revision.`,
44
- { jobKey, fieldPath: 'with.agentRef', value: ref },
45
- );
46
- }
47
- return Object.freeze({
48
- kind: 'revision' as const,
49
- agentRevisionId: resolved.agentRevisionId,
50
- agentContentHash: resolved.agentContentHash,
51
- agentSlug: resolved.slug,
52
- });
53
- }
54
- return Object.freeze({ kind: 'deferred' as const });
55
- }
56
- if (ref !== undefined) {
57
- // ref is present but not a literal string → expression.
58
- return Object.freeze({ kind: 'deferred' as const });
59
- }
60
-
61
- throw new WorkflowDslError(
62
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
63
- `Job '${jobKey}' is an agent-shaped action but does not set \`with.agentRef\`. ` +
64
- `\`agentRef\` (a \`<slug>\` or \`<slug>@<version>\` reference) is the only way a workflow names its agent.`,
65
- { jobKey, actionId: actionManifest.metadata.id },
66
- );
67
- }
68
-
69
- function isLiteralString(value: unknown): value is string {
70
- return typeof value === 'string' && !ANY_INTERPOLATION_RE.test(value);
71
- }
@@ -1,135 +0,0 @@
1
- import { MatrixStrategyKind, WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
2
- import type {
3
- CompiledDynamicMatrixStrategy,
4
- CompiledSingleStrategy,
5
- CompiledStaticMatrixStrategy,
6
- CompiledStrategy,
7
- } from '@xemahq/kernel-contracts/workflow';
8
- import { WorkflowDslError } from '../errors';
9
- import type { WorkflowStrategyDeclaration } from '../types';
10
- import { compileExpression } from '../expression';
11
- import { stripInterpolation } from '../expression/interpolation';
12
-
13
- /** Hard cap on matrix cardinality — static and dynamic both enforce this. */
14
- const MATRIX_MAX_ENTRIES = 64;
15
- const MATRIX_DEFAULT_MAX_PARALLEL = 8;
16
-
17
- /**
18
- * Compile an authored strategy declaration into a CompiledStrategy. Performs
19
- * cartesian expansion for static matrices at compile time and pre-validates
20
- * the dynamic `from:` expression.
21
- *
22
- * Static matrix cardinality is the product of each axis's length. The cap
23
- * is applied to the product, not per-axis, so 8×8 (=64) is the ceiling.
24
- */
25
- export function compileStrategy(
26
- declaration: WorkflowStrategyDeclaration | undefined,
27
- jobKey: string,
28
- ): CompiledStrategy {
29
- if (!declaration) {
30
- const single: CompiledSingleStrategy = { kind: MatrixStrategyKind.SINGLE };
31
- return single;
32
- }
33
-
34
- if ('matrix' in declaration) {
35
- return expandStaticMatrix(declaration.matrix, declaration.maxParallel, jobKey);
36
- }
37
-
38
- return compileDynamicStrategy(declaration.dynamic, declaration.maxParallel, jobKey);
39
- }
40
-
41
- function expandStaticMatrix(
42
- axes: Readonly<Record<string, readonly unknown[]>>,
43
- maxParallelRaw: number | undefined,
44
- jobKey: string,
45
- ): CompiledStaticMatrixStrategy {
46
- const axisKeys = Object.keys(axes).sort();
47
- if (axisKeys.length === 0) {
48
- throw new WorkflowDslError(
49
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
50
- `Job '${jobKey}' declares strategy.matrix with no axes.`,
51
- { jobKey },
52
- );
53
- }
54
-
55
- // Cartesian product in lexicographic order of axisKeys for determinism.
56
- let entries: Record<string, unknown>[] = [{}];
57
- for (const axis of axisKeys) {
58
- const values = axes[axis]!;
59
- const next: Record<string, unknown>[] = [];
60
- for (const partial of entries) {
61
- for (const value of values) {
62
- next.push({ ...partial, [axis]: value });
63
- }
64
- if (next.length > MATRIX_MAX_ENTRIES) break;
65
- }
66
- entries = next;
67
- if (entries.length > MATRIX_MAX_ENTRIES) break;
68
- }
69
-
70
- if (entries.length > MATRIX_MAX_ENTRIES) {
71
- throw new WorkflowDslError(
72
- WorkflowErrorCode.DSL_MATRIX_CARDINALITY_EXCEEDED,
73
- `Job '${jobKey}' static matrix expansion produces ${entries.length} entries; cap is ${MATRIX_MAX_ENTRIES}.`,
74
- { jobKey, cap: MATRIX_MAX_ENTRIES, cardinality: entries.length },
75
- );
76
- }
77
-
78
- const maxParallel = resolveMaxParallel(maxParallelRaw, entries.length);
79
-
80
- return {
81
- kind: MatrixStrategyKind.STATIC,
82
- entries: entries.map(Object.freeze) as readonly Readonly<Record<string, unknown>>[],
83
- maxParallel,
84
- };
85
- }
86
-
87
- function compileDynamicStrategy(
88
- declaration: {
89
- readonly from: string;
90
- readonly as: string;
91
- readonly maxEntries?: number;
92
- readonly keyBy?: string;
93
- },
94
- maxParallelRaw: number | undefined,
95
- jobKey: string,
96
- ): CompiledDynamicMatrixStrategy {
97
- const body = stripInterpolation(declaration.from);
98
- // Compile expression at compile time — this rejects unknown roots and
99
- // bad syntax now, so the runtime evaluator can only fail on an unknown
100
- // property during real evaluation.
101
- compileExpression(body);
102
-
103
- const maxEntries = declaration.maxEntries ?? MATRIX_MAX_ENTRIES;
104
- if (maxEntries < 1 || maxEntries > MATRIX_MAX_ENTRIES) {
105
- throw new WorkflowDslError(
106
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
107
- `Job '${jobKey}' strategy.dynamic.maxEntries must be in [1, ${MATRIX_MAX_ENTRIES}]; got ${maxEntries}.`,
108
- { jobKey, maxEntries, cap: MATRIX_MAX_ENTRIES },
109
- );
110
- }
111
- const maxParallel = resolveMaxParallel(maxParallelRaw, maxEntries);
112
-
113
- return {
114
- kind: MatrixStrategyKind.DYNAMIC,
115
- fromExpression: body,
116
- bindingName: declaration.as,
117
- maxEntries,
118
- maxParallel,
119
- keyBy: declaration.keyBy ?? null,
120
- };
121
- }
122
-
123
- function resolveMaxParallel(raw: number | undefined, cardinality: number): number {
124
- if (raw === undefined) {
125
- return Math.min(MATRIX_DEFAULT_MAX_PARALLEL, Math.max(1, cardinality));
126
- }
127
- if (raw < 1) {
128
- throw new WorkflowDslError(
129
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
130
- `strategy.maxParallel must be >= 1 (got ${raw}).`,
131
- { raw },
132
- );
133
- }
134
- return Math.min(raw, MATRIX_MAX_ENTRIES);
135
- }