@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,548 +0,0 @@
1
- /**
2
- * Compile-time helpers for `xema/review@*` steps.
3
- *
4
- * The review action's author surface is intentionally small:
5
- *
6
- * uses: xema/review@<semver>
7
- * needs: [<producer>]
8
- * with:
9
- * subject: ${{ needs.<producer>.outputs.deliverable }} # or an array
10
- * redraft: { step: <producer> } # optional
11
- * reviewers: [...]
12
- * policy: { kind: ... }
13
- * iterationTimeoutSeconds: ...
14
- * onIterationTimeout: ...
15
- *
16
- * The worker contract on `reviewWorkflow` is different: it expects
17
- * `subjects: unknown[]` (always-array) and `redraft: { uses, with }`
18
- * (embedded action invocation, not a step ref). This module bridges
19
- * the two:
20
- *
21
- * 1. `validateReviewSteps` — throws on bad author shapes BEFORE
22
- * anything else compiles. Catches missing/dangling step refs,
23
- * shape mismatches, and the forbidden `redraft set without
24
- * subject` combination.
25
- *
26
- * 2. `computeReviewNeedsExtensions` — returns a map of
27
- * `reviewJobKey → string[]` of extra needs the compiler must add
28
- * so the review step's expression context (built from `needs:` at
29
- * dispatch) can resolve every `${{ needs.* }}` reference embedded
30
- * in the redraft step's inherited `with:` block. Authors don't
31
- * have to repeat the producer's transitive needs by hand.
32
- *
33
- * 3. `rewriteReviewStepWith` — produces the IR `with:` for a review
34
- * step: `subject` normalized to `subjects: unknown[]`, and
35
- * `redraft.step` resolved into the named producer's
36
- * `{ uses, with }`.
37
- *
38
- * The action manifest's input schema (in built-in-action-manifests.ts)
39
- * describes the AUTHOR shape. The IR shape produced here is the
40
- * worker's runtime contract — they intentionally diverge.
41
- */
42
-
43
- import {
44
- WorkflowErrorCode,
45
- type CompiledManifestSource,
46
- } from '@xemahq/kernel-contracts/workflow';
47
-
48
- import { WorkflowDslError } from '../errors';
49
- import {
50
- compileExpression,
51
- ExpressionNodeKind,
52
- type ExpressionNode,
53
- } from '../expression';
54
- import { stripInterpolation } from '../expression/interpolation';
55
- import type {
56
- WorkflowDocument,
57
- WorkflowJobDeclaration,
58
- WorkflowStrategyDeclaration,
59
- } from '../types';
60
-
61
- /** Matches `xema/review@<semver>` (1, 2, or 3 dotted version segments). */
62
- const REVIEW_ACTION_RE =
63
- /^xema\/review@[0-9]+(\.[0-9]+(\.[0-9]+(-[A-Za-z0-9.-]+)?)?)?$/;
64
-
65
- /** Matches `xema/agent@<semver>` — only valid `redraft.step` producer kind. */
66
- const AGENT_ACTION_RE =
67
- /^xema\/agent@[0-9]+(\.[0-9]+(\.[0-9]+(-[A-Za-z0-9.-]+)?)?)?$/;
68
-
69
- export function isReviewActionRef(uses: string): boolean {
70
- return REVIEW_ACTION_RE.test(uses);
71
- }
72
-
73
- interface ReviewWithShape {
74
- readonly subject: string | readonly string[] | undefined;
75
- readonly redraft: { readonly step: string } | undefined;
76
- }
77
-
78
- /**
79
- * Per-job redraft-strategy descriptor carried on the IR `redraft` block
80
- * when the producer step has a matrix strategy. Mirrors the worker
81
- * consumer that runs the review workflow — kept JSON-serializable so the
82
- * IR survives Temporal payload encoding.
83
- *
84
- * `static` entries are already cartesian-expanded at compile time;
85
- * `dynamic.fromExpression` is the producer's expression body (already
86
- * `${{ … }}`-stripped) which the parent dispatcher evaluates against
87
- * the review step's expression context before spawning the child
88
- * workflow. `keyBy` is informational only — per-CU keyed targeting is
89
- * a planned future feature; today every matrix entry is always re-run.
90
- */
91
- export type ReviewRedraftStrategyIR =
92
- | {
93
- readonly kind: 'static';
94
- readonly entries: readonly Readonly<Record<string, unknown>>[];
95
- readonly maxParallel: number;
96
- }
97
- | {
98
- readonly kind: 'dynamic';
99
- readonly fromExpression: string;
100
- readonly bindingName: string;
101
- readonly maxEntries: number;
102
- readonly maxParallel: number;
103
- readonly keyBy: string | null;
104
- };
105
-
106
- /**
107
- * Lightweight typed projection of a review step's `with:` block.
108
- * Throws DSL_SEMANTIC_INVALID on malformed shape; downstream consumers
109
- * (rewriter, validators) trust the projection.
110
- */
111
- function projectReviewWith(
112
- jobKey: string,
113
- rawWith: Readonly<Record<string, unknown>> | undefined,
114
- ): ReviewWithShape {
115
- const w = rawWith ?? {};
116
- const subjectRaw = (w as Record<string, unknown>)['subject'];
117
- let subject: ReviewWithShape['subject'];
118
- if (subjectRaw === undefined) {
119
- subject = undefined;
120
- } else if (typeof subjectRaw === 'string') {
121
- if (subjectRaw.length === 0) {
122
- throw new WorkflowDslError(
123
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
124
- `Job '${jobKey}' with.subject must be a non-empty string or array of strings.`,
125
- { jobKey, fieldPath: 'with.subject' },
126
- );
127
- }
128
- subject = subjectRaw;
129
- } else if (Array.isArray(subjectRaw)) {
130
- if (subjectRaw.length === 0) {
131
- throw new WorkflowDslError(
132
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
133
- `Job '${jobKey}' with.subject must be a non-empty array (got []).`,
134
- { jobKey, fieldPath: 'with.subject' },
135
- );
136
- }
137
- for (let i = 0; i < subjectRaw.length; i++) {
138
- const item = subjectRaw[i];
139
- if (typeof item !== 'string' || item.length === 0) {
140
- throw new WorkflowDslError(
141
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
142
- `Job '${jobKey}' with.subject[${i}] must be a non-empty string.`,
143
- { jobKey, fieldPath: `with.subject[${i}]` },
144
- );
145
- }
146
- }
147
- subject = subjectRaw as readonly string[];
148
- } else {
149
- throw new WorkflowDslError(
150
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
151
- `Job '${jobKey}' with.subject must be a string or an array of strings (got ${typeof subjectRaw}).`,
152
- { jobKey, fieldPath: 'with.subject' },
153
- );
154
- }
155
-
156
- const redraftRaw = (w as Record<string, unknown>)['redraft'];
157
- let redraft: ReviewWithShape['redraft'];
158
- if (redraftRaw === undefined) {
159
- redraft = undefined;
160
- } else if (
161
- typeof redraftRaw === 'object' &&
162
- redraftRaw !== null &&
163
- !Array.isArray(redraftRaw)
164
- ) {
165
- const stepRaw = (redraftRaw as Record<string, unknown>)['step'];
166
- if (typeof stepRaw !== 'string' || stepRaw.length === 0) {
167
- throw new WorkflowDslError(
168
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
169
- `Job '${jobKey}' with.redraft.step must be a non-empty string naming a producer step.`,
170
- { jobKey, fieldPath: 'with.redraft.step' },
171
- );
172
- }
173
- redraft = { step: stepRaw };
174
- } else {
175
- throw new WorkflowDslError(
176
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
177
- `Job '${jobKey}' with.redraft must be an object of shape '{ step: <stepName> }'.`,
178
- { jobKey, fieldPath: 'with.redraft' },
179
- );
180
- }
181
-
182
- return { subject, redraft };
183
- }
184
-
185
- /**
186
- * Validate every review step in the workflow. Run before topo sort +
187
- * expression validation so author errors surface with clear messages
188
- * before downstream passes throw less-helpful errors.
189
- */
190
- export function validateReviewSteps(workflow: WorkflowDocument): void {
191
- for (const [jobKey, job] of Object.entries(workflow.jobs)) {
192
- if (!isReviewActionRef(job.uses)) {
193
- continue;
194
- }
195
- const shape = projectReviewWith(jobKey, job.with);
196
-
197
- // `redraft` set without `subject` is forbidden — the producer
198
- // must be authored as a separate step that publishes a deliverable
199
- // as the iter-1 subject. This keeps the standalone draft step
200
- // observable in the run UI and avoids the "ghost step that runs
201
- // but is ignored" footgun.
202
- if (shape.redraft !== undefined && shape.subject === undefined) {
203
- throw new WorkflowDslError(
204
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
205
- `Job '${jobKey}' has 'redraft' set but no 'subject'. Author the producer as a separate step and reference it via 'subject: \${{ needs.${shape.redraft.step}.outputs.deliverable }}'.`,
206
- { jobKey, fieldPath: 'with' },
207
- );
208
- }
209
-
210
- if (shape.redraft !== undefined) {
211
- const stepName = shape.redraft.step;
212
- const producer = workflow.jobs[stepName];
213
- if (!producer) {
214
- throw new WorkflowDslError(
215
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
216
- `Job '${jobKey}' with.redraft.step references unknown job '${stepName}'.`,
217
- { jobKey, fieldPath: 'with.redraft.step', referencedJob: stepName },
218
- );
219
- }
220
- if (stepName === jobKey) {
221
- throw new WorkflowDslError(
222
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
223
- `Job '${jobKey}' with.redraft.step cannot reference itself.`,
224
- { jobKey, fieldPath: 'with.redraft.step' },
225
- );
226
- }
227
- if (!AGENT_ACTION_RE.test(producer.uses)) {
228
- throw new WorkflowDslError(
229
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
230
- `Job '${jobKey}' with.redraft.step = '${stepName}' must reference a step that uses 'xema/agent@<semver>' (got '${producer.uses}').`,
231
- {
232
- jobKey,
233
- fieldPath: 'with.redraft.step',
234
- referencedJob: stepName,
235
- referencedUses: producer.uses,
236
- },
237
- );
238
- }
239
- // Matrix-strategy producers ARE allowed here — on `reject`, the
240
- // worker re-dispatches every matrix entry of the producer in
241
- // parallel (one entry → one redraft agent dispatch). The DSL
242
- // pre-resolves the matrix shape onto the IR `redraft.strategy`
243
- // block so the parent dispatcher (root-run.workflow.ts) can
244
- // evaluate dynamic `from:` expressions against the review step's
245
- // expression context before spawning the child review workflow.
246
- const declaredNeeds = job.needs ?? [];
247
- if (!declaredNeeds.includes(stepName)) {
248
- throw new WorkflowDslError(
249
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
250
- `Job '${jobKey}' with.redraft.step = '${stepName}' must also be listed in 'needs:'. Add 'needs: [${[...declaredNeeds, stepName].map((s) => `'${s}'`).join(', ')}]'.`,
251
- {
252
- jobKey,
253
- fieldPath: 'with.redraft.step',
254
- referencedJob: stepName,
255
- },
256
- );
257
- }
258
- }
259
- }
260
- }
261
-
262
- /**
263
- * Compute extra `needs:` entries each review step requires beyond what
264
- * the author declared. Specifically: for any review step with
265
- * `redraft.step: <producer>`, add `<producer>`'s OWN `needs:` list so
266
- * the review step's expression context (built from its `needs:`) has
267
- * every upstream value the embedded redraft `with:` may reference at
268
- * dispatch time.
269
- *
270
- * Returns an empty map when no review steps have a redraft. The caller
271
- * folds these into the topo-sort input alongside `matrixGather`.
272
- */
273
- export function computeReviewNeedsExtensions(
274
- workflow: WorkflowDocument,
275
- ): Readonly<Record<string, readonly string[]>> {
276
- const out: Record<string, readonly string[]> = {};
277
- for (const [jobKey, job] of Object.entries(workflow.jobs)) {
278
- if (!isReviewActionRef(job.uses)) {
279
- continue;
280
- }
281
- const shape = projectReviewWith(jobKey, job.with);
282
- if (shape.redraft === undefined) {
283
- continue;
284
- }
285
- const producer = workflow.jobs[shape.redraft.step];
286
- if (!producer) {
287
- // validateReviewSteps already throws for this case; defensive only.
288
- continue;
289
- }
290
- const declared = new Set<string>(producer.needs ?? []);
291
- // For dynamic-matrix producers, the `from:` expression is evaluated
292
- // by the parent dispatcher at review-dispatch time against the
293
- // review step's expression context — every `needs.<X>` it
294
- // references MUST therefore be in the review step's `needs:` too.
295
- if (producer.strategy !== undefined && 'dynamic' in producer.strategy) {
296
- const fromRefs = collectNeedsReferencesFromExpression(
297
- producer.strategy.dynamic.from,
298
- );
299
- for (const ref of fromRefs) {
300
- declared.add(ref);
301
- }
302
- }
303
- if (declared.size === 0) {
304
- continue;
305
- }
306
- out[jobKey] = Object.freeze([...declared]) as readonly string[];
307
- }
308
- return Object.freeze(out);
309
- }
310
-
311
- /**
312
- * Walk a `${{ … }}`-wrapped expression body and collect every
313
- * `needs.<X>` root reference. Used by the review-step needs extension
314
- * pass when the redraft producer is a dynamic matrix — its `from:`
315
- * expression resolves at the parent dispatcher's review-dispatch time
316
- * against the review step's expression context, so every referenced
317
- * upstream step must be in the review step's `needs:` too. Returns a
318
- * deduplicated sorted-by-insertion list of step keys.
319
- */
320
- function collectNeedsReferencesFromExpression(
321
- rawExpression: string,
322
- ): readonly string[] {
323
- const body = stripInterpolation(rawExpression);
324
- const ast = compileExpression(body);
325
- const seen = new Set<string>();
326
- walkForNeedsRefs(ast, seen);
327
- return [...seen];
328
- }
329
-
330
- function walkForNeedsRefs(node: ExpressionNode, sink: Set<string>): void {
331
- switch (node.kind) {
332
- case ExpressionNodeKind.MEMBER: {
333
- // Match `needs.<X>` (target is the `needs` identifier).
334
- if (
335
- node.target.kind === ExpressionNodeKind.IDENTIFIER &&
336
- node.target.name === 'needs'
337
- ) {
338
- sink.add(node.property);
339
- }
340
- walkForNeedsRefs(node.target, sink);
341
- return;
342
- }
343
- case ExpressionNodeKind.INDEX:
344
- walkForNeedsRefs(node.target, sink);
345
- walkForNeedsRefs(node.index, sink);
346
- return;
347
- case ExpressionNodeKind.CALL:
348
- for (const arg of node.args) {
349
- walkForNeedsRefs(arg, sink);
350
- }
351
- return;
352
- case ExpressionNodeKind.UNARY_NOT:
353
- walkForNeedsRefs(node.operand, sink);
354
- return;
355
- case ExpressionNodeKind.BINARY_EQ:
356
- case ExpressionNodeKind.BINARY_NEQ:
357
- case ExpressionNodeKind.BINARY_AND:
358
- case ExpressionNodeKind.BINARY_OR:
359
- walkForNeedsRefs(node.left, sink);
360
- walkForNeedsRefs(node.right, sink);
361
- return;
362
- case ExpressionNodeKind.LITERAL:
363
- case ExpressionNodeKind.IDENTIFIER:
364
- return;
365
- }
366
- }
367
-
368
- /**
369
- * Rewrite a review step's `with:` block from the author shape to the
370
- * worker contract:
371
- *
372
- * - Author `subject: <string>` → IR `subjects: [<string>]`
373
- * - Author `subject: <string[]>` → IR `subjects: <string[]>`
374
- * - Author `redraft: { step: X }` → IR `redraft: { uses, with }`
375
- * looked up from the named producer step.
376
- *
377
- * Other fields pass through verbatim. Inputs that are not review steps
378
- * fall back to `payload.with ?? {}` unchanged — callers may always
379
- * route through this helper without an `isReviewActionRef` check.
380
- */
381
- export function rewriteReviewStepWith(
382
- jobKey: string,
383
- payload: WorkflowJobDeclaration,
384
- workflow: WorkflowDocument,
385
- manifestSourcesByJob: Readonly<Record<string, CompiledManifestSource | null>>,
386
- ): Readonly<Record<string, unknown>> {
387
- if (!isReviewActionRef(payload.uses)) {
388
- return payload.with ?? {};
389
- }
390
- const shape = projectReviewWith(jobKey, payload.with);
391
- const out: Record<string, unknown> = { ...(payload.with ?? {}) };
392
- // Drop the author-only fields so the IR carries only worker-facing
393
- // shapes.
394
- delete out['subject'];
395
- delete out['redraft'];
396
-
397
- if (shape.subject !== undefined) {
398
- out['subjects'] =
399
- typeof shape.subject === 'string' ? [shape.subject] : [...shape.subject];
400
- }
401
-
402
- if (shape.redraft !== undefined) {
403
- const producer = workflow.jobs[shape.redraft.step];
404
- if (!producer) {
405
- // validateReviewSteps already throws here; keep defensive.
406
- throw new WorkflowDslError(
407
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
408
- `Job '${jobKey}' with.redraft.step references unknown job '${shape.redraft.step}'.`,
409
- { jobKey, fieldPath: 'with.redraft.step' },
410
- );
411
- }
412
- // Preserve the original step name on the IR `redraft` block. The
413
- // worker uses it as the `artifactScope` override when the redraft's
414
- // agent activity emits its outputs, so the regenerated bytes
415
- // version the SAME logical artifact as the original draft (e.g.
416
- // v2 of `draft.response`) rather than landing under the review
417
- // step's identity (which would create a parallel `review.response`
418
- // artifact and starve the reviewer's iter-2 view of the new draft).
419
- //
420
- // For matrix producers, also carry the producer's matrix shape
421
- // onto the IR so the parent dispatcher (root-run.workflow.ts) can
422
- // expand entries at review-dispatch time and pass the resolved
423
- // bindings into the review child workflow. On `reject`, the worker
424
- // re-dispatches every matrix entry in parallel — one redraft agent
425
- // dispatch per entry.
426
- const ir: Record<string, unknown> = {
427
- step: shape.redraft.step,
428
- uses: producer.uses,
429
- with: producer.with ?? {},
430
- manifestSource: requireRedraftManifestSource(
431
- jobKey,
432
- shape.redraft.step,
433
- manifestSourcesByJob,
434
- ),
435
- };
436
- const strategy = compileRedraftStrategyIR(producer.strategy);
437
- if (strategy !== null) {
438
- ir['strategy'] = strategy;
439
- }
440
- if (producer.matrixGather && producer.matrixGather.length > 0) {
441
- ir['matrixGather'] = [...producer.matrixGather];
442
- }
443
- out['redraft'] = ir;
444
- }
445
-
446
- return out;
447
- }
448
-
449
- function requireRedraftManifestSource(
450
- reviewJobKey: string,
451
- producerJobKey: string,
452
- manifestSourcesByJob: Readonly<Record<string, CompiledManifestSource | null>>,
453
- ): CompiledManifestSource {
454
- const source = manifestSourcesByJob[producerJobKey];
455
- if (source === undefined || source === null) {
456
- throw new WorkflowDslError(
457
- WorkflowErrorCode.DSL_SEMANTIC_INVALID,
458
- `Job '${reviewJobKey}' redraft producer '${producerJobKey}' has no compiled Agent revision source.`,
459
- {
460
- jobKey: reviewJobKey,
461
- fieldPath: 'with.redraft.step',
462
- referencedJob: producerJobKey,
463
- },
464
- );
465
- }
466
- return source;
467
- }
468
-
469
- /**
470
- * Pre-resolve a producer step's authored matrix strategy into a
471
- * JSON-serializable IR descriptor the worker carries through the
472
- * review step's `with.redraft`. `null` for non-matrix producers — the
473
- * worker takes the legacy single-dispatch path when `strategy` is
474
- * absent on the IR `redraft` block. Static matrices are NOT
475
- * cartesian-expanded here (that's the job of `compileStrategy` which
476
- * runs against the producer's own job IR); we propagate the raw axes
477
- * because the worker reuses the same compile-time expansion behavior
478
- * via the parent dispatcher's per-job CompiledJob lookup. Keeping this
479
- * descriptor small and shape-faithful avoids drift with
480
- * `compileStrategy`'s output — the parent dispatcher reads the
481
- * producer's own `CompiledJob.strategy` (Source of truth) and uses
482
- * THIS descriptor only as a discriminator hint + entry-resolution
483
- * fallback. Mirror of the worker contract `ReviewRedraftStrategyIR`.
484
- */
485
- function compileRedraftStrategyIR(
486
- strategy: WorkflowStrategyDeclaration | undefined,
487
- ): ReviewRedraftStrategyIR | null {
488
- if (strategy === undefined) {
489
- return null;
490
- }
491
- if ('matrix' in strategy) {
492
- // Carry static axes; the worker side mirrors `compileStrategy`'s
493
- // cartesian expansion. `maxParallel` defaults align with
494
- // `matrix.ts:resolveMaxParallel` — when absent, the worker uses 8.
495
- const entries = expandStaticMatrixAxes(strategy.matrix);
496
- const maxParallel =
497
- strategy.maxParallel !== undefined && strategy.maxParallel > 0
498
- ? Math.min(strategy.maxParallel, entries.length || 1)
499
- : Math.min(8, Math.max(1, entries.length));
500
- return {
501
- kind: 'static',
502
- entries,
503
- maxParallel,
504
- };
505
- }
506
- const dyn = strategy.dynamic;
507
- const body = stripInterpolation(dyn.from);
508
- const maxEntries = dyn.maxEntries ?? 64;
509
- const maxParallel =
510
- strategy.maxParallel !== undefined && strategy.maxParallel > 0
511
- ? Math.min(strategy.maxParallel, maxEntries)
512
- : Math.min(8, maxEntries);
513
- return {
514
- kind: 'dynamic',
515
- fromExpression: body,
516
- bindingName: dyn.as,
517
- maxEntries,
518
- maxParallel,
519
- keyBy: dyn.keyBy ?? null,
520
- };
521
- }
522
-
523
- /**
524
- * Cartesian expansion of a static-matrix axes block. Mirrors
525
- * `expandStaticMatrix` in `./matrix.ts` minus the cap-checking, which
526
- * has already been enforced on the producer's own CompiledJob by the
527
- * normal compile path. Lexicographic axis order keeps replay
528
- * deterministic.
529
- */
530
- function expandStaticMatrixAxes(
531
- axes: Readonly<Record<string, readonly unknown[]>>,
532
- ): readonly Readonly<Record<string, unknown>>[] {
533
- const axisKeys = Object.keys(axes).sort();
534
- let entries: Record<string, unknown>[] = [{}];
535
- for (const axis of axisKeys) {
536
- const values = axes[axis]!;
537
- const next: Record<string, unknown>[] = [];
538
- for (const partial of entries) {
539
- for (const value of values) {
540
- next.push({ ...partial, [axis]: value });
541
- }
542
- }
543
- entries = next;
544
- }
545
- return entries.map(Object.freeze) as readonly Readonly<
546
- Record<string, unknown>
547
- >[];
548
- }