@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,280 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Character-level scanner over TypeScript Zod source, used to locate the
|
|
3
|
-
* `z.object({ … })` literals a deliverable spec declares at the TOP level
|
|
4
|
-
* and to read their field names.
|
|
5
|
-
*
|
|
6
|
-
* Deliberately NOT a TypeScript parse or a Zod evaluation: introspection
|
|
7
|
-
* runs inside the DSL compiler, the CI gate, and a request-scoped API
|
|
8
|
-
* handler, none of which may transpile or execute spec-authored code.
|
|
9
|
-
* A scanner is enough because the only question asked is "which
|
|
10
|
-
* identifiers sit at depth 0 of this object literal".
|
|
11
|
-
*
|
|
12
|
-
* What it tracks, and why the naive regex it replaced could not:
|
|
13
|
-
* - comments, strings, template literals and regex literals, so a
|
|
14
|
-
* `z.object(`, a brace, or a comma appearing INSIDE one of those is
|
|
15
|
-
* never read as code;
|
|
16
|
-
* - brace + paren depth, so a NESTED `z.object(` (a field's own shape)
|
|
17
|
-
* is never confused with a top-level schema declaration;
|
|
18
|
-
* - the `const <Name> =` binding preceding each top-level literal, so a
|
|
19
|
-
* spec can name its contract instead of relying on source order.
|
|
20
|
-
*
|
|
21
|
-
* All three walkers below share ONE trivia primitive (`skipTriviaAt`) so
|
|
22
|
-
* they cannot drift into disagreeing about what counts as code.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/** A `z.object({ … })` literal declared at the top level of the source. */
|
|
26
|
-
export interface TopLevelZodObjectDeclaration {
|
|
27
|
-
/** Binding name (`export const Foo = z.object({…})` → `Foo`), or null when anonymous. */
|
|
28
|
-
readonly exportName: string | null;
|
|
29
|
-
/** Field identifiers at depth 0 of the literal, in source order, de-duplicated. */
|
|
30
|
-
readonly keys: readonly string[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* If a comment / string / template / regex literal STARTS at `index`,
|
|
35
|
-
* return the index just past its end; otherwise return `index` unchanged.
|
|
36
|
-
* An unterminated token consumes the rest of the source, which makes the
|
|
37
|
-
* callers treat it as "no readable shape from here" rather than
|
|
38
|
-
* re-entering code state mid-literal.
|
|
39
|
-
*
|
|
40
|
-
* Regex detection is bounded on purpose: a `/` is treated as a regex only
|
|
41
|
-
* when the previous non-whitespace code character is `(` or `,` — the only
|
|
42
|
-
* positions a regex occupies in a schema-declaration module
|
|
43
|
-
* (`.regex(/…/)`, `.refine(x, /…/)`). Division never appears in that
|
|
44
|
-
* position, so there is no ambiguous case to guess at.
|
|
45
|
-
*/
|
|
46
|
-
function skipTriviaAt(source: string, index: number, prevCode: string): number {
|
|
47
|
-
const ch = source[index];
|
|
48
|
-
const next = source[index + 1];
|
|
49
|
-
|
|
50
|
-
if (ch === '/' && next === '/') return endOfLineComment(source, index + 2);
|
|
51
|
-
if (ch === '/' && next === '*') return endOfBlockComment(source, index + 2);
|
|
52
|
-
if (ch === "'" || ch === '"' || ch === '`') {
|
|
53
|
-
return endOfQuoted(source, index + 1, ch);
|
|
54
|
-
}
|
|
55
|
-
if (ch === '/' && (prevCode === '(' || prevCode === ',')) {
|
|
56
|
-
return endOfQuoted(source, index + 1, '/');
|
|
57
|
-
}
|
|
58
|
-
return index;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function endOfLineComment(source: string, from: number): number {
|
|
62
|
-
const nl = source.indexOf('\n', from);
|
|
63
|
-
return nl === -1 ? source.length : nl + 1;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function endOfBlockComment(source: string, from: number): number {
|
|
67
|
-
const end = source.indexOf('*/', from);
|
|
68
|
-
return end === -1 ? source.length : end + 2;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** End of a `'…'` / `"…"` / `` `…` `` / `/…/` token, honoring backslash escapes. */
|
|
72
|
-
function endOfQuoted(source: string, from: number, terminator: string): number {
|
|
73
|
-
for (let i = from; i < source.length; i += 1) {
|
|
74
|
-
const ch = source[i];
|
|
75
|
-
if (ch === '\\') {
|
|
76
|
-
i += 1;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (ch === terminator) return i + 1;
|
|
80
|
-
}
|
|
81
|
-
return source.length;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Nesting counters shared by the depth-tracking walkers. */
|
|
85
|
-
interface Depths {
|
|
86
|
-
brace: number;
|
|
87
|
-
paren: number;
|
|
88
|
-
bracket: number;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function newDepths(): Depths {
|
|
92
|
-
return { brace: 0, paren: 0, bracket: 0 };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function applyDepth(depths: Depths, ch: string | undefined): void {
|
|
96
|
-
if (ch === '{') depths.brace += 1;
|
|
97
|
-
else if (ch === '}') depths.brace = Math.max(0, depths.brace - 1);
|
|
98
|
-
else if (ch === '(') depths.paren += 1;
|
|
99
|
-
else if (ch === ')') depths.paren = Math.max(0, depths.paren - 1);
|
|
100
|
-
else if (ch === '[') depths.bracket += 1;
|
|
101
|
-
else if (ch === ']') depths.bracket = Math.max(0, depths.bracket - 1);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function isAtTopLevel(depths: Depths): boolean {
|
|
105
|
-
return (
|
|
106
|
-
depths.brace === 0 && depths.paren === 0 && depths.bracket === 0
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Find every `z.object({ … })` literal that sits at depth 0 — i.e. every
|
|
112
|
-
* schema the module declares in its own right, as opposed to a shape
|
|
113
|
-
* nested inside another schema.
|
|
114
|
-
*
|
|
115
|
-
* Literals whose braces never close are skipped: an unbalanced source has
|
|
116
|
-
* no readable shape, and reporting a truncated key list would be worse
|
|
117
|
-
* than reporting none.
|
|
118
|
-
*/
|
|
119
|
-
export function scanTopLevelZodObjectDeclarations(
|
|
120
|
-
source: string,
|
|
121
|
-
): readonly TopLevelZodObjectDeclaration[] {
|
|
122
|
-
const out: TopLevelZodObjectDeclaration[] = [];
|
|
123
|
-
const depths = newDepths();
|
|
124
|
-
let prevCode = '';
|
|
125
|
-
let i = 0;
|
|
126
|
-
|
|
127
|
-
while (i < source.length) {
|
|
128
|
-
const afterTrivia = skipTriviaAt(source, i, prevCode);
|
|
129
|
-
if (afterTrivia !== i) {
|
|
130
|
-
i = afterTrivia;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (isAtTopLevel(depths) && matchesZodObjectAt(source, i)) {
|
|
135
|
-
const bodyStart = openingBraceIndexAfterZodObject(source, i);
|
|
136
|
-
const bodyEnd =
|
|
137
|
-
bodyStart === null ? null : matchingBraceIndex(source, bodyStart);
|
|
138
|
-
if (bodyStart !== null && bodyEnd !== null) {
|
|
139
|
-
out.push({
|
|
140
|
-
exportName: bindingNameBefore(source, i),
|
|
141
|
-
keys: parseObjectKeysAtTopLevel(source.slice(bodyStart, bodyEnd)),
|
|
142
|
-
});
|
|
143
|
-
// Resume AFTER the literal: its interior is already accounted
|
|
144
|
-
// for, and re-walking it could only produce nested matches we
|
|
145
|
-
// deliberately exclude.
|
|
146
|
-
i = bodyEnd + 1;
|
|
147
|
-
prevCode = '}';
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const ch = source[i];
|
|
153
|
-
applyDepth(depths, ch);
|
|
154
|
-
if (ch !== undefined && !/\s/.test(ch)) prevCode = ch;
|
|
155
|
-
i += 1;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return out;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** True when `z.object` starts exactly at `index` and is not part of a longer identifier. */
|
|
162
|
-
function matchesZodObjectAt(source: string, index: number): boolean {
|
|
163
|
-
if (!source.startsWith('z.object', index)) return false;
|
|
164
|
-
const prev = index > 0 ? (source[index - 1] ?? '') : '';
|
|
165
|
-
return !/[A-Za-z0-9_$.]/.test(prev);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Index just past the `{` of `z.object ( {`, or null when the call is not an object literal. */
|
|
169
|
-
function openingBraceIndexAfterZodObject(
|
|
170
|
-
source: string,
|
|
171
|
-
index: number,
|
|
172
|
-
): number | null {
|
|
173
|
-
let i = skipWhitespace(source, index + 'z.object'.length);
|
|
174
|
-
if (source[i] !== '(') return null;
|
|
175
|
-
i = skipWhitespace(source, i + 1);
|
|
176
|
-
if (source[i] !== '{') return null;
|
|
177
|
-
return i + 1;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Index of the `}` closing the literal that starts at `bodyStart`, or null
|
|
182
|
-
* when it never closes.
|
|
183
|
-
*/
|
|
184
|
-
function matchingBraceIndex(source: string, bodyStart: number): number | null {
|
|
185
|
-
let depth = 1;
|
|
186
|
-
let prevCode = '{';
|
|
187
|
-
let i = bodyStart;
|
|
188
|
-
|
|
189
|
-
while (i < source.length) {
|
|
190
|
-
const afterTrivia = skipTriviaAt(source, i, prevCode);
|
|
191
|
-
if (afterTrivia !== i) {
|
|
192
|
-
i = afterTrivia;
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
const ch = source[i];
|
|
196
|
-
if (ch === '{') depth += 1;
|
|
197
|
-
else if (ch === '}') {
|
|
198
|
-
depth -= 1;
|
|
199
|
-
if (depth === 0) return i;
|
|
200
|
-
}
|
|
201
|
-
if (ch !== undefined && !/\s/.test(ch)) prevCode = ch;
|
|
202
|
-
i += 1;
|
|
203
|
-
}
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const BINDING_LOOKBEHIND = 400;
|
|
208
|
-
const BINDING_TAIL =
|
|
209
|
-
/(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::[^=]*)?=\s*$/;
|
|
210
|
-
|
|
211
|
-
/** Name bound to the literal starting at `index` (`export const Foo = z.object(`), or null. */
|
|
212
|
-
function bindingNameBefore(source: string, index: number): string | null {
|
|
213
|
-
const from = Math.max(0, index - BINDING_LOOKBEHIND);
|
|
214
|
-
return BINDING_TAIL.exec(source.slice(from, index))?.[1] ?? null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function skipWhitespace(source: string, index: number): number {
|
|
218
|
-
let i = index;
|
|
219
|
-
while (i < source.length && /\s/.test(source[i]!)) i += 1;
|
|
220
|
-
return i;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Field identifiers at depth 0 of an object-literal body.
|
|
225
|
-
*
|
|
226
|
-
* Splits on depth-0 commas and reads the leading key of each segment, so
|
|
227
|
-
* a key is recorded exactly once. (The predecessor scanned for a key after
|
|
228
|
-
* BOTH the comma and the newline that followed it, emitting every
|
|
229
|
-
* multi-line key twice — harmless for a set-membership test, but the
|
|
230
|
-
* duplicates leaked into compiler error messages and the "known fields"
|
|
231
|
-
* preview shown to authors.)
|
|
232
|
-
*/
|
|
233
|
-
function parseObjectKeysAtTopLevel(body: string): readonly string[] {
|
|
234
|
-
const keys: string[] = [];
|
|
235
|
-
const seen = new Set<string>();
|
|
236
|
-
for (const segment of splitTopLevelSegments(body)) {
|
|
237
|
-
const key = leadingKeyOf(segment);
|
|
238
|
-
if (key !== null && !seen.has(key)) {
|
|
239
|
-
seen.add(key);
|
|
240
|
-
keys.push(key);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return Object.freeze(keys);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function splitTopLevelSegments(body: string): readonly string[] {
|
|
247
|
-
const segments: string[] = [];
|
|
248
|
-
const depths = newDepths();
|
|
249
|
-
let prevCode = '';
|
|
250
|
-
let start = 0;
|
|
251
|
-
let i = 0;
|
|
252
|
-
|
|
253
|
-
while (i < body.length) {
|
|
254
|
-
const afterTrivia = skipTriviaAt(body, i, prevCode);
|
|
255
|
-
if (afterTrivia !== i) {
|
|
256
|
-
i = afterTrivia;
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
const ch = body[i];
|
|
260
|
-
if (ch === ',' && isAtTopLevel(depths)) {
|
|
261
|
-
segments.push(body.slice(start, i));
|
|
262
|
-
start = i + 1;
|
|
263
|
-
} else {
|
|
264
|
-
applyDepth(depths, ch);
|
|
265
|
-
}
|
|
266
|
-
if (ch !== undefined && !/\s/.test(ch)) prevCode = ch;
|
|
267
|
-
i += 1;
|
|
268
|
-
}
|
|
269
|
-
segments.push(body.slice(start));
|
|
270
|
-
return segments;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const LEADING_TRIVIA = /^(?:\s|\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)+/;
|
|
274
|
-
const LEADING_KEY = /^(?:([A-Za-z_$][A-Za-z0-9_$]*)|'([^']+)'|"([^"]+)")\s*:/;
|
|
275
|
-
|
|
276
|
-
function leadingKeyOf(segment: string): string | null {
|
|
277
|
-
const match = LEADING_KEY.exec(segment.replace(LEADING_TRIVIA, ''));
|
|
278
|
-
if (!match) return null;
|
|
279
|
-
return match[1] ?? match[2] ?? match[3] ?? null;
|
|
280
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// ── Dispatch-input descriptor extraction ──
|
|
3
|
-
//
|
|
4
|
-
// Shared, tree-shakable module that projects a parsed workflow
|
|
5
|
-
// document's `on.workflow_dispatch.inputs` declaration into a
|
|
6
|
-
// normalized `DispatchInputDescriptor[]` the frontend renders as a
|
|
7
|
-
// dispatch form and the engine consumes for endpoint-returned schema
|
|
8
|
-
// metadata.
|
|
9
|
-
//
|
|
10
|
-
// Single source of truth: the engine and the web frontend both call
|
|
11
|
-
// this function. Keeping the extraction + field-type enumeration in
|
|
12
|
-
// one place removes the "two validation authorities" anti-pattern
|
|
13
|
-
// (per CLAUDE.md fail-fast rule). The pure extraction logic here is
|
|
14
|
-
// NOT validation — the engine's DSL validator (in `lib/validate.ts`)
|
|
15
|
-
// remains the sole validator at compile + dispatch time.
|
|
16
|
-
//
|
|
17
|
-
// No Ajv, no js-yaml — this module is deliberately self-contained so
|
|
18
|
-
// the frontend bundle can import it without pulling heavy
|
|
19
|
-
// transitive deps through tree-shaking.
|
|
20
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Closed set of field types surfaced to the dispatch form. Mirrors
|
|
24
|
-
* `WorkflowInputDeclaration.type` but narrows to the subset the form
|
|
25
|
-
* renderer knows how to render. Unknown type values in the parsed
|
|
26
|
-
* document cause the field to be skipped (fail-fast — no silent
|
|
27
|
-
* downgrade to a string input).
|
|
28
|
-
*/
|
|
29
|
-
export enum DispatchInputFieldType {
|
|
30
|
-
String = 'string',
|
|
31
|
-
Number = 'number',
|
|
32
|
-
Integer = 'integer',
|
|
33
|
-
Boolean = 'boolean',
|
|
34
|
-
Object = 'object',
|
|
35
|
-
Array = 'array',
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Normalized descriptor the dispatch form consumes. Flat + closed
|
|
40
|
-
* shape so renderer code doesn't have to probe optional fields.
|
|
41
|
-
*/
|
|
42
|
-
export interface DispatchInputDescriptor {
|
|
43
|
-
readonly name: string;
|
|
44
|
-
readonly type: DispatchInputFieldType;
|
|
45
|
-
readonly required: boolean;
|
|
46
|
-
readonly description: string | null;
|
|
47
|
-
/** String values only — non-string enum members are dropped. */
|
|
48
|
-
readonly enumValues: readonly string[] | null;
|
|
49
|
-
/** For `type: string`, a hint for multiline rendering. Closed set. */
|
|
50
|
-
readonly format: DispatchInputStringFormat | null;
|
|
51
|
-
/** For `type: array`, the item type (only `string[]` is form-rendered; other item types fall to raw JSON). */
|
|
52
|
-
readonly itemType: DispatchInputFieldType | null;
|
|
53
|
-
readonly defaultValue: unknown;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export enum DispatchInputStringFormat {
|
|
57
|
-
Multiline = 'multiline',
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Extracts descriptors from a parsed workflow document. Returns an
|
|
62
|
-
* empty array when the document declares no dispatch inputs (reusable
|
|
63
|
-
* workflows or schedule/webhook-only workflows). Never throws — a
|
|
64
|
-
* malformed inputs bag (unknown `type`, non-object entries) drops the
|
|
65
|
-
* offending field rather than failing the whole extraction, so the
|
|
66
|
-
* dispatch UI stays renderable even if one input declaration is
|
|
67
|
-
* stale. The engine's compile path remains authoritative for
|
|
68
|
-
* validation.
|
|
69
|
-
*/
|
|
70
|
-
export function extractDispatchInputDescriptors(
|
|
71
|
-
parsedDocument: unknown,
|
|
72
|
-
): readonly DispatchInputDescriptor[] {
|
|
73
|
-
const inputs = readDispatchInputsBag(parsedDocument);
|
|
74
|
-
if (!inputs) return [];
|
|
75
|
-
const descriptors: DispatchInputDescriptor[] = [];
|
|
76
|
-
for (const [name, raw] of Object.entries(inputs)) {
|
|
77
|
-
const descriptor = toDescriptor(name, raw);
|
|
78
|
-
if (descriptor !== null) descriptors.push(descriptor);
|
|
79
|
-
}
|
|
80
|
-
return descriptors;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function readDispatchInputsBag(
|
|
84
|
-
parsedDocument: unknown,
|
|
85
|
-
): Record<string, unknown> | null {
|
|
86
|
-
if (!parsedDocument || typeof parsedDocument !== 'object') return null;
|
|
87
|
-
const doc = parsedDocument as Record<string, unknown>;
|
|
88
|
-
const on = doc['on'];
|
|
89
|
-
if (!on || typeof on !== 'object') return null;
|
|
90
|
-
const wfDispatch = (on as Record<string, unknown>)['workflow_dispatch'];
|
|
91
|
-
if (!wfDispatch || typeof wfDispatch !== 'object') return null;
|
|
92
|
-
const inputs = (wfDispatch as Record<string, unknown>)['inputs'];
|
|
93
|
-
if (!inputs || typeof inputs !== 'object' || Array.isArray(inputs)) {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
return inputs as Record<string, unknown>;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function toDescriptor(
|
|
100
|
-
name: string,
|
|
101
|
-
raw: unknown,
|
|
102
|
-
): DispatchInputDescriptor | null {
|
|
103
|
-
if (!raw || typeof raw !== 'object') return null;
|
|
104
|
-
const field = raw as Record<string, unknown>;
|
|
105
|
-
const type = parseFieldType(field['type']);
|
|
106
|
-
if (type === null) return null;
|
|
107
|
-
const required = field['required'] === true;
|
|
108
|
-
const description =
|
|
109
|
-
typeof field['description'] === 'string' ? field['description'] : null;
|
|
110
|
-
const enumValues = parseEnumValues(field['enum']);
|
|
111
|
-
const format = parseStringFormat(field['format']);
|
|
112
|
-
const itemType = parseItemType(field['items']);
|
|
113
|
-
return {
|
|
114
|
-
name,
|
|
115
|
-
type,
|
|
116
|
-
required,
|
|
117
|
-
description,
|
|
118
|
-
enumValues,
|
|
119
|
-
format,
|
|
120
|
-
itemType,
|
|
121
|
-
defaultValue: field['default'] ?? undefined,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function parseFieldType(value: unknown): DispatchInputFieldType | null {
|
|
126
|
-
if (typeof value !== 'string') return null;
|
|
127
|
-
switch (value) {
|
|
128
|
-
case 'string':
|
|
129
|
-
return DispatchInputFieldType.String;
|
|
130
|
-
case 'number':
|
|
131
|
-
return DispatchInputFieldType.Number;
|
|
132
|
-
case 'integer':
|
|
133
|
-
return DispatchInputFieldType.Integer;
|
|
134
|
-
case 'boolean':
|
|
135
|
-
return DispatchInputFieldType.Boolean;
|
|
136
|
-
case 'object':
|
|
137
|
-
return DispatchInputFieldType.Object;
|
|
138
|
-
case 'array':
|
|
139
|
-
return DispatchInputFieldType.Array;
|
|
140
|
-
default:
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function parseEnumValues(value: unknown): readonly string[] | null {
|
|
146
|
-
if (!Array.isArray(value) || value.length === 0) return null;
|
|
147
|
-
const strings = value.filter((v): v is string => typeof v === 'string');
|
|
148
|
-
return strings.length === value.length ? strings : null;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function parseStringFormat(value: unknown): DispatchInputStringFormat | null {
|
|
152
|
-
if (value === 'multiline') return DispatchInputStringFormat.Multiline;
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function parseItemType(value: unknown): DispatchInputFieldType | null {
|
|
157
|
-
if (!value || typeof value !== 'object') return null;
|
|
158
|
-
const items = value as Record<string, unknown>;
|
|
159
|
-
return parseFieldType(items['type']);
|
|
160
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DispatchInputFieldType,
|
|
3
|
-
DispatchInputStringFormat,
|
|
4
|
-
type DispatchInputDescriptor,
|
|
5
|
-
} from './index';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Project a flat `DispatchInputDescriptor[]` into a JSON Schema
|
|
9
|
-
* Draft 2020-12 object the MCP gateway uses as `inputSchema` on
|
|
10
|
-
* workflow-tool projections (and the frontend dispatch dialog uses
|
|
11
|
-
* for form rendering parity).
|
|
12
|
-
*
|
|
13
|
-
* Single source of truth for the descriptor → JSON Schema mapping so
|
|
14
|
-
* the gateway, the engine, and the frontend agree on the shape an
|
|
15
|
-
* agent sees vs the shape a human sees in the dispatch dialog.
|
|
16
|
-
*
|
|
17
|
-
* Closed-set conservatively: unknown enum values are dropped (the
|
|
18
|
-
* extractor already filters), unknown type combinations fall back to
|
|
19
|
-
* `{ type: 'object' }` to keep the schema valid rather than throwing.
|
|
20
|
-
*/
|
|
21
|
-
export function dispatchInputsToJsonSchema(
|
|
22
|
-
descriptors: readonly DispatchInputDescriptor[],
|
|
23
|
-
): Readonly<Record<string, unknown>> {
|
|
24
|
-
const properties: Record<string, Record<string, unknown>> = {};
|
|
25
|
-
const required: string[] = [];
|
|
26
|
-
for (const d of descriptors) {
|
|
27
|
-
properties[d.name] = descriptorToProperty(d);
|
|
28
|
-
if (d.required) required.push(d.name);
|
|
29
|
-
}
|
|
30
|
-
const schema: Record<string, unknown> = {
|
|
31
|
-
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
32
|
-
type: 'object',
|
|
33
|
-
properties,
|
|
34
|
-
additionalProperties: false,
|
|
35
|
-
};
|
|
36
|
-
if (required.length > 0) {
|
|
37
|
-
schema['required'] = required;
|
|
38
|
-
}
|
|
39
|
-
return Object.freeze(schema);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function descriptorToProperty(d: DispatchInputDescriptor): Record<string, unknown> {
|
|
43
|
-
const prop: Record<string, unknown> = { type: d.type };
|
|
44
|
-
if (d.description) {
|
|
45
|
-
prop['description'] = d.description;
|
|
46
|
-
}
|
|
47
|
-
if (d.enumValues && d.enumValues.length > 0) {
|
|
48
|
-
prop['enum'] = [...d.enumValues];
|
|
49
|
-
}
|
|
50
|
-
if (d.format === DispatchInputStringFormat.Multiline) {
|
|
51
|
-
prop['x-widget'] = 'multiline';
|
|
52
|
-
}
|
|
53
|
-
if (d.type === DispatchInputFieldType.Array && d.itemType) {
|
|
54
|
-
prop['items'] = { type: d.itemType };
|
|
55
|
-
}
|
|
56
|
-
if (d.defaultValue !== undefined) {
|
|
57
|
-
prop['default'] = d.defaultValue;
|
|
58
|
-
}
|
|
59
|
-
return prop;
|
|
60
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
|
|
2
|
-
import { WorkflowDslError } from './errors';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Duration literal parser. Accepts `<int><unit>` where unit is `s`, `m`, or
|
|
6
|
-
* `h`. Deterministic; rejects malformed input with a typed error. There is
|
|
7
|
-
* no silent zero fallback — callers must pass a value that matches the
|
|
8
|
-
* schema pattern (already enforced by Ajv upstream).
|
|
9
|
-
*
|
|
10
|
-
* Cap: total duration limited to 8760h (= 365 days), regardless of the
|
|
11
|
-
* unit used. The cap is a sanity guard
|
|
12
|
-
* against typos (`100000h` ≈ 11 years), not a policy bound — real
|
|
13
|
-
* human-driven flows (compliance reviews, multi-stakeholder approvals,
|
|
14
|
-
* SCM PRs that go a quarter without merging) routinely take months.
|
|
15
|
-
*
|
|
16
|
-
* For redraft loops (`xema/review@v3`), the workflow `continueAsNew`s
|
|
17
|
-
* every 25 iterations, which resets the runtime clock — the practical
|
|
18
|
-
* ceiling on a single review is unbounded. This cap applies only to a
|
|
19
|
-
* single iteration window of the child workflow.
|
|
20
|
-
*/
|
|
21
|
-
const MAX_HOURS = 8760;
|
|
22
|
-
|
|
23
|
-
export function parseDurationMs(literal: string): number {
|
|
24
|
-
const match = /^([1-9][0-9]*)(s|m|h)$/.exec(literal);
|
|
25
|
-
if (!match) {
|
|
26
|
-
throw new WorkflowDslError(
|
|
27
|
-
WorkflowErrorCode.DSL_SCHEMA_INVALID,
|
|
28
|
-
`Duration literal does not match pattern <int><s|m|h>: ${literal}`,
|
|
29
|
-
{ literal },
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
const value = Number.parseInt(match[1]!, 10);
|
|
33
|
-
const unit = match[2] as 's' | 'm' | 'h';
|
|
34
|
-
const ms = unit === 's' ? value * 1_000 : unit === 'm' ? value * 60_000 : value * 3_600_000;
|
|
35
|
-
if (ms > MAX_HOURS * 3_600_000) {
|
|
36
|
-
throw new WorkflowDslError(
|
|
37
|
-
WorkflowErrorCode.DSL_SCHEMA_INVALID,
|
|
38
|
-
`Duration exceeds cap of ${MAX_HOURS}h: ${literal}`,
|
|
39
|
-
{ literal, maxHours: MAX_HOURS },
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
return ms;
|
|
43
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Structured error produced by every DSL entry point (validate, parseYaml,
|
|
5
|
-
* evaluateExpression, compile). Callers MUST branch on `.code` — free-form
|
|
6
|
-
* message matching is forbidden so error handling stays exhaustive.
|
|
7
|
-
*/
|
|
8
|
-
export class WorkflowDslError extends Error {
|
|
9
|
-
readonly code: WorkflowErrorCode;
|
|
10
|
-
readonly details: Readonly<Record<string, unknown>>;
|
|
11
|
-
|
|
12
|
-
constructor(
|
|
13
|
-
code: WorkflowErrorCode,
|
|
14
|
-
message: string,
|
|
15
|
-
details: Readonly<Record<string, unknown>> = {},
|
|
16
|
-
) {
|
|
17
|
-
super(message);
|
|
18
|
-
this.name = 'WorkflowDslError';
|
|
19
|
-
this.code = code;
|
|
20
|
-
this.details = details;
|
|
21
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
toJSON(): Readonly<Record<string, unknown>> {
|
|
25
|
-
return {
|
|
26
|
-
name: this.name,
|
|
27
|
-
code: this.code,
|
|
28
|
-
message: this.message,
|
|
29
|
-
details: this.details,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Narrowing helper. */
|
|
35
|
-
export function isWorkflowDslError(err: unknown): err is WorkflowDslError {
|
|
36
|
-
return err instanceof WorkflowDslError;
|
|
37
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AST node kinds for parsed expressions. Closed set.
|
|
3
|
-
*/
|
|
4
|
-
export enum ExpressionNodeKind {
|
|
5
|
-
LITERAL = 'literal',
|
|
6
|
-
IDENTIFIER = 'identifier',
|
|
7
|
-
MEMBER = 'member',
|
|
8
|
-
INDEX = 'index',
|
|
9
|
-
CALL = 'call',
|
|
10
|
-
UNARY_NOT = 'unary-not',
|
|
11
|
-
BINARY_EQ = 'binary-eq',
|
|
12
|
-
BINARY_NEQ = 'binary-neq',
|
|
13
|
-
BINARY_AND = 'binary-and',
|
|
14
|
-
BINARY_OR = 'binary-or',
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Closed set of root identifier bindings allowed in expressions. */
|
|
18
|
-
export enum ExpressionRoot {
|
|
19
|
-
NEEDS = 'needs',
|
|
20
|
-
MATRIX = 'matrix',
|
|
21
|
-
INPUTS = 'inputs',
|
|
22
|
-
VARS = 'vars',
|
|
23
|
-
/**
|
|
24
|
-
* Project / org secret variables resolved at dispatch from the
|
|
25
|
-
* project-registry-api variable store, declared by the workflow under
|
|
26
|
-
* `requires.variables[*].secret: true`. Values are strings; expressions
|
|
27
|
-
* reading this namespace are subject to log redaction by the runtime.
|
|
28
|
-
*/
|
|
29
|
-
SECRETS = 'secrets',
|
|
30
|
-
TRIGGER = 'trigger',
|
|
31
|
-
JOB = 'job',
|
|
32
|
-
/**
|
|
33
|
-
* System-managed canonical context for the run. Mirrors GitHub
|
|
34
|
-
* Actions' `github.*` namespace: stable, system-populated, available
|
|
35
|
-
* in every expression. Exposes org/project/run/workflow/job/actor
|
|
36
|
-
* fields so authors don't have to reach into trigger metadata for
|
|
37
|
-
* what is really run-level state.
|
|
38
|
-
*/
|
|
39
|
-
XEMA = 'xema',
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Closed set of built-in function names. Any other identifier-call fails. */
|
|
43
|
-
export enum ExpressionFunction {
|
|
44
|
-
FROM_JSON = 'fromJSON',
|
|
45
|
-
TO_JSON = 'toJSON',
|
|
46
|
-
/**
|
|
47
|
-
* `format(template, ...args)` — GitHub-Actions-style positional
|
|
48
|
-
* interpolation. The template is a literal string with `{0}`, `{1}`,
|
|
49
|
-
* ... placeholders; each is replaced with the stringified arg at that
|
|
50
|
-
* index. Authors use this when they need to combine an expression
|
|
51
|
-
* value with surrounding literal text — partial `${{ ... }}`
|
|
52
|
-
* interpolation in YAML is intentionally rejected, so `format` is the
|
|
53
|
-
* supported way to build composed strings.
|
|
54
|
-
*/
|
|
55
|
-
FORMAT = 'format',
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export type ExpressionNode =
|
|
59
|
-
| LiteralNode
|
|
60
|
-
| IdentifierNode
|
|
61
|
-
| MemberNode
|
|
62
|
-
| IndexNode
|
|
63
|
-
| CallNode
|
|
64
|
-
| UnaryNotNode
|
|
65
|
-
| BinaryNode;
|
|
66
|
-
|
|
67
|
-
export interface LiteralNode {
|
|
68
|
-
readonly kind: ExpressionNodeKind.LITERAL;
|
|
69
|
-
readonly value: string | number | boolean | null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface IdentifierNode {
|
|
73
|
-
readonly kind: ExpressionNodeKind.IDENTIFIER;
|
|
74
|
-
readonly name: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface MemberNode {
|
|
78
|
-
readonly kind: ExpressionNodeKind.MEMBER;
|
|
79
|
-
readonly target: ExpressionNode;
|
|
80
|
-
readonly property: string;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface IndexNode {
|
|
84
|
-
readonly kind: ExpressionNodeKind.INDEX;
|
|
85
|
-
readonly target: ExpressionNode;
|
|
86
|
-
readonly index: ExpressionNode;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface CallNode {
|
|
90
|
-
readonly kind: ExpressionNodeKind.CALL;
|
|
91
|
-
readonly callee: ExpressionFunction;
|
|
92
|
-
readonly args: readonly ExpressionNode[];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export interface UnaryNotNode {
|
|
96
|
-
readonly kind: ExpressionNodeKind.UNARY_NOT;
|
|
97
|
-
readonly operand: ExpressionNode;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export interface BinaryNode {
|
|
101
|
-
readonly kind:
|
|
102
|
-
| ExpressionNodeKind.BINARY_EQ
|
|
103
|
-
| ExpressionNodeKind.BINARY_NEQ
|
|
104
|
-
| ExpressionNodeKind.BINARY_AND
|
|
105
|
-
| ExpressionNodeKind.BINARY_OR;
|
|
106
|
-
readonly left: ExpressionNode;
|
|
107
|
-
readonly right: ExpressionNode;
|
|
108
|
-
}
|