@xemahq/dsl 0.6.0 → 0.7.0
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/dist/schema/reusable-workflow.schema.json +9 -1
- package/dist/schema/workflow.schema.json +39 -13
- package/dist/workflow/lib/compiler/compile.d.ts.map +1 -1
- package/dist/workflow/lib/compiler/compile.js +105 -20
- package/dist/workflow/lib/compiler/compile.js.map +1 -1
- package/dist/workflow/lib/compiler/concurrency.js +1 -1
- package/dist/workflow/lib/compiler/concurrency.js.map +1 -1
- package/dist/workflow/lib/compiler/manifest-source.d.ts +2 -1
- package/dist/workflow/lib/compiler/manifest-source.d.ts.map +1 -1
- package/dist/workflow/lib/compiler/manifest-source.js +13 -4
- package/dist/workflow/lib/compiler/manifest-source.js.map +1 -1
- package/dist/workflow/lib/compiler/review-step.d.ts +2 -1
- package/dist/workflow/lib/compiler/review-step.d.ts.map +1 -1
- package/dist/workflow/lib/compiler/review-step.js +13 -1
- package/dist/workflow/lib/compiler/review-step.js.map +1 -1
- package/dist/workflow/lib/compiler/types.d.ts +3 -2
- package/dist/workflow/lib/compiler/types.d.ts.map +1 -1
- package/dist/workflow/lib/expression/context.d.ts +1 -1
- package/dist/workflow/lib/expression/context.d.ts.map +1 -1
- package/dist/workflow/lib/expression/context.js +1 -1
- package/dist/workflow/lib/expression/context.js.map +1 -1
- package/dist/workflow/lib/types.d.ts +1 -1
- package/dist/workflow/lib/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/schema/reusable-workflow.schema.json +9 -1
- package/schema/workflow.schema.json +39 -13
- package/src/workflow/lib/compiler/compile.ts +302 -149
- package/src/workflow/lib/compiler/concurrency.ts +1 -1
- package/src/workflow/lib/compiler/manifest-source.ts +23 -8
- package/src/workflow/lib/compiler/review-step.ts +33 -2
- package/src/workflow/lib/compiler/types.ts +9 -9
- package/src/workflow/lib/expression/context.ts +2 -2
- package/src/workflow/lib/types.ts +38 -8
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type ActionRef,
|
|
7
7
|
type Briefcase,
|
|
8
8
|
type CompiledJob,
|
|
9
|
+
type CompiledManifestSource,
|
|
9
10
|
type CompiledRun,
|
|
10
11
|
type PermissionResource,
|
|
11
12
|
type PermissionScope,
|
|
@@ -75,7 +76,8 @@ import type {
|
|
|
75
76
|
* 8. Emit CompiledRun with a deterministic sha256.
|
|
76
77
|
*/
|
|
77
78
|
export function compileWorkflow(input: CompileInput): CompiledRun {
|
|
78
|
-
const { workflowRef, trigger, resolvedRefs,
|
|
79
|
+
const { workflowRef, trigger, resolvedRefs, workflowDefinitionContentHash } =
|
|
80
|
+
input;
|
|
79
81
|
|
|
80
82
|
// Fold `defaults.gate` into every decision-gate job's `with:` BEFORE any
|
|
81
83
|
// other pass — expression validation, needs-shape validation, and the
|
|
@@ -83,7 +85,11 @@ export function compileWorkflow(input: CompileInput): CompiledRun {
|
|
|
83
85
|
// like inline-authored values. Explicit per-gate keys always win.
|
|
84
86
|
const workflow = applyGateDefaults(input.workflow, resolvedRefs);
|
|
85
87
|
|
|
86
|
-
const inputs = bindTriggerInputs(
|
|
88
|
+
const inputs = bindTriggerInputs(
|
|
89
|
+
workflow,
|
|
90
|
+
trigger,
|
|
91
|
+
input.previewMode ?? false,
|
|
92
|
+
);
|
|
87
93
|
const vars: Readonly<Record<string, unknown>> = workflow.vars ?? {};
|
|
88
94
|
|
|
89
95
|
// W5 definition-vs-run boundary: the CompiledRun is the single
|
|
@@ -103,7 +109,12 @@ export function compileWorkflow(input: CompileInput): CompiledRun {
|
|
|
103
109
|
...(workflow.requires?.wallets ?? []),
|
|
104
110
|
]) as readonly string[];
|
|
105
111
|
const permissions = normalizePermissions(workflow.permissions);
|
|
106
|
-
const concurrency = compileConcurrency(
|
|
112
|
+
const concurrency = compileConcurrency(
|
|
113
|
+
workflow.concurrency,
|
|
114
|
+
trigger,
|
|
115
|
+
inputs,
|
|
116
|
+
vars,
|
|
117
|
+
);
|
|
107
118
|
const defaults = resolveWorkflowDefaults(workflow.defaults);
|
|
108
119
|
|
|
109
120
|
// Validate every expression everywhere at compile time. We throw away the
|
|
@@ -161,7 +172,10 @@ export function compileWorkflow(input: CompileInput): CompiledRun {
|
|
|
161
172
|
// job's declared deliverable spec. Skips when the engine couldn't
|
|
162
173
|
// pre-fetch spec content (preview mode or specs without introspectable
|
|
163
174
|
// shape).
|
|
164
|
-
validateDeliverableValueExpressions(
|
|
175
|
+
validateDeliverableValueExpressions(
|
|
176
|
+
workflow,
|
|
177
|
+
input.resolvedDeliverableSpecs ?? {},
|
|
178
|
+
);
|
|
165
179
|
|
|
166
180
|
// Payload reach-in validation: `needs.<X>.outputs.<name>.<field>`
|
|
167
181
|
// chains where `<field>` is not an ArtifactRef envelope field reach
|
|
@@ -189,128 +203,150 @@ export function compileWorkflow(input: CompileInput): CompiledRun {
|
|
|
189
203
|
}),
|
|
190
204
|
);
|
|
191
205
|
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
manifestSha256: resolved.manifestSha256,
|
|
198
|
-
executionKind: resolveJobExecutionKind(key, payload.with, resolved),
|
|
199
|
-
taskQueue: resolved.taskQueue,
|
|
200
|
-
actionKind: resolveActionKind(resolved),
|
|
201
|
-
isReusableWorkflow: resolved.isReusableWorkflow,
|
|
202
|
-
// Pin the manifest's `inputs:` schema into the compiled ref so the
|
|
203
|
-
// worker validates `with:` against the schema that was in effect at
|
|
204
|
-
// compile time — not whatever's currently published. Reusable
|
|
205
|
-
// workflows have their own `workflow_call.inputs`, validated by
|
|
206
|
-
// their own compile pass.
|
|
207
|
-
inputsSchema: resolved.isReusableWorkflow
|
|
208
|
-
? null
|
|
209
|
-
: resolved.actionManifest?.spec.inputs ?? null,
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const jobPermissions = normalizePermissions(payload.permissions);
|
|
213
|
-
assertJobPermissionsFit(jobPermissions, permissions, key);
|
|
214
|
-
|
|
215
|
-
const strategy = compileStrategy(payload.strategy, key);
|
|
216
|
-
|
|
217
|
-
// When the job uses a reusable workflow, mount planning is governed
|
|
218
|
-
// by the reusable workflow's own jobs, not by this job. So we emit an
|
|
219
|
-
// empty plan — the child workflow's compiler run will produce its own.
|
|
220
|
-
const mountPlan = resolved.isReusableWorkflow
|
|
221
|
-
? { readOnly: {}, readWrite: {} }
|
|
222
|
-
: compileMountPlan(key, payload.with, resolved.actionManifest);
|
|
223
|
-
|
|
224
|
-
// For agent-shaped actions, pre-resolve the manifest source so the
|
|
225
|
-
// worker sees a single discriminated shape (`ref` / `inline` /
|
|
226
|
-
// `inline-deferred`) instead of branching on input fields at
|
|
227
|
-
// dispatch. Reusable-workflow jobs return null — their nested
|
|
228
|
-
// CompiledRun owns its own agent steps.
|
|
229
|
-
const manifestSource = resolved.isReusableWorkflow
|
|
206
|
+
const manifestSourcesByJob: Record<string, CompiledManifestSource | null> =
|
|
207
|
+
{};
|
|
208
|
+
for (const [jobKey, job] of Object.entries(workflow.jobs)) {
|
|
209
|
+
const resolved = lookupResolvedRef(job.uses, resolvedRefs);
|
|
210
|
+
manifestSourcesByJob[jobKey] = resolved.isReusableWorkflow
|
|
230
211
|
? null
|
|
231
|
-
: compileManifestSource(
|
|
212
|
+
: compileManifestSource(
|
|
213
|
+
jobKey,
|
|
214
|
+
job.with,
|
|
215
|
+
resolved.actionManifest,
|
|
216
|
+
input.resolvedAgents ?? {},
|
|
217
|
+
);
|
|
218
|
+
}
|
|
232
219
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
resolved
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
220
|
+
const compiledJobs: CompiledJob[] = orderedJobs.map(
|
|
221
|
+
({ key, payload, needs }) => {
|
|
222
|
+
const resolved = lookupResolvedRef(payload.uses, resolvedRefs);
|
|
223
|
+
const actionRef: ActionRef = {
|
|
224
|
+
id: resolved.id,
|
|
225
|
+
version: resolved.version,
|
|
226
|
+
manifestSha256: resolved.manifestSha256,
|
|
227
|
+
executionKind: resolveJobExecutionKind(key, payload.with, resolved),
|
|
228
|
+
taskQueue: resolved.taskQueue,
|
|
229
|
+
actionKind: resolveActionKind(resolved),
|
|
230
|
+
isReusableWorkflow: resolved.isReusableWorkflow,
|
|
231
|
+
// Pin the manifest's `inputs:` schema into the compiled ref so the
|
|
232
|
+
// worker validates `with:` against the schema that was in effect at
|
|
233
|
+
// compile time — not whatever's currently published. Reusable
|
|
234
|
+
// workflows have their own `workflow_call.inputs`, validated by
|
|
235
|
+
// their own compile pass.
|
|
236
|
+
inputsSchema: resolved.isReusableWorkflow
|
|
237
|
+
? null
|
|
238
|
+
: (resolved.actionManifest?.spec.inputs ?? null),
|
|
239
|
+
};
|
|
243
240
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
jobKey: key,
|
|
274
|
-
actionId: actionRef.id,
|
|
275
|
-
inputsSchema: actionRef.inputsSchema,
|
|
276
|
-
withValue: compiledWith,
|
|
277
|
-
scope: input.installationScope,
|
|
278
|
-
});
|
|
241
|
+
const jobPermissions = normalizePermissions(payload.permissions);
|
|
242
|
+
assertJobPermissionsFit(jobPermissions, permissions, key);
|
|
243
|
+
|
|
244
|
+
const strategy = compileStrategy(payload.strategy, key);
|
|
245
|
+
|
|
246
|
+
// When the job uses a reusable workflow, mount planning is governed
|
|
247
|
+
// by the reusable workflow's own jobs, not by this job. So we emit an
|
|
248
|
+
// empty plan — the child workflow's compiler run will produce its own.
|
|
249
|
+
const mountPlan = resolved.isReusableWorkflow
|
|
250
|
+
? { readOnly: {}, readWrite: {} }
|
|
251
|
+
: compileMountPlan(key, payload.with, resolved.actionManifest);
|
|
252
|
+
|
|
253
|
+
// For agent-shaped actions, pre-resolve the manifest source so the
|
|
254
|
+
// worker sees one immutable revision pin or an explicitly deferred
|
|
255
|
+
// expression instead of branching on authoring selectors at dispatch.
|
|
256
|
+
// Reusable-workflow jobs return null — their nested CompiledRun owns its
|
|
257
|
+
// own agent steps.
|
|
258
|
+
const manifestSource = manifestSourcesByJob[key] ?? null;
|
|
259
|
+
|
|
260
|
+
const retry = resolveJobRetry(
|
|
261
|
+
defaults.retry,
|
|
262
|
+
resolved.actionManifest?.spec.retryDefaults ?? null,
|
|
263
|
+
payload.retry,
|
|
264
|
+
);
|
|
265
|
+
const timeoutMs = resolveJobTimeout(
|
|
266
|
+
defaults.timeoutMs,
|
|
267
|
+
resolved.actionManifest?.spec.timeoutDefaults ?? null,
|
|
268
|
+
payload.timeout,
|
|
269
|
+
);
|
|
279
270
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
271
|
+
// Validate `if` expression shape now — runtime evaluator only fails on
|
|
272
|
+
// unknown bindings after this point. Shorthand: rewrite a
|
|
273
|
+
// bare `needs.<X>` reference to `needs.<X>.outcome == 'ok'` so the
|
|
274
|
+
// evaluator sees the explicit success check (an envelope object is
|
|
275
|
+
// truthy even on failure — silent always-true is the trap this
|
|
276
|
+
// rewrite explicitly avoids).
|
|
277
|
+
const ifExpression =
|
|
278
|
+
payload.if !== undefined
|
|
279
|
+
? (() => {
|
|
280
|
+
const body = stripInterpolation(payload.if!);
|
|
281
|
+
const rewritten = rewriteBareNeedsInIf(body);
|
|
282
|
+
compileExpression(rewritten);
|
|
283
|
+
return rewritten;
|
|
284
|
+
})()
|
|
285
|
+
: null;
|
|
286
|
+
|
|
287
|
+
// For `xema/review@*` steps, rewrite `with:` from the author shape
|
|
288
|
+
// (`subject` + `redraft.step`) to the worker contract
|
|
289
|
+
// (`subjects` + embedded `redraft: { uses, with }`). Other steps
|
|
290
|
+
// pass through verbatim.
|
|
291
|
+
const compiledWith = attachResolvedReviewerPins(
|
|
292
|
+
rewriteReviewStepWith(key, payload, workflow, manifestSourcesByJob),
|
|
293
|
+
input.resolvedAgents ?? {},
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Compile-time installation-binding gate: when the engine threaded
|
|
297
|
+
// an installationScope through CompileInput, walk every literal
|
|
298
|
+
// `x-installation-resource` field in the `with:` block and confirm
|
|
299
|
+
// its value is bound to the calling installation. Rejects unbound
|
|
300
|
+
// walletIds (etc.) BEFORE the dispatch creates a run, instead of
|
|
301
|
+
// failing 3 activities later when connector-gateway-api refuses to
|
|
302
|
+
// mint credentials. Skips entirely for system / org-wide dispatches.
|
|
303
|
+
validateInstallationResourceBindings({
|
|
304
|
+
jobKey: key,
|
|
305
|
+
actionId: actionRef.id,
|
|
306
|
+
inputsSchema: actionRef.inputsSchema,
|
|
307
|
+
withValue: compiledWith,
|
|
308
|
+
scope: input.installationScope,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return Object.freeze({
|
|
312
|
+
jobKey: key,
|
|
313
|
+
title: payload.title ?? null,
|
|
314
|
+
needs,
|
|
315
|
+
matrixGather: Object.freeze([
|
|
316
|
+
...(payload.matrixGather ?? []),
|
|
317
|
+
]) as readonly string[],
|
|
318
|
+
ifExpression,
|
|
319
|
+
strategy,
|
|
320
|
+
action: actionRef,
|
|
321
|
+
mountPlan,
|
|
322
|
+
manifestSource,
|
|
323
|
+
with: Object.freeze({ ...compiledWith }) as Readonly<
|
|
324
|
+
Record<string, unknown>
|
|
325
|
+
>,
|
|
326
|
+
// Strip the `${{ ... }}` wrapper at compile time so the runtime
|
|
327
|
+
// evaluator receives expression bodies directly — same convention
|
|
328
|
+
// as `ifExpression`. `validateAllAuthoredExpressions` already
|
|
329
|
+
// compiled each body to surface invalid expressions as DSL errors.
|
|
330
|
+
outputs: compileOutputsMap(payload.outputs, `jobs.${key}.outputs`),
|
|
331
|
+
retry,
|
|
332
|
+
timeoutMs,
|
|
333
|
+
permissions: jobPermissions,
|
|
334
|
+
// The DSL emits null; the engine (which owns the reusable-workflow
|
|
335
|
+
// registry) post-processes each CompiledRun and attaches the nested
|
|
336
|
+
// compiled body for jobs whose action is a reusable workflow.
|
|
337
|
+
reusableCompiledRun: null,
|
|
338
|
+
payloadReachIns: Object.freeze(
|
|
339
|
+
collectPayloadReachInsForJob(
|
|
340
|
+
workflow,
|
|
341
|
+
key,
|
|
342
|
+
payload,
|
|
343
|
+
input.resolvedDeliverableSpecs ?? {},
|
|
344
|
+
resolvedRefs,
|
|
345
|
+
),
|
|
310
346
|
),
|
|
311
|
-
)
|
|
312
|
-
}
|
|
313
|
-
|
|
347
|
+
}) as CompiledJob;
|
|
348
|
+
},
|
|
349
|
+
);
|
|
314
350
|
|
|
315
351
|
const snapshotCreatedAt = input.trigger.triggeredAt;
|
|
316
352
|
const workflowCallOutputs = compileOutputsMap(
|
|
@@ -332,7 +368,7 @@ export function compileWorkflow(input: CompileInput): CompiledRun {
|
|
|
332
368
|
defaults,
|
|
333
369
|
jobs: Object.freeze(compiledJobs) as readonly CompiledJob[],
|
|
334
370
|
snapshotCreatedAt,
|
|
335
|
-
|
|
371
|
+
workflowDefinitionContentHash,
|
|
336
372
|
workflowCallOutputs,
|
|
337
373
|
workflowOutputs,
|
|
338
374
|
// Spread only when present so a run dispatched without a briefcase
|
|
@@ -593,6 +629,24 @@ function validateJobLiteralReferences(
|
|
|
593
629
|
resolvedAgents,
|
|
594
630
|
);
|
|
595
631
|
validateReviewerAgents(jobKey, withMap['reviewers'], resolvedAgents);
|
|
632
|
+
validateReviewerAgents(jobKey, withMap['recipients'], resolvedAgents);
|
|
633
|
+
const escalationChain = withMap['escalationChain'];
|
|
634
|
+
if (Array.isArray(escalationChain)) {
|
|
635
|
+
for (let i = 0; i < escalationChain.length; i++) {
|
|
636
|
+
const step = escalationChain[i];
|
|
637
|
+
if (
|
|
638
|
+
step !== null &&
|
|
639
|
+
typeof step === 'object' &&
|
|
640
|
+
!Array.isArray(step)
|
|
641
|
+
) {
|
|
642
|
+
validateReviewerAgents(
|
|
643
|
+
jobKey,
|
|
644
|
+
(step as Readonly<Record<string, unknown>>)['recipients'],
|
|
645
|
+
resolvedAgents,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
596
650
|
}
|
|
597
651
|
if (resolvedSpecs) {
|
|
598
652
|
assertLiteralDeliverableSpec(
|
|
@@ -626,12 +680,84 @@ function validateReviewerAgents(
|
|
|
626
680
|
assertLiteralAgent(
|
|
627
681
|
jobKey,
|
|
628
682
|
`with.reviewers[${i}].agentRef`,
|
|
629
|
-
(
|
|
683
|
+
(
|
|
684
|
+
(reviewer as Record<string, unknown>)['target'] as
|
|
685
|
+
| Readonly<Record<string, unknown>>
|
|
686
|
+
| undefined
|
|
687
|
+
)?.['agentRef'] ?? (reviewer as Record<string, unknown>)['agentRef'],
|
|
630
688
|
resolvedAgents,
|
|
631
689
|
);
|
|
632
690
|
}
|
|
633
691
|
}
|
|
634
692
|
|
|
693
|
+
/**
|
|
694
|
+
* Freeze literal Agent recipients at compile time. Reviewer/decision-gate
|
|
695
|
+
* targets are independent Agent invocations, so their authored `agentRef`
|
|
696
|
+
* selector must not survive as the runtime identity. The pin travels inside
|
|
697
|
+
* the target (whose action schema deliberately permits extension fields) and
|
|
698
|
+
* is copied into AgentDeciderActivity input by the deterministic workflow.
|
|
699
|
+
*/
|
|
700
|
+
function attachResolvedReviewerPins(
|
|
701
|
+
withBlock: Readonly<Record<string, unknown>>,
|
|
702
|
+
resolvedAgents: Readonly<Record<string, ResolvedAgentMeta>>,
|
|
703
|
+
): Readonly<Record<string, unknown>> {
|
|
704
|
+
const pinRecipients = (value: unknown): unknown => {
|
|
705
|
+
if (!Array.isArray(value)) return value;
|
|
706
|
+
return value.map((recipient) => {
|
|
707
|
+
if (
|
|
708
|
+
recipient === null ||
|
|
709
|
+
typeof recipient !== 'object' ||
|
|
710
|
+
Array.isArray(recipient)
|
|
711
|
+
) {
|
|
712
|
+
return recipient;
|
|
713
|
+
}
|
|
714
|
+
const record = recipient as Readonly<Record<string, unknown>>;
|
|
715
|
+
if (record['kind'] !== 'agent') return recipient;
|
|
716
|
+
const target = record['target'];
|
|
717
|
+
if (
|
|
718
|
+
target === null ||
|
|
719
|
+
typeof target !== 'object' ||
|
|
720
|
+
Array.isArray(target)
|
|
721
|
+
) {
|
|
722
|
+
return recipient;
|
|
723
|
+
}
|
|
724
|
+
const targetRecord = target as Readonly<Record<string, unknown>>;
|
|
725
|
+
const agentRef = targetRecord['agentRef'];
|
|
726
|
+
if (!isLiteralStringValue(agentRef)) return recipient;
|
|
727
|
+
const resolved = resolvedAgents[agentRef];
|
|
728
|
+
if (resolved === undefined) return recipient;
|
|
729
|
+
return Object.freeze({
|
|
730
|
+
...record,
|
|
731
|
+
target: Object.freeze({
|
|
732
|
+
...targetRecord,
|
|
733
|
+
agentRevisionPin: Object.freeze({
|
|
734
|
+
agentRevisionId: resolved.agentRevisionId,
|
|
735
|
+
agentContentHash: resolved.agentContentHash,
|
|
736
|
+
agentSlug: resolved.slug,
|
|
737
|
+
}),
|
|
738
|
+
}),
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
const out: Record<string, unknown> = { ...withBlock };
|
|
744
|
+
if ('reviewers' in out) out['reviewers'] = pinRecipients(out['reviewers']);
|
|
745
|
+
if ('recipients' in out) out['recipients'] = pinRecipients(out['recipients']);
|
|
746
|
+
if (Array.isArray(out['escalationChain'])) {
|
|
747
|
+
out['escalationChain'] = out['escalationChain'].map((step) => {
|
|
748
|
+
if (step === null || typeof step !== 'object' || Array.isArray(step)) {
|
|
749
|
+
return step;
|
|
750
|
+
}
|
|
751
|
+
const record = step as Readonly<Record<string, unknown>>;
|
|
752
|
+
return Object.freeze({
|
|
753
|
+
...record,
|
|
754
|
+
recipients: pinRecipients(record['recipients']),
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
return Object.freeze(out);
|
|
759
|
+
}
|
|
760
|
+
|
|
635
761
|
/**
|
|
636
762
|
* Match the literal/expression policy used by `validateAllAuthoredExpressions`:
|
|
637
763
|
* a value is "literal" when it is a string that does NOT contain `${{`. The
|
|
@@ -651,31 +777,22 @@ function assertLiteralAgent(
|
|
|
651
777
|
if (!isLiteralStringValue(value)) {
|
|
652
778
|
return;
|
|
653
779
|
}
|
|
654
|
-
|
|
655
|
-
// `resolvedAgents` is keyed by bare slug, so parse the slug segment off
|
|
656
|
-
// before the existence check. This is the single agent-naming path.
|
|
657
|
-
const slug = agentRefSlug(value);
|
|
658
|
-
if (resolvedAgents[slug] !== undefined) {
|
|
780
|
+
if (resolvedAgents[value] !== undefined) {
|
|
659
781
|
return;
|
|
660
782
|
}
|
|
661
783
|
const knownPreview = formatKnownPreview(Object.keys(resolvedAgents));
|
|
662
784
|
throw new WorkflowDslError(
|
|
663
785
|
WorkflowErrorCode.DSL_UNKNOWN_AGENT,
|
|
664
786
|
`Job '${jobKey}' ${fieldPath} = '${value}' is not a registered agent. Known: [${knownPreview}].`,
|
|
665
|
-
{
|
|
787
|
+
{
|
|
788
|
+
jobKey,
|
|
789
|
+
fieldPath,
|
|
790
|
+
value,
|
|
791
|
+
knownCount: Object.keys(resolvedAgents).length,
|
|
792
|
+
},
|
|
666
793
|
);
|
|
667
794
|
}
|
|
668
795
|
|
|
669
|
-
/**
|
|
670
|
-
* Parse the bare slug off an `agentRef`. An `agentRef` is either a
|
|
671
|
-
* `<slug>` or a `<slug>@<version>` reference; `resolvedAgents` is keyed
|
|
672
|
-
* by bare slug.
|
|
673
|
-
*/
|
|
674
|
-
function agentRefSlug(ref: string): string {
|
|
675
|
-
const at = ref.indexOf('@');
|
|
676
|
-
return at === -1 ? ref : ref.slice(0, at);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
796
|
function assertLiteralDeliverableSpec(
|
|
680
797
|
jobKey: string,
|
|
681
798
|
fieldPath: string,
|
|
@@ -816,11 +933,19 @@ function compileOutputsMap(
|
|
|
816
933
|
*/
|
|
817
934
|
function compileWorkflowOutputs(
|
|
818
935
|
workflow: WorkflowDocument,
|
|
819
|
-
): Readonly<
|
|
936
|
+
): Readonly<
|
|
937
|
+
Record<
|
|
938
|
+
string,
|
|
939
|
+
import('@xemahq/kernel-contracts/workflow').WorkflowOutputDescriptor
|
|
940
|
+
>
|
|
941
|
+
> {
|
|
820
942
|
const raw = workflow.outputs;
|
|
821
943
|
if (!raw || Object.keys(raw).length === 0) {
|
|
822
944
|
return Object.freeze({}) as Readonly<
|
|
823
|
-
Record<
|
|
945
|
+
Record<
|
|
946
|
+
string,
|
|
947
|
+
import('@xemahq/kernel-contracts/workflow').WorkflowOutputDescriptor
|
|
948
|
+
>
|
|
824
949
|
>;
|
|
825
950
|
}
|
|
826
951
|
const seenSlugs = new Set<string>();
|
|
@@ -841,7 +966,11 @@ function compileWorkflowOutputs(
|
|
|
841
966
|
throw new WorkflowDslError(
|
|
842
967
|
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
843
968
|
`outputs.${name}: job "${decl.fromJob}" does not declare an output named "${decl.fromOutput}"`,
|
|
844
|
-
{
|
|
969
|
+
{
|
|
970
|
+
outputName: name,
|
|
971
|
+
fromJob: decl.fromJob,
|
|
972
|
+
fromOutput: decl.fromOutput,
|
|
973
|
+
},
|
|
845
974
|
);
|
|
846
975
|
}
|
|
847
976
|
if (seenSlugs.has(decl.slug)) {
|
|
@@ -881,7 +1010,10 @@ function compileWorkflowOutputs(
|
|
|
881
1010
|
}
|
|
882
1011
|
}
|
|
883
1012
|
return Object.freeze(compiled) as Readonly<
|
|
884
|
-
Record<
|
|
1013
|
+
Record<
|
|
1014
|
+
string,
|
|
1015
|
+
import('@xemahq/kernel-contracts/workflow').WorkflowOutputDescriptor
|
|
1016
|
+
>
|
|
885
1017
|
>;
|
|
886
1018
|
}
|
|
887
1019
|
|
|
@@ -912,7 +1044,11 @@ function validateMatrixGather(doc: WorkflowDocument): void {
|
|
|
912
1044
|
{ jobKey, target },
|
|
913
1045
|
);
|
|
914
1046
|
}
|
|
915
|
-
if (
|
|
1047
|
+
if (
|
|
1048
|
+
!targetDecl.strategy ||
|
|
1049
|
+
(!('matrix' in targetDecl.strategy) &&
|
|
1050
|
+
!('dynamic' in targetDecl.strategy))
|
|
1051
|
+
) {
|
|
916
1052
|
throw new WorkflowDslError(
|
|
917
1053
|
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
918
1054
|
`Job '${jobKey}' matrixGather references '${target}' which has no matrix/dynamic strategy; gathering only makes sense across matrix entries.`,
|
|
@@ -1189,7 +1325,8 @@ function validateNeedsAccess(
|
|
|
1189
1325
|
}
|
|
1190
1326
|
|
|
1191
1327
|
const keyBy = isDynamicMatrix
|
|
1192
|
-
? (upstream.strategy as { dynamic: { keyBy?: string } }).dynamic.keyBy ??
|
|
1328
|
+
? ((upstream.strategy as { dynamic: { keyBy?: string } }).dynamic.keyBy ??
|
|
1329
|
+
null)
|
|
1193
1330
|
: null;
|
|
1194
1331
|
|
|
1195
1332
|
// Bare `needs.X.outputs` is always OK — consumer chooses to receive
|
|
@@ -1199,7 +1336,13 @@ function validateNeedsAccess(
|
|
|
1199
1336
|
const head = access.steps[0]!;
|
|
1200
1337
|
|
|
1201
1338
|
if (head.kind === 'member' && head.name === 'byKey') {
|
|
1202
|
-
validateByKeyChain(
|
|
1339
|
+
validateByKeyChain(
|
|
1340
|
+
access,
|
|
1341
|
+
keyBy,
|
|
1342
|
+
consumerJobKey,
|
|
1343
|
+
consumerBinding,
|
|
1344
|
+
fieldPath,
|
|
1345
|
+
);
|
|
1203
1346
|
return;
|
|
1204
1347
|
}
|
|
1205
1348
|
|
|
@@ -1373,7 +1516,10 @@ function collectMatrixMemberPath(
|
|
|
1373
1516
|
|
|
1374
1517
|
function assertStrategyInvariants(compiled: CompiledRun): void {
|
|
1375
1518
|
for (const job of compiled.jobs) {
|
|
1376
|
-
if (
|
|
1519
|
+
if (
|
|
1520
|
+
job.strategy.kind === MatrixStrategyKind.STATIC &&
|
|
1521
|
+
job.strategy.entries.length === 0
|
|
1522
|
+
) {
|
|
1377
1523
|
throw new WorkflowDslError(
|
|
1378
1524
|
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
1379
1525
|
`Job '${job.jobKey}' static strategy expansion produced zero entries.`,
|
|
@@ -1384,7 +1530,8 @@ function assertStrategyInvariants(compiled: CompiledRun): void {
|
|
|
1384
1530
|
|
|
1385
1531
|
// Permission escalation is already checked per-job. Cycle checked in DAG.
|
|
1386
1532
|
// No-op placeholder kept explicit for the next author.
|
|
1387
|
-
const _permissionRef: Readonly<Record<PermissionResource, PermissionScope>> =
|
|
1533
|
+
const _permissionRef: Readonly<Record<PermissionResource, PermissionScope>> =
|
|
1534
|
+
compiled.permissions;
|
|
1388
1535
|
void _permissionRef;
|
|
1389
1536
|
}
|
|
1390
1537
|
|
|
@@ -1408,7 +1555,13 @@ function validateDeliverableValueExpressions(
|
|
|
1408
1555
|
for (const { source, fieldPath } of expressions) {
|
|
1409
1556
|
const ast = compileExpression(source);
|
|
1410
1557
|
walkDeliverableAccesses(ast, (access) => {
|
|
1411
|
-
validateDeliverableAccess(
|
|
1558
|
+
validateDeliverableAccess(
|
|
1559
|
+
doc,
|
|
1560
|
+
jobKey,
|
|
1561
|
+
fieldPath,
|
|
1562
|
+
access,
|
|
1563
|
+
resolvedSpecs,
|
|
1564
|
+
);
|
|
1412
1565
|
});
|
|
1413
1566
|
}
|
|
1414
1567
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { WorkflowDslError } from '../errors';
|
|
7
7
|
import { ANY_INTERPOLATION_RE } from '../expression/interpolation';
|
|
8
8
|
import type { ActionManifest } from '../types';
|
|
9
|
+
import type { ResolvedAgentMeta } from './types';
|
|
9
10
|
import { isAgentShapedAction } from './action-shape';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -14,11 +15,11 @@ import { isAgentShapedAction } from './action-shape';
|
|
|
14
15
|
* `agentRef` is the SOLE way a workflow names its agent. Terminal shapes:
|
|
15
16
|
* • `null` — non-agent action (the action manifest doesn't expose
|
|
16
17
|
* `agentRef` on its `inputs:` schema).
|
|
17
|
-
* • `{ kind: '
|
|
18
|
-
*
|
|
19
|
-
* • `{ kind: '
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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.
|
|
22
23
|
*
|
|
23
24
|
* An agent-shaped action with no `agentRef` fails fast — there is no
|
|
24
25
|
* inline `mounts` short-form anymore.
|
|
@@ -27,6 +28,7 @@ export function compileManifestSource(
|
|
|
27
28
|
jobKey: string,
|
|
28
29
|
withBlock: Readonly<Record<string, unknown>> | undefined,
|
|
29
30
|
actionManifest: ActionManifest | null,
|
|
31
|
+
resolvedAgents: Readonly<Record<string, ResolvedAgentMeta>>,
|
|
30
32
|
): CompiledManifestSource | null {
|
|
31
33
|
if (!isAgentShapedAction(actionManifest)) return null;
|
|
32
34
|
const block = withBlock ?? {};
|
|
@@ -34,13 +36,26 @@ export function compileManifestSource(
|
|
|
34
36
|
const ref = block['agentRef'];
|
|
35
37
|
if (typeof ref === 'string' && ref.length > 0) {
|
|
36
38
|
if (isLiteralString(ref)) {
|
|
37
|
-
|
|
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
|
+
});
|
|
38
53
|
}
|
|
39
|
-
return Object.freeze({ kind: '
|
|
54
|
+
return Object.freeze({ kind: 'deferred' as const });
|
|
40
55
|
}
|
|
41
56
|
if (ref !== undefined) {
|
|
42
57
|
// ref is present but not a literal string → expression.
|
|
43
|
-
return Object.freeze({ kind: '
|
|
58
|
+
return Object.freeze({ kind: 'deferred' as const });
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
throw new WorkflowDslError(
|