@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.
- package/package.json +2 -3
- package/src/deliverable-spec/index.ts +0 -19
- package/src/deliverable-spec/lib/schema.ts +0 -270
- package/src/deliverable-spec/lib/types.ts +0 -26
- package/src/payload-codec/index.ts +0 -44
- package/src/payload-codec/lib/blob-store.ts +0 -176
- package/src/payload-codec/lib/codec-context.ts +0 -38
- package/src/payload-codec/lib/codec.ts +0 -605
- package/src/payload-codec/lib/enums.ts +0 -58
- package/src/payload-codec/lib/errors.ts +0 -54
- package/src/payload-codec/lib/http-blob-store.ts +0 -267
- package/src/payload-codec/lib/lru-cache.ts +0 -81
- package/src/payload-codec/lib/payload.ts +0 -26
- package/src/payload-codec/temporal/index.ts +0 -36
- package/src/workflow/index.ts +0 -108
- package/src/workflow/lib/action-input-validator.ts +0 -160
- package/src/workflow/lib/compiler/action-shape.ts +0 -71
- package/src/workflow/lib/compiler/canonical-json.ts +0 -66
- package/src/workflow/lib/compiler/compile.ts +0 -1742
- package/src/workflow/lib/compiler/concurrency.ts +0 -223
- package/src/workflow/lib/compiler/dag.ts +0 -108
- package/src/workflow/lib/compiler/gate-defaults.ts +0 -153
- package/src/workflow/lib/compiler/index.ts +0 -11
- package/src/workflow/lib/compiler/inputs.ts +0 -254
- package/src/workflow/lib/compiler/installation-resource-validator.ts +0 -114
- package/src/workflow/lib/compiler/manifest-source.ts +0 -71
- package/src/workflow/lib/compiler/matrix.ts +0 -135
- package/src/workflow/lib/compiler/mount-plan.ts +0 -190
- package/src/workflow/lib/compiler/payload-reach-in.ts +0 -497
- package/src/workflow/lib/compiler/permissions.ts +0 -64
- package/src/workflow/lib/compiler/retry-timeout.ts +0 -105
- package/src/workflow/lib/compiler/review-step.ts +0 -548
- package/src/workflow/lib/compiler/types.ts +0 -172
- package/src/workflow/lib/compiler/variable-requirements.ts +0 -208
- package/src/workflow/lib/deliverable-spec-introspection-error.ts +0 -63
- package/src/workflow/lib/deliverable-spec-keys.ts +0 -147
- package/src/workflow/lib/deliverable-spec-source-scan.ts +0 -280
- package/src/workflow/lib/dispatch-inputs/index.ts +0 -160
- package/src/workflow/lib/dispatch-inputs/to-json-schema.ts +0 -60
- package/src/workflow/lib/duration.ts +0 -43
- package/src/workflow/lib/errors.ts +0 -37
- package/src/workflow/lib/expression/ast.ts +0 -108
- package/src/workflow/lib/expression/context.ts +0 -148
- package/src/workflow/lib/expression/evaluator.ts +0 -492
- package/src/workflow/lib/expression/index.ts +0 -28
- package/src/workflow/lib/expression/interpolation.ts +0 -84
- package/src/workflow/lib/expression/parser.ts +0 -264
- package/src/workflow/lib/expression/template.ts +0 -117
- package/src/workflow/lib/expression/tokenizer.ts +0 -200
- package/src/workflow/lib/expression/tokens.ts +0 -30
- package/src/workflow/lib/expression/walk-artifact-refs.ts +0 -232
- package/src/workflow/lib/installation-resource-kind.ts +0 -107
- package/src/workflow/lib/schemas-loader.ts +0 -64
- package/src/workflow/lib/serializer.ts +0 -30
- package/src/workflow/lib/types.ts +0 -417
- package/src/workflow/lib/validate.ts +0 -199
- package/src/workspace-manifest/index.ts +0 -27
- package/src/workspace-manifest/lib/compile.ts +0 -619
- package/src/workspace-manifest/lib/interpolate.ts +0 -166
- package/src/workspace-manifest/lib/resolve-extends.ts +0 -260
- package/src/workspace-manifest/lib/schema.ts +0 -692
- 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
|
-
}
|