@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.
- package/dist/payload-codec/lib/blob-store.js +0 -0
- package/dist/payload-codec/lib/blob-store.js.map +1 -1
- package/package.json +3 -4
- 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 -0
- 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,223 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
WorkflowErrorCode,
|
|
3
|
-
WorkflowTriggerKind,
|
|
4
|
-
type ConcurrencyGroup,
|
|
5
|
-
type TriggerPayload,
|
|
6
|
-
} from '@xemahq/kernel-contracts/workflow';
|
|
7
|
-
import { WorkflowDslError } from '../errors';
|
|
8
|
-
import type { WorkflowConcurrencyDeclaration } from '../types';
|
|
9
|
-
import {
|
|
10
|
-
EMPTY_CONTEXT_SEEDS,
|
|
11
|
-
XemaActorKind,
|
|
12
|
-
compileExpression,
|
|
13
|
-
evaluateExpression,
|
|
14
|
-
parseTemplate,
|
|
15
|
-
TemplateSegmentKind,
|
|
16
|
-
type ExpressionContext,
|
|
17
|
-
type ExpressionNode,
|
|
18
|
-
type TemplateSegment,
|
|
19
|
-
} from '../expression';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Deterministically expand the authored concurrency `group` template into a
|
|
23
|
-
* concrete lease key. Two passes:
|
|
24
|
-
*
|
|
25
|
-
* 1. **Compile time** (`compileConcurrencyGroupTemplate`): tokenize and
|
|
26
|
-
* pre-compile every embedded expression. Authoring errors fire here so
|
|
27
|
-
* they block snapshot creation, not dispatch.
|
|
28
|
-
* 2. **Dispatch time** (`expandCompiledConcurrencyTemplate`): evaluate each
|
|
29
|
-
* pre-compiled expression against the trigger / inputs / vars context
|
|
30
|
-
* and stitch the literal + evaluated segments back together.
|
|
31
|
-
*
|
|
32
|
-
* Concurrency `group` is the one DSL field that opts in to partial
|
|
33
|
-
* interpolation (`product-development:${{ inputs.projectId }}`). Every
|
|
34
|
-
* other authored expression site enforces the "fully wrapped or pure
|
|
35
|
-
* literal" policy through `extractInterpolations` — see `compile.ts`.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
interface CompiledTemplateExpressionSegment {
|
|
39
|
-
readonly kind: TemplateSegmentKind.Expression;
|
|
40
|
-
readonly source: string;
|
|
41
|
-
readonly node: ExpressionNode;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface CompiledTemplateLiteralSegment {
|
|
45
|
-
readonly kind: TemplateSegmentKind.Literal;
|
|
46
|
-
readonly value: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
type CompiledTemplateSegment =
|
|
50
|
-
| CompiledTemplateExpressionSegment
|
|
51
|
-
| CompiledTemplateLiteralSegment;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Tokenize and pre-compile every interpolation in the authored template.
|
|
55
|
-
* Pure function: same input always yields the same compiled segments.
|
|
56
|
-
*/
|
|
57
|
-
function compileConcurrencyGroupTemplate(
|
|
58
|
-
authored: string,
|
|
59
|
-
): readonly CompiledTemplateSegment[] {
|
|
60
|
-
const segments = parseTemplate(authored, ['concurrency', 'group']);
|
|
61
|
-
return segments.map((segment) => compileSegment(segment));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function compileSegment(segment: TemplateSegment): CompiledTemplateSegment {
|
|
65
|
-
if (segment.kind === TemplateSegmentKind.Literal) {
|
|
66
|
-
return { kind: TemplateSegmentKind.Literal, value: segment.value };
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
kind: TemplateSegmentKind.Expression,
|
|
70
|
-
source: segment.source,
|
|
71
|
-
node: compileExpression(segment.source),
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function expandCompiledConcurrencyTemplate(
|
|
76
|
-
authored: string,
|
|
77
|
-
segments: readonly CompiledTemplateSegment[],
|
|
78
|
-
ctx: ExpressionContext,
|
|
79
|
-
): string {
|
|
80
|
-
const out: string[] = [];
|
|
81
|
-
for (const segment of segments) {
|
|
82
|
-
if (segment.kind === TemplateSegmentKind.Literal) {
|
|
83
|
-
out.push(segment.value);
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
const value = evaluateExpression(segment.node, ctx);
|
|
87
|
-
out.push(stringifyConcurrencyValue(authored, segment.source, value));
|
|
88
|
-
}
|
|
89
|
-
return out.join('');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function stringifyConcurrencyValue(
|
|
93
|
-
authored: string,
|
|
94
|
-
expressionSource: string,
|
|
95
|
-
value: unknown,
|
|
96
|
-
): string {
|
|
97
|
-
if (value === null || value === undefined) {
|
|
98
|
-
throw new WorkflowDslError(
|
|
99
|
-
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
100
|
-
`Concurrency group expression '${expressionSource}' resolved to ${value === null ? 'null' : 'undefined'} — refuse to build an ambiguous lease key.`,
|
|
101
|
-
{ authored, expression: expressionSource },
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
if (
|
|
105
|
-
typeof value !== 'string' &&
|
|
106
|
-
typeof value !== 'number' &&
|
|
107
|
-
typeof value !== 'boolean'
|
|
108
|
-
) {
|
|
109
|
-
throw new WorkflowDslError(
|
|
110
|
-
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
111
|
-
`Concurrency group expression '${expressionSource}' must resolve to string/number/boolean; got ${typeof value}.`,
|
|
112
|
-
{ authored, expression: expressionSource, resultKind: typeof value },
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
return String(value);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function buildConcurrencyContext(
|
|
119
|
-
trigger: TriggerPayload,
|
|
120
|
-
inputs: Readonly<Record<string, unknown>>,
|
|
121
|
-
vars: Readonly<Record<string, unknown>>,
|
|
122
|
-
): ExpressionContext {
|
|
123
|
-
// Concurrency runs before any job: no `needs`, no matrix, no `job`.
|
|
124
|
-
// `xema.*` is seeded with the run-level identity available at this
|
|
125
|
-
// point (org/project/correlation/actor) so authors may key concurrency
|
|
126
|
-
// groups off `xema.org.id` or `xema.project.id` without reaching into
|
|
127
|
-
// trigger metadata.
|
|
128
|
-
return {
|
|
129
|
-
needs: EMPTY_CONTEXT_SEEDS.needs,
|
|
130
|
-
matrix: EMPTY_CONTEXT_SEEDS.matrix,
|
|
131
|
-
inputs,
|
|
132
|
-
vars,
|
|
133
|
-
secrets: EMPTY_CONTEXT_SEEDS.secrets,
|
|
134
|
-
trigger,
|
|
135
|
-
job: EMPTY_CONTEXT_SEEDS.job,
|
|
136
|
-
payloadCache: EMPTY_CONTEXT_SEEDS.payloadCache,
|
|
137
|
-
xema: {
|
|
138
|
-
org: { id: trigger.orgId },
|
|
139
|
-
project: { id: trigger.projectId },
|
|
140
|
-
run: {
|
|
141
|
-
id: '',
|
|
142
|
-
attempt: 0,
|
|
143
|
-
startedAtIso: trigger.triggeredAt,
|
|
144
|
-
correlationId: trigger.correlationId,
|
|
145
|
-
actor: {
|
|
146
|
-
subject: trigger.actorSubject,
|
|
147
|
-
kind: actorKindFromTrigger(trigger),
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
workflow: { key: '', revisionNumber: 0 },
|
|
151
|
-
job: { key: '', attempt: 0 },
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function actorKindFromTrigger(trigger: TriggerPayload): XemaActorKind {
|
|
157
|
-
if (trigger.actorSubject) return XemaActorKind.User;
|
|
158
|
-
switch (trigger.kind) {
|
|
159
|
-
case WorkflowTriggerKind.WEBHOOK:
|
|
160
|
-
return XemaActorKind.Webhook;
|
|
161
|
-
case WorkflowTriggerKind.SCHEDULE:
|
|
162
|
-
return XemaActorKind.Schedule;
|
|
163
|
-
default:
|
|
164
|
-
return XemaActorKind.System;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Compile + expand the authored concurrency declaration into the runtime
|
|
170
|
-
* `ConcurrencyGroup`. Returns `null` when the workflow declares no
|
|
171
|
-
* concurrency at all.
|
|
172
|
-
*
|
|
173
|
-
* Compile-time errors (malformed template, malformed expression body) and
|
|
174
|
-
* dispatch-time errors (expression evaluates to null/undefined or a
|
|
175
|
-
* non-stringifiable type, expanded key is empty) all surface as typed
|
|
176
|
-
* `WorkflowDslError`s — never silently degraded to a default group.
|
|
177
|
-
*/
|
|
178
|
-
export function compileConcurrency(
|
|
179
|
-
declaration: WorkflowConcurrencyDeclaration | undefined,
|
|
180
|
-
trigger: TriggerPayload,
|
|
181
|
-
inputs: Readonly<Record<string, unknown>>,
|
|
182
|
-
vars: Readonly<Record<string, unknown>>,
|
|
183
|
-
): ConcurrencyGroup | null {
|
|
184
|
-
if (!declaration) return null;
|
|
185
|
-
|
|
186
|
-
const compiledSegments = compileConcurrencyGroupTemplate(declaration.group);
|
|
187
|
-
const ctx = buildConcurrencyContext(trigger, inputs, vars);
|
|
188
|
-
const leaseKey = expandCompiledConcurrencyTemplate(
|
|
189
|
-
declaration.group,
|
|
190
|
-
compiledSegments,
|
|
191
|
-
ctx,
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
if (leaseKey.length === 0) {
|
|
195
|
-
throw new WorkflowDslError(
|
|
196
|
-
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
197
|
-
`Concurrency group expression '${declaration.group}' expanded to an empty string.`,
|
|
198
|
-
{ authored: declaration.group },
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
leaseKey,
|
|
204
|
-
mode: declaration.mode,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Compile-time-only validation hook: parse and pre-compile the concurrency
|
|
210
|
-
* template so authoring errors block snapshot creation. Used by
|
|
211
|
-
* `validateAllAuthoredExpressions` in the main compiler pass; the result is
|
|
212
|
-
* discarded because the dispatch path recompiles from the same source
|
|
213
|
-
* (determinism is preserved by the canonical workflow document hash).
|
|
214
|
-
*/
|
|
215
|
-
export function validateConcurrencyGroupTemplate(
|
|
216
|
-
declaration: WorkflowConcurrencyDeclaration | undefined,
|
|
217
|
-
): void {
|
|
218
|
-
if (!declaration) return;
|
|
219
|
-
// Parse + compile every embedded expression; throw on any syntax error.
|
|
220
|
-
// The returned segments are intentionally discarded — the dispatch path
|
|
221
|
-
// recompiles from the same source string (deterministic).
|
|
222
|
-
compileConcurrencyGroupTemplate(declaration.group);
|
|
223
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
|
|
2
|
-
import { WorkflowDslError } from '../errors';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Topological sort for the job DAG. Returns jobs in a stable order where
|
|
6
|
-
* every job appears after all its `needs` predecessors.
|
|
7
|
-
*
|
|
8
|
-
* Cycle detection: Kahn's algorithm. If any node remains after the queue
|
|
9
|
-
* drains, there's a cycle. Stable order: within each ready set, jobs are
|
|
10
|
-
* visited in input order (so authoring order drives UI layout).
|
|
11
|
-
*
|
|
12
|
-
* Missing-need detection: if any `needs` entry references a jobKey that
|
|
13
|
-
* isn't in the graph, fail fast (can't be silently dropped — would hide
|
|
14
|
-
* a copy-paste bug).
|
|
15
|
-
*/
|
|
16
|
-
export function topologicalSort<T>(
|
|
17
|
-
nodes: readonly { key: string; payload: T; needs: readonly string[] }[],
|
|
18
|
-
): readonly { key: string; payload: T; needs: readonly string[] }[] {
|
|
19
|
-
const byKey = new Map<string, { key: string; payload: T; needs: readonly string[] }>();
|
|
20
|
-
const inputOrder = new Map<string, number>();
|
|
21
|
-
nodes.forEach((node, i) => {
|
|
22
|
-
if (byKey.has(node.key)) {
|
|
23
|
-
throw new WorkflowDslError(
|
|
24
|
-
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
25
|
-
`Duplicate job key: ${node.key}`,
|
|
26
|
-
{ key: node.key },
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
byKey.set(node.key, node);
|
|
30
|
-
inputOrder.set(node.key, i);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// Validate needs references before building indegree — unknown refs are
|
|
34
|
-
// a structural error, not a cycle error.
|
|
35
|
-
for (const node of nodes) {
|
|
36
|
-
for (const needKey of node.needs) {
|
|
37
|
-
if (!byKey.has(needKey)) {
|
|
38
|
-
throw new WorkflowDslError(
|
|
39
|
-
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
40
|
-
`Job '${node.key}' declares needs: [${needKey}] but no such job exists.`,
|
|
41
|
-
{ jobKey: node.key, missingNeed: needKey },
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
if (needKey === node.key) {
|
|
45
|
-
throw new WorkflowDslError(
|
|
46
|
-
WorkflowErrorCode.DSL_CYCLE_DETECTED,
|
|
47
|
-
`Job '${node.key}' cannot depend on itself.`,
|
|
48
|
-
{ jobKey: node.key },
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const indegree = new Map<string, number>();
|
|
55
|
-
const dependents = new Map<string, string[]>();
|
|
56
|
-
for (const node of nodes) {
|
|
57
|
-
indegree.set(node.key, node.needs.length);
|
|
58
|
-
dependents.set(node.key, []);
|
|
59
|
-
}
|
|
60
|
-
for (const node of nodes) {
|
|
61
|
-
for (const needKey of node.needs) {
|
|
62
|
-
dependents.get(needKey)!.push(node.key);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const ready: string[] = [];
|
|
67
|
-
for (const [key, d] of indegree) {
|
|
68
|
-
if (d === 0) ready.push(key);
|
|
69
|
-
}
|
|
70
|
-
stableSortByInputOrder(ready, inputOrder);
|
|
71
|
-
|
|
72
|
-
const sorted: { key: string; payload: T; needs: readonly string[] }[] = [];
|
|
73
|
-
while (ready.length > 0) {
|
|
74
|
-
const key = ready.shift()!;
|
|
75
|
-
sorted.push(byKey.get(key)!);
|
|
76
|
-
const children = dependents.get(key)!;
|
|
77
|
-
const newlyReady: string[] = [];
|
|
78
|
-
for (const child of children) {
|
|
79
|
-
const next = indegree.get(child)! - 1;
|
|
80
|
-
indegree.set(child, next);
|
|
81
|
-
if (next === 0) newlyReady.push(child);
|
|
82
|
-
}
|
|
83
|
-
stableSortByInputOrder(newlyReady, inputOrder);
|
|
84
|
-
ready.push(...newlyReady);
|
|
85
|
-
stableSortByInputOrder(ready, inputOrder);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (sorted.length !== nodes.length) {
|
|
89
|
-
const stuck = [...indegree.entries()]
|
|
90
|
-
.filter(([, d]) => d > 0)
|
|
91
|
-
.map(([k]) => k);
|
|
92
|
-
throw new WorkflowDslError(
|
|
93
|
-
WorkflowErrorCode.DSL_CYCLE_DETECTED,
|
|
94
|
-
`Workflow DAG contains a cycle. Jobs in cycle or downstream of one: ${stuck.join(', ')}`,
|
|
95
|
-
{ stuck },
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return sorted;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function stableSortByInputOrder(keys: string[], inputOrder: Map<string, number>): void {
|
|
103
|
-
keys.sort((a, b) => {
|
|
104
|
-
const ai = inputOrder.get(a)!;
|
|
105
|
-
const bi = inputOrder.get(b)!;
|
|
106
|
-
return ai - bi;
|
|
107
|
-
});
|
|
108
|
-
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// ── Workflow-level decision-gate defaults (`defaults.gate`) ──
|
|
3
|
-
//
|
|
4
|
-
// Gate-heavy workflows (e.g. the software-dev product-development pipeline)
|
|
5
|
-
// repeat the same decision-gate configuration on every gate job:
|
|
6
|
-
// timeoutSeconds, onTimeout, autonomyMode, recipients, policy. `defaults.gate`
|
|
7
|
-
// lets the author declare that configuration ONCE at the workflow level; the
|
|
8
|
-
// compiler folds it into each gate job's `with:` before any other compile
|
|
9
|
-
// pass runs, so downstream validation, expression compilation, and the
|
|
10
|
-
// CompiledRun snapshot all observe the merged document — expressions inside
|
|
11
|
-
// defaults compile per-job exactly as if written inline.
|
|
12
|
-
//
|
|
13
|
-
// Merge contract (deterministic, no silent deep-merging):
|
|
14
|
-
// • Applies ONLY to jobs whose `uses:` resolves to the `xema/decision-gate`
|
|
15
|
-
// action id — matched on the resolved ref's id, regardless of version.
|
|
16
|
-
// • Fills ABSENT `with:` keys only. Explicit per-gate values always win.
|
|
17
|
-
// • Fill is per top-level key: a gate that declares its own `policy` or
|
|
18
|
-
// `recipients` keeps that value verbatim — there is no partial merge
|
|
19
|
-
// inside a key's object/array value.
|
|
20
|
-
// • Fail-fast validation: every `defaults.gate` key must be declared in
|
|
21
|
-
// the resolved gate manifest's `inputs.properties` (guards drift between
|
|
22
|
-
// the DSL schema's closed key set and the pinned gate action version).
|
|
23
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
-
|
|
25
|
-
import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
|
|
26
|
-
import { WorkflowDslError } from '../errors';
|
|
27
|
-
import { compileExpression, extractInterpolations } from '../expression';
|
|
28
|
-
import type { WorkflowDocument, WorkflowGateDefaults } from '../types';
|
|
29
|
-
import type { ResolvedRef } from './types';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Canonical action id of the decision-gate primitive. `defaults.gate`
|
|
33
|
-
* targets jobs by this id — never by the raw `uses:` string — so any
|
|
34
|
-
* pinned version (`xema/decision-gate@1.2.0`, `@2.x`, …) participates.
|
|
35
|
-
*/
|
|
36
|
-
export const DECISION_GATE_ACTION_ID = 'xema/decision-gate';
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Fold `defaults.gate` into every decision-gate job's `with:` block and
|
|
40
|
-
* return the (possibly rewritten) document. Pure: never mutates the input.
|
|
41
|
-
*
|
|
42
|
-
* Runs BEFORE all other compile passes so the merged `with:` flows through
|
|
43
|
-
* expression validation, needs-shape validation, review rewriting, the
|
|
44
|
-
* action input-schema pin, and the snapshot hash exactly like authored
|
|
45
|
-
* values.
|
|
46
|
-
*
|
|
47
|
-
* Jobs whose `uses:` is missing from `resolvedRefs` are skipped here — the
|
|
48
|
-
* per-job compile loop already fails them with `DSL_UNKNOWN_ACTION`, and
|
|
49
|
-
* pre-empting that error would reorder author-facing diagnostics.
|
|
50
|
-
*/
|
|
51
|
-
export function applyGateDefaults(
|
|
52
|
-
workflow: WorkflowDocument,
|
|
53
|
-
resolvedRefs: Readonly<Record<string, ResolvedRef>>,
|
|
54
|
-
): WorkflowDocument {
|
|
55
|
-
const gate = workflow.defaults?.gate;
|
|
56
|
-
if (gate === undefined) return workflow;
|
|
57
|
-
const gateEntries = Object.entries(gate).filter(
|
|
58
|
-
([, value]) => value !== undefined,
|
|
59
|
-
);
|
|
60
|
-
if (gateEntries.length === 0) return workflow;
|
|
61
|
-
|
|
62
|
-
// Validate every `${{ … }}` expression inside the defaults block up
|
|
63
|
-
// front — even when no gate job consumes it. A malformed expression in
|
|
64
|
-
// `defaults.gate` is an authoring error regardless of merge fan-out;
|
|
65
|
-
// deferring it until a gate job appears would be a silent latency.
|
|
66
|
-
for (const ext of extractInterpolations(
|
|
67
|
-
gate as Readonly<Record<string, unknown>>,
|
|
68
|
-
['defaults', 'gate'],
|
|
69
|
-
)) {
|
|
70
|
-
compileExpression(ext.source);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let anyChanged = false;
|
|
74
|
-
const rewrittenJobs: Record<string, WorkflowDocument['jobs'][string]> = {};
|
|
75
|
-
for (const [jobKey, job] of Object.entries(workflow.jobs)) {
|
|
76
|
-
const resolved = resolvedRefs[job.uses];
|
|
77
|
-
if (
|
|
78
|
-
resolved === undefined ||
|
|
79
|
-
resolved.isReusableWorkflow ||
|
|
80
|
-
resolved.id !== DECISION_GATE_ACTION_ID
|
|
81
|
-
) {
|
|
82
|
-
rewrittenJobs[jobKey] = job;
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
assertGateDefaultKeysDeclared(jobKey, gate, resolved);
|
|
87
|
-
|
|
88
|
-
const authored = job.with ?? {};
|
|
89
|
-
let jobChanged = false;
|
|
90
|
-
const merged: Record<string, unknown> = { ...authored };
|
|
91
|
-
for (const [key, value] of gateEntries) {
|
|
92
|
-
if (Object.hasOwn(authored, key)) continue;
|
|
93
|
-
merged[key] = value;
|
|
94
|
-
jobChanged = true;
|
|
95
|
-
}
|
|
96
|
-
if (!jobChanged) {
|
|
97
|
-
rewrittenJobs[jobKey] = job;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
anyChanged = true;
|
|
101
|
-
rewrittenJobs[jobKey] = { ...job, with: merged };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (!anyChanged) return workflow;
|
|
105
|
-
return { ...workflow, jobs: rewrittenJobs };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Fail-fast subset check: every `defaults.gate` key must be a declared
|
|
110
|
-
* input of the resolved gate action version. The JSON schema already
|
|
111
|
-
* closes the key set to the canonical decision-gate input names; this
|
|
112
|
-
* check guards manifest drift (e.g. a future gate version renaming or
|
|
113
|
-
* dropping an input while a workflow still defaults it).
|
|
114
|
-
*
|
|
115
|
-
* Skipped only when the manifest declares no introspectable
|
|
116
|
-
* `inputs.properties` map — nothing to prove against; the worker's
|
|
117
|
-
* pinned input-schema validation still rejects unknown keys at dispatch.
|
|
118
|
-
*/
|
|
119
|
-
function assertGateDefaultKeysDeclared(
|
|
120
|
-
jobKey: string,
|
|
121
|
-
gate: WorkflowGateDefaults,
|
|
122
|
-
resolved: ResolvedRef,
|
|
123
|
-
): void {
|
|
124
|
-
const inputs = resolved.actionManifest?.spec.inputs;
|
|
125
|
-
const properties =
|
|
126
|
-
inputs && typeof inputs === 'object'
|
|
127
|
-
? (inputs as { properties?: unknown }).properties
|
|
128
|
-
: undefined;
|
|
129
|
-
if (
|
|
130
|
-
properties === null ||
|
|
131
|
-
properties === undefined ||
|
|
132
|
-
typeof properties !== 'object' ||
|
|
133
|
-
Array.isArray(properties)
|
|
134
|
-
) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
const declared = properties as Readonly<Record<string, unknown>>;
|
|
138
|
-
for (const [key, value] of Object.entries(gate)) {
|
|
139
|
-
if (value === undefined) continue;
|
|
140
|
-
if (Object.hasOwn(declared, key)) continue;
|
|
141
|
-
throw new WorkflowDslError(
|
|
142
|
-
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
143
|
-
`defaults.gate.${key} is not a declared input of ${resolved.id}@${resolved.version} (consumed by job '${jobKey}'). Declared inputs: [${Object.keys(declared).sort((a, b) => a.localeCompare(b)).join(', ')}].`,
|
|
144
|
-
{
|
|
145
|
-
jobKey,
|
|
146
|
-
key,
|
|
147
|
-
actionId: resolved.id,
|
|
148
|
-
actionVersion: resolved.version,
|
|
149
|
-
declaredInputs: Object.keys(declared),
|
|
150
|
-
},
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export { compileWorkflow } from './compile';
|
|
2
|
-
export { DECISION_GATE_ACTION_ID, applyGateDefaults } from './gate-defaults';
|
|
3
|
-
export { canonicalJsonSha256, canonicalJsonStringify } from './canonical-json';
|
|
4
|
-
export { compileManifestSource } from './manifest-source';
|
|
5
|
-
export { getWorkspaceManifestContract, isAgentShapedAction } from './action-shape';
|
|
6
|
-
export type {
|
|
7
|
-
CompileInput,
|
|
8
|
-
ResolvedAgentMeta,
|
|
9
|
-
ResolvedDeliverableSpec,
|
|
10
|
-
ResolvedRef,
|
|
11
|
-
} from './types';
|