@xemahq/dsl 0.1.1
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/LICENSE +201 -0
- package/dist/deliverable-spec/index.d.ts +3 -0
- package/dist/deliverable-spec/index.d.ts.map +1 -0
- package/dist/deliverable-spec/index.js +19 -0
- package/dist/deliverable-spec/index.js.map +1 -0
- package/dist/deliverable-spec/lib/schema.d.ts +151 -0
- package/dist/deliverable-spec/lib/schema.d.ts.map +1 -0
- package/dist/deliverable-spec/lib/schema.js +139 -0
- package/dist/deliverable-spec/lib/schema.js.map +1 -0
- package/dist/deliverable-spec/lib/types.d.ts +8 -0
- package/dist/deliverable-spec/lib/types.d.ts.map +1 -0
- package/dist/deliverable-spec/lib/types.js +3 -0
- package/dist/deliverable-spec/lib/types.js.map +1 -0
- package/dist/payload-codec/index.d.ts +8 -0
- package/dist/payload-codec/index.d.ts.map +1 -0
- package/dist/payload-codec/index.js +27 -0
- package/dist/payload-codec/index.js.map +1 -0
- package/dist/payload-codec/lib/blob-store.d.ts +37 -0
- package/dist/payload-codec/lib/blob-store.d.ts.map +1 -0
- package/dist/payload-codec/lib/blob-store.js +0 -0
- package/dist/payload-codec/lib/blob-store.js.map +1 -0
- package/dist/payload-codec/lib/codec-context.d.ts +6 -0
- package/dist/payload-codec/lib/codec-context.d.ts.map +1 -0
- package/dist/payload-codec/lib/codec-context.js +16 -0
- package/dist/payload-codec/lib/codec-context.js.map +1 -0
- package/dist/payload-codec/lib/codec.d.ts +51 -0
- package/dist/payload-codec/lib/codec.d.ts.map +1 -0
- package/dist/payload-codec/lib/codec.js +330 -0
- package/dist/payload-codec/lib/codec.js.map +1 -0
- package/dist/payload-codec/lib/enums.d.ts +18 -0
- package/dist/payload-codec/lib/enums.d.ts.map +1 -0
- package/dist/payload-codec/lib/enums.js +23 -0
- package/dist/payload-codec/lib/enums.js.map +1 -0
- package/dist/payload-codec/lib/errors.d.ts +18 -0
- package/dist/payload-codec/lib/errors.d.ts.map +1 -0
- package/dist/payload-codec/lib/errors.js +39 -0
- package/dist/payload-codec/lib/errors.js.map +1 -0
- package/dist/payload-codec/lib/http-blob-store.d.ts +21 -0
- package/dist/payload-codec/lib/http-blob-store.d.ts.map +1 -0
- package/dist/payload-codec/lib/http-blob-store.js +139 -0
- package/dist/payload-codec/lib/http-blob-store.js.map +1 -0
- package/dist/payload-codec/lib/lru-cache.d.ts +12 -0
- package/dist/payload-codec/lib/lru-cache.d.ts.map +1 -0
- package/dist/payload-codec/lib/lru-cache.js +59 -0
- package/dist/payload-codec/lib/lru-cache.js.map +1 -0
- package/dist/schema/action.schema.json +181 -0
- package/dist/schema/reusable-workflow.schema.json +46 -0
- package/dist/schema/workflow.schema.json +373 -0
- package/dist/workflow/index.d.ts +14 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +49 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/lib/action-input-validator.d.ts +10 -0
- package/dist/workflow/lib/action-input-validator.d.ts.map +1 -0
- package/dist/workflow/lib/action-input-validator.js +69 -0
- package/dist/workflow/lib/action-input-validator.js.map +1 -0
- package/dist/workflow/lib/compiler/action-shape.d.ts +5 -0
- package/dist/workflow/lib/compiler/action-shape.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/action-shape.js +43 -0
- package/dist/workflow/lib/compiler/action-shape.js.map +1 -0
- package/dist/workflow/lib/compiler/canonical-json.d.ts +3 -0
- package/dist/workflow/lib/compiler/canonical-json.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/canonical-json.js +45 -0
- package/dist/workflow/lib/compiler/canonical-json.js.map +1 -0
- package/dist/workflow/lib/compiler/compile.d.ts +4 -0
- package/dist/workflow/lib/compiler/compile.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/compile.js +794 -0
- package/dist/workflow/lib/compiler/compile.js.map +1 -0
- package/dist/workflow/lib/compiler/concurrency.d.ts +5 -0
- package/dist/workflow/lib/compiler/concurrency.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/concurrency.js +104 -0
- package/dist/workflow/lib/compiler/concurrency.js.map +1 -0
- package/dist/workflow/lib/compiler/dag.d.ts +10 -0
- package/dist/workflow/lib/compiler/dag.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/dag.js +74 -0
- package/dist/workflow/lib/compiler/dag.js.map +1 -0
- package/dist/workflow/lib/compiler/index.d.ts +6 -0
- package/dist/workflow/lib/compiler/index.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/index.js +14 -0
- package/dist/workflow/lib/compiler/index.js.map +1 -0
- package/dist/workflow/lib/compiler/inputs.d.ts +4 -0
- package/dist/workflow/lib/compiler/inputs.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/inputs.js +108 -0
- package/dist/workflow/lib/compiler/inputs.js.map +1 -0
- package/dist/workflow/lib/compiler/installation-resource-validator.d.ts +9 -0
- package/dist/workflow/lib/compiler/installation-resource-validator.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/installation-resource-validator.js +76 -0
- package/dist/workflow/lib/compiler/installation-resource-validator.js.map +1 -0
- package/dist/workflow/lib/compiler/manifest-source.d.ts +4 -0
- package/dist/workflow/lib/compiler/manifest-source.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/manifest-source.js +100 -0
- package/dist/workflow/lib/compiler/manifest-source.js.map +1 -0
- package/dist/workflow/lib/compiler/matrix.d.ts +4 -0
- package/dist/workflow/lib/compiler/matrix.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/matrix.js +76 -0
- package/dist/workflow/lib/compiler/matrix.js.map +1 -0
- package/dist/workflow/lib/compiler/mount-plan.d.ts +4 -0
- package/dist/workflow/lib/compiler/mount-plan.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/mount-plan.js +96 -0
- package/dist/workflow/lib/compiler/mount-plan.js.map +1 -0
- package/dist/workflow/lib/compiler/payload-reach-in.d.ts +14 -0
- package/dist/workflow/lib/compiler/payload-reach-in.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/payload-reach-in.js +273 -0
- package/dist/workflow/lib/compiler/payload-reach-in.js.map +1 -0
- package/dist/workflow/lib/compiler/permissions.d.ts +6 -0
- package/dist/workflow/lib/compiler/permissions.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/permissions.js +43 -0
- package/dist/workflow/lib/compiler/permissions.js.map +1 -0
- package/dist/workflow/lib/compiler/retry-timeout.d.ts +6 -0
- package/dist/workflow/lib/compiler/retry-timeout.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/retry-timeout.js +64 -0
- package/dist/workflow/lib/compiler/retry-timeout.js.map +1 -0
- package/dist/workflow/lib/compiler/review-step.d.ts +18 -0
- package/dist/workflow/lib/compiler/review-step.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/review-step.js +247 -0
- package/dist/workflow/lib/compiler/review-step.js.map +1 -0
- package/dist/workflow/lib/compiler/types.d.ts +42 -0
- package/dist/workflow/lib/compiler/types.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/types.js +3 -0
- package/dist/workflow/lib/compiler/types.js.map +1 -0
- package/dist/workflow/lib/compiler/variable-requirements.d.ts +5 -0
- package/dist/workflow/lib/compiler/variable-requirements.d.ts.map +1 -0
- package/dist/workflow/lib/compiler/variable-requirements.js +119 -0
- package/dist/workflow/lib/compiler/variable-requirements.js.map +1 -0
- package/dist/workflow/lib/deliverable-spec-keys.d.ts +3 -0
- package/dist/workflow/lib/deliverable-spec-keys.d.ts.map +1 -0
- package/dist/workflow/lib/deliverable-spec-keys.js +90 -0
- package/dist/workflow/lib/deliverable-spec-keys.js.map +1 -0
- package/dist/workflow/lib/dispatch-inputs/index.d.ts +23 -0
- package/dist/workflow/lib/dispatch-inputs/index.d.ts.map +1 -0
- package/dist/workflow/lib/dispatch-inputs/index.js +106 -0
- package/dist/workflow/lib/dispatch-inputs/index.js.map +1 -0
- package/dist/workflow/lib/dispatch-inputs/to-json-schema.d.ts +3 -0
- package/dist/workflow/lib/dispatch-inputs/to-json-schema.d.ts.map +1 -0
- package/dist/workflow/lib/dispatch-inputs/to-json-schema.js +43 -0
- package/dist/workflow/lib/dispatch-inputs/to-json-schema.js.map +1 -0
- package/dist/workflow/lib/duration.d.ts +2 -0
- package/dist/workflow/lib/duration.d.ts.map +1 -0
- package/dist/workflow/lib/duration.js +26 -0
- package/dist/workflow/lib/duration.js.map +1 -0
- package/dist/workflow/lib/errors.d.ts +9 -0
- package/dist/workflow/lib/errors.d.ts.map +1 -0
- package/dist/workflow/lib/errors.js +28 -0
- package/dist/workflow/lib/errors.js.map +1 -0
- package/dist/workflow/lib/expression/ast.d.ts +61 -0
- package/dist/workflow/lib/expression/ast.d.ts.map +1 -0
- package/dist/workflow/lib/expression/ast.js +34 -0
- package/dist/workflow/lib/expression/ast.js.map +1 -0
- package/dist/workflow/lib/expression/context.d.ts +63 -0
- package/dist/workflow/lib/expression/context.d.ts.map +1 -0
- package/dist/workflow/lib/expression/context.js +32 -0
- package/dist/workflow/lib/expression/context.js.map +1 -0
- package/dist/workflow/lib/expression/evaluator.d.ts +5 -0
- package/dist/workflow/lib/expression/evaluator.d.ts.map +1 -0
- package/dist/workflow/lib/expression/evaluator.js +291 -0
- package/dist/workflow/lib/expression/evaluator.js.map +1 -0
- package/dist/workflow/lib/expression/index.d.ts +9 -0
- package/dist/workflow/lib/expression/index.d.ts.map +1 -0
- package/dist/workflow/lib/expression/index.js +26 -0
- package/dist/workflow/lib/expression/index.js.map +1 -0
- package/dist/workflow/lib/expression/interpolation.d.ts +9 -0
- package/dist/workflow/lib/expression/interpolation.d.ts.map +1 -0
- package/dist/workflow/lib/expression/interpolation.js +51 -0
- package/dist/workflow/lib/expression/interpolation.js.map +1 -0
- package/dist/workflow/lib/expression/parser.d.ts +4 -0
- package/dist/workflow/lib/expression/parser.d.ts.map +1 -0
- package/dist/workflow/lib/expression/parser.js +203 -0
- package/dist/workflow/lib/expression/parser.js.map +1 -0
- package/dist/workflow/lib/expression/template.d.ts +18 -0
- package/dist/workflow/lib/expression/template.d.ts.map +1 -0
- package/dist/workflow/lib/expression/template.js +63 -0
- package/dist/workflow/lib/expression/template.js.map +1 -0
- package/dist/workflow/lib/expression/tokenizer.d.ts +3 -0
- package/dist/workflow/lib/expression/tokenizer.d.ts.map +1 -0
- package/dist/workflow/lib/expression/tokenizer.js +153 -0
- package/dist/workflow/lib/expression/tokenizer.js.map +1 -0
- package/dist/workflow/lib/expression/tokens.d.ts +25 -0
- package/dist/workflow/lib/expression/tokens.d.ts.map +1 -0
- package/dist/workflow/lib/expression/tokens.js +24 -0
- package/dist/workflow/lib/expression/tokens.js.map +1 -0
- package/dist/workflow/lib/expression/walk-artifact-refs.d.ts +5 -0
- package/dist/workflow/lib/expression/walk-artifact-refs.d.ts.map +1 -0
- package/dist/workflow/lib/expression/walk-artifact-refs.js +138 -0
- package/dist/workflow/lib/expression/walk-artifact-refs.js.map +1 -0
- package/dist/workflow/lib/installation-resource-kind.d.ts +14 -0
- package/dist/workflow/lib/installation-resource-kind.d.ts.map +1 -0
- package/dist/workflow/lib/installation-resource-kind.js +59 -0
- package/dist/workflow/lib/installation-resource-kind.js.map +1 -0
- package/dist/workflow/lib/schemas-loader.d.ts +4 -0
- package/dist/workflow/lib/schemas-loader.d.ts.map +1 -0
- package/dist/workflow/lib/schemas-loader.js +36 -0
- package/dist/workflow/lib/schemas-loader.js.map +1 -0
- package/dist/workflow/lib/serializer.d.ts +3 -0
- package/dist/workflow/lib/serializer.d.ts.map +1 -0
- package/dist/workflow/lib/serializer.js +15 -0
- package/dist/workflow/lib/serializer.js.map +1 -0
- package/dist/workflow/lib/types.d.ts +179 -0
- package/dist/workflow/lib/types.d.ts.map +1 -0
- package/dist/workflow/lib/types.js +3 -0
- package/dist/workflow/lib/types.js.map +1 -0
- package/dist/workflow/lib/validate.d.ts +8 -0
- package/dist/workflow/lib/validate.d.ts.map +1 -0
- package/dist/workflow/lib/validate.js +119 -0
- package/dist/workflow/lib/validate.js.map +1 -0
- package/dist/workspace-manifest/index.d.ts +6 -0
- package/dist/workspace-manifest/index.d.ts.map +1 -0
- package/dist/workspace-manifest/index.js +22 -0
- package/dist/workspace-manifest/index.js.map +1 -0
- package/dist/workspace-manifest/lib/compile.d.ts +8 -0
- package/dist/workspace-manifest/lib/compile.d.ts.map +1 -0
- package/dist/workspace-manifest/lib/compile.js +439 -0
- package/dist/workspace-manifest/lib/compile.js.map +1 -0
- package/dist/workspace-manifest/lib/interpolate.d.ts +12 -0
- package/dist/workspace-manifest/lib/interpolate.d.ts.map +1 -0
- package/dist/workspace-manifest/lib/interpolate.js +81 -0
- package/dist/workspace-manifest/lib/interpolate.js.map +1 -0
- package/dist/workspace-manifest/lib/resolve-extends.d.ts +10 -0
- package/dist/workspace-manifest/lib/resolve-extends.d.ts.map +1 -0
- package/dist/workspace-manifest/lib/resolve-extends.js +108 -0
- package/dist/workspace-manifest/lib/resolve-extends.js.map +1 -0
- package/dist/workspace-manifest/lib/schema.d.ts +710 -0
- package/dist/workspace-manifest/lib/schema.d.ts.map +1 -0
- package/dist/workspace-manifest/lib/schema.js +355 -0
- package/dist/workspace-manifest/lib/schema.js.map +1 -0
- package/dist/workspace-manifest/lib/types.d.ts +153 -0
- package/dist/workspace-manifest/lib/types.d.ts.map +1 -0
- package/dist/workspace-manifest/lib/types.js +10 -0
- package/dist/workspace-manifest/lib/types.js.map +1 -0
- package/package.json +79 -0
- package/schema/action.schema.json +181 -0
- package/schema/reusable-workflow.schema.json +46 -0
- package/schema/workflow.schema.json +373 -0
- package/src/deliverable-spec/index.ts +19 -0
- package/src/deliverable-spec/lib/schema.ts +248 -0
- package/src/deliverable-spec/lib/types.ts +26 -0
- package/src/payload-codec/index.ts +40 -0
- package/src/payload-codec/lib/blob-store.ts +0 -0
- package/src/payload-codec/lib/codec-context.ts +38 -0
- package/src/payload-codec/lib/codec.ts +593 -0
- package/src/payload-codec/lib/enums.ts +58 -0
- package/src/payload-codec/lib/errors.ts +54 -0
- package/src/payload-codec/lib/http-blob-store.ts +257 -0
- package/src/payload-codec/lib/lru-cache.ts +81 -0
- package/src/workflow/index.ts +98 -0
- package/src/workflow/lib/action-input-validator.ts +160 -0
- package/src/workflow/lib/compiler/action-shape.ts +71 -0
- package/src/workflow/lib/compiler/canonical-json.ts +53 -0
- package/src/workflow/lib/compiler/compile.ts +1518 -0
- package/src/workflow/lib/compiler/concurrency.ts +223 -0
- package/src/workflow/lib/compiler/dag.ts +108 -0
- package/src/workflow/lib/compiler/index.ts +10 -0
- package/src/workflow/lib/compiler/inputs.ts +199 -0
- package/src/workflow/lib/compiler/installation-resource-validator.ts +114 -0
- package/src/workflow/lib/compiler/manifest-source.ts +176 -0
- package/src/workflow/lib/compiler/matrix.ts +135 -0
- package/src/workflow/lib/compiler/mount-plan.ts +202 -0
- package/src/workflow/lib/compiler/payload-reach-in.ts +497 -0
- package/src/workflow/lib/compiler/permissions.ts +64 -0
- package/src/workflow/lib/compiler/retry-timeout.ts +105 -0
- package/src/workflow/lib/compiler/review-step.ts +517 -0
- package/src/workflow/lib/compiler/types.ts +170 -0
- package/src/workflow/lib/compiler/variable-requirements.ts +208 -0
- package/src/workflow/lib/deliverable-spec-keys.ts +109 -0
- package/src/workflow/lib/dispatch-inputs/index.ts +160 -0
- package/src/workflow/lib/dispatch-inputs/to-json-schema.ts +60 -0
- package/src/workflow/lib/duration.ts +48 -0
- package/src/workflow/lib/errors.ts +37 -0
- package/src/workflow/lib/expression/ast.ts +108 -0
- package/src/workflow/lib/expression/context.ts +148 -0
- package/src/workflow/lib/expression/evaluator.ts +492 -0
- package/src/workflow/lib/expression/index.ts +28 -0
- package/src/workflow/lib/expression/interpolation.ts +84 -0
- package/src/workflow/lib/expression/parser.ts +264 -0
- package/src/workflow/lib/expression/template.ts +117 -0
- package/src/workflow/lib/expression/tokenizer.ts +200 -0
- package/src/workflow/lib/expression/tokens.ts +30 -0
- package/src/workflow/lib/expression/walk-artifact-refs.ts +232 -0
- package/src/workflow/lib/installation-resource-kind.ts +107 -0
- package/src/workflow/lib/schemas-loader.ts +64 -0
- package/src/workflow/lib/serializer.ts +30 -0
- package/src/workflow/lib/types.ts +361 -0
- package/src/workflow/lib/validate.ts +199 -0
- package/src/workspace-manifest/index.ts +27 -0
- package/src/workspace-manifest/lib/compile.ts +608 -0
- package/src/workspace-manifest/lib/interpolate.ts +140 -0
- package/src/workspace-manifest/lib/resolve-extends.ts +260 -0
- package/src/workspace-manifest/lib/schema.ts +612 -0
- package/src/workspace-manifest/lib/types.ts +392 -0
|
@@ -0,0 +1,223 @@
|
|
|
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: '', version: '' },
|
|
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
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { compileWorkflow } from './compile';
|
|
2
|
+
export { canonicalJsonSha256, canonicalJsonStringify } from './canonical-json';
|
|
3
|
+
export { compileManifestSource } from './manifest-source';
|
|
4
|
+
export { getWorkspaceManifestContract, isAgentShapedAction } from './action-shape';
|
|
5
|
+
export type {
|
|
6
|
+
CompileInput,
|
|
7
|
+
ResolvedAgent,
|
|
8
|
+
ResolvedDeliverableSpec,
|
|
9
|
+
ResolvedRef,
|
|
10
|
+
} from './types';
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WorkflowErrorCode,
|
|
3
|
+
WorkflowTriggerKind,
|
|
4
|
+
type TriggerPayload,
|
|
5
|
+
} from '@xemahq/kernel-contracts/workflow';
|
|
6
|
+
import { WorkflowDslError } from '../errors';
|
|
7
|
+
import type { WorkflowDocument, WorkflowInputDeclaration } from '../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Bind trigger inputs against the workflow's declared input schema(s),
|
|
11
|
+
* applying declared defaults. Missing-required / type-mismatch is a
|
|
12
|
+
* compile error — never a silent zero/null substitution (rule 2).
|
|
13
|
+
*
|
|
14
|
+
* We use the trigger kind to pick which declaration block governs:
|
|
15
|
+
* - workflow_dispatch → on.workflow_dispatch.inputs
|
|
16
|
+
* - schedule → on.schedule[*].inputs (pre-bound on the schedule itself)
|
|
17
|
+
* - workflow_call → on.workflow_call.inputs
|
|
18
|
+
* - webhook → no input declaration; payload is the whole envelope
|
|
19
|
+
*
|
|
20
|
+
* Scheduled triggers carry their pre-bound inputs in `trigger.inputs`; we
|
|
21
|
+
* validate against the workflow_dispatch declaration if present, or pass
|
|
22
|
+
* through un-validated (schedule-authored inputs are trusted) — the engine
|
|
23
|
+
* validates schedule-level inputs at schedule-creation time instead.
|
|
24
|
+
*/
|
|
25
|
+
export function bindTriggerInputs(
|
|
26
|
+
workflow: WorkflowDocument,
|
|
27
|
+
trigger: TriggerPayload,
|
|
28
|
+
previewMode = false,
|
|
29
|
+
): Readonly<Record<string, unknown>> {
|
|
30
|
+
switch (trigger.kind) {
|
|
31
|
+
case WorkflowTriggerKind.WORKFLOW_DISPATCH: {
|
|
32
|
+
if (workflow.on.workflow_dispatch === undefined) {
|
|
33
|
+
throw new WorkflowDslError(
|
|
34
|
+
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
35
|
+
`Workflow does not declare an 'on.workflow_dispatch' trigger and cannot be dispatched. Declared triggers: [${Object.keys(workflow.on).sort((a, b) => a.localeCompare(b)).join(', ') || '(none)'}].`,
|
|
36
|
+
{ triggerKind: 'workflow_dispatch', declaredTriggers: Object.keys(workflow.on) },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const decl = workflow.on.workflow_dispatch.inputs ?? {};
|
|
40
|
+
return bindInputs(decl, trigger.inputs, 'workflow_dispatch', previewMode);
|
|
41
|
+
}
|
|
42
|
+
case WorkflowTriggerKind.WORKFLOW_CALL: {
|
|
43
|
+
if (workflow.on.workflow_call === undefined) {
|
|
44
|
+
throw new WorkflowDslError(
|
|
45
|
+
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
46
|
+
`Workflow does not declare an 'on.workflow_call' trigger and cannot be invoked from another workflow. Declared triggers: [${Object.keys(workflow.on).sort((a, b) => a.localeCompare(b)).join(', ') || '(none)'}].`,
|
|
47
|
+
{ triggerKind: 'workflow_call', declaredTriggers: Object.keys(workflow.on) },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const decl = workflow.on.workflow_call.inputs ?? {};
|
|
51
|
+
return bindInputs(decl, trigger.inputs, 'workflow_call', previewMode);
|
|
52
|
+
}
|
|
53
|
+
case WorkflowTriggerKind.SCHEDULE: {
|
|
54
|
+
// Schedule inputs were validated against workflow_dispatch.inputs at
|
|
55
|
+
// schedule-create time by workflow-engine-api. Here we just validate
|
|
56
|
+
// the resulting bound payload against the same schema (defense in
|
|
57
|
+
// depth; cheap).
|
|
58
|
+
const decl = workflow.on.workflow_dispatch?.inputs ?? {};
|
|
59
|
+
return bindInputs(decl, trigger.inputs, 'schedule', previewMode);
|
|
60
|
+
}
|
|
61
|
+
case WorkflowTriggerKind.WEBHOOK: {
|
|
62
|
+
// Webhook does not declare typed inputs at the workflow level —
|
|
63
|
+
// validation of the canonical envelope happens at integration-adapters-api.
|
|
64
|
+
return trigger.payload;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function bindInputs(
|
|
70
|
+
declarations: Readonly<Record<string, WorkflowInputDeclaration>>,
|
|
71
|
+
received: Readonly<Record<string, unknown>>,
|
|
72
|
+
label: string,
|
|
73
|
+
previewMode = false,
|
|
74
|
+
): Readonly<Record<string, unknown>> {
|
|
75
|
+
const out: Record<string, unknown> = {};
|
|
76
|
+
const seen = new Set<string>();
|
|
77
|
+
|
|
78
|
+
for (const [name, decl] of Object.entries(declarations)) {
|
|
79
|
+
seen.add(name);
|
|
80
|
+
const hasValue = Object.hasOwn(received, name);
|
|
81
|
+
if (!hasValue) {
|
|
82
|
+
handleMissingInput(out, name, decl, label, previewMode);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const value = received[name];
|
|
86
|
+
assertInputType(value, decl, name, label);
|
|
87
|
+
out[name] = value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Reject unknown inputs — fail-fast beats silent pass-through.
|
|
91
|
+
for (const name of Object.keys(received)) {
|
|
92
|
+
if (!seen.has(name)) {
|
|
93
|
+
throw new WorkflowDslError(
|
|
94
|
+
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
95
|
+
`${label} trigger received unknown input '${name}'. Declared inputs: [${Object.keys(declarations).sort((a, b) => a.localeCompare(b)).join(', ') || '(none)'}].`,
|
|
96
|
+
{ label, inputName: name },
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function handleMissingInput(
|
|
105
|
+
out: Record<string, unknown>,
|
|
106
|
+
name: string,
|
|
107
|
+
decl: WorkflowInputDeclaration,
|
|
108
|
+
label: string,
|
|
109
|
+
previewMode: boolean,
|
|
110
|
+
): void {
|
|
111
|
+
if (decl.required !== true) {
|
|
112
|
+
// Optional inputs are always materialized in the bound payload so
|
|
113
|
+
// `${{ inputs.<name> }}` resolves deterministically — declared default
|
|
114
|
+
// when present, otherwise null. Without this, expressions referencing
|
|
115
|
+
// a not-provided optional input throw `Unknown property` at runtime.
|
|
116
|
+
out[name] = decl.default === undefined ? null : decl.default;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (previewMode) {
|
|
120
|
+
// In preview mode, substitute a type-appropriate sentinel so the
|
|
121
|
+
// compiler can validate structure (DAG, expressions, matrix)
|
|
122
|
+
// without requiring real input values. Never used on real runs.
|
|
123
|
+
out[name] = previewSentinel(decl.type);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
throw new WorkflowDslError(
|
|
127
|
+
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
128
|
+
`${label} trigger is missing required input '${name}'.`,
|
|
129
|
+
{ label, inputName: name },
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Returns a type-appropriate sentinel value for use in preview mode when
|
|
135
|
+
* a required input is missing. The sentinel must pass `assertInputType`
|
|
136
|
+
* for its declared type — it is never used outside of preview compilation.
|
|
137
|
+
*/
|
|
138
|
+
function previewSentinel(type: WorkflowInputDeclaration['type']): unknown {
|
|
139
|
+
switch (type) {
|
|
140
|
+
case 'string':
|
|
141
|
+
return '';
|
|
142
|
+
case 'number':
|
|
143
|
+
return 0;
|
|
144
|
+
case 'integer':
|
|
145
|
+
return 0;
|
|
146
|
+
case 'boolean':
|
|
147
|
+
return false;
|
|
148
|
+
case 'object':
|
|
149
|
+
return {};
|
|
150
|
+
case 'array':
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function assertInputType(
|
|
156
|
+
value: unknown,
|
|
157
|
+
decl: WorkflowInputDeclaration,
|
|
158
|
+
name: string,
|
|
159
|
+
label: string,
|
|
160
|
+
): void {
|
|
161
|
+
const received = classify(value);
|
|
162
|
+
if (received !== decl.type) {
|
|
163
|
+
throw new WorkflowDslError(
|
|
164
|
+
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
165
|
+
`${label} trigger input '${name}' expected ${decl.type}, received ${received}.`,
|
|
166
|
+
{ label, inputName: name, expected: decl.type, received },
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (decl.enum !== undefined) {
|
|
170
|
+
if (!decl.enum.includes(value)) {
|
|
171
|
+
throw new WorkflowDslError(
|
|
172
|
+
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
173
|
+
`${label} trigger input '${name}' value not in enum.`,
|
|
174
|
+
{ label, inputName: name, enum: decl.enum },
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function classify(value: unknown): WorkflowInputDeclaration['type'] {
|
|
181
|
+
if (Array.isArray(value)) return 'array';
|
|
182
|
+
if (value === null) return 'object'; // null treated as absence; Ajv doesn't emit null type for us
|
|
183
|
+
switch (typeof value) {
|
|
184
|
+
case 'string':
|
|
185
|
+
return 'string';
|
|
186
|
+
case 'boolean':
|
|
187
|
+
return 'boolean';
|
|
188
|
+
case 'number':
|
|
189
|
+
return Number.isInteger(value) ? 'integer' : 'number';
|
|
190
|
+
case 'object':
|
|
191
|
+
return 'object';
|
|
192
|
+
default:
|
|
193
|
+
throw new WorkflowDslError(
|
|
194
|
+
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
195
|
+
`Unsupported input value kind: ${typeof value}`,
|
|
196
|
+
{ valueKind: typeof value },
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
|
|
2
|
+
|
|
3
|
+
import { WorkflowDslError } from '../errors';
|
|
4
|
+
import {
|
|
5
|
+
collectInstallationResourceHints,
|
|
6
|
+
type InstallationResourceFieldHint,
|
|
7
|
+
} from '../installation-resource-kind';
|
|
8
|
+
|
|
9
|
+
import type { InstallationCompileScope } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Compile-time check that every `x-installation-resource` field on the
|
|
13
|
+
* action manifest's `inputs:` schema points at a resource bound to the
|
|
14
|
+
* calling biome installation.
|
|
15
|
+
*
|
|
16
|
+
* Cases handled:
|
|
17
|
+
* - Expression-shaped value (`${{ ... }}`): the literal isn't known
|
|
18
|
+
* yet, so we skip the binding check. Runtime auth lives at the
|
|
19
|
+
* activity layer (`AuthFailed` from credentials/resolve).
|
|
20
|
+
* - Literal string: must be in `boundResources[kind]`. Missing →
|
|
21
|
+
* DSL error with `inputs.<path>` pointer.
|
|
22
|
+
* - Array of strings (rare): every element must be bound.
|
|
23
|
+
* - Undefined + optional: no-op.
|
|
24
|
+
*
|
|
25
|
+
* Skipped entirely when `installationScope` is undefined (system /
|
|
26
|
+
* org-wide dispatch) — only biome-installed runs get this gate.
|
|
27
|
+
*/
|
|
28
|
+
export function validateInstallationResourceBindings(input: {
|
|
29
|
+
jobKey: string;
|
|
30
|
+
actionId: string;
|
|
31
|
+
inputsSchema: unknown;
|
|
32
|
+
withValue: Readonly<Record<string, unknown>>;
|
|
33
|
+
scope: InstallationCompileScope | undefined;
|
|
34
|
+
}): void {
|
|
35
|
+
if (!input.scope) {return;}
|
|
36
|
+
if (!input.inputsSchema) {return;}
|
|
37
|
+
const hints = collectInstallationResourceHints(input.inputsSchema);
|
|
38
|
+
if (hints.length === 0) {return;}
|
|
39
|
+
for (const hint of hints) {
|
|
40
|
+
const value = readPath(input.withValue, hint.path);
|
|
41
|
+
if (value === undefined || value === null || value === '') {
|
|
42
|
+
if (hint.optional) {continue;}
|
|
43
|
+
throw new WorkflowDslError(
|
|
44
|
+
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
45
|
+
`jobs.${input.jobKey}.with.${hint.path} is required (action ${input.actionId} declares x-installation-resource kind="${hint.kind}") but the with-block omitted it`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const ctx = {
|
|
49
|
+
jobKey: input.jobKey,
|
|
50
|
+
actionId: input.actionId,
|
|
51
|
+
scope: input.scope,
|
|
52
|
+
};
|
|
53
|
+
if (typeof value === 'string') {
|
|
54
|
+
assertBound(ctx, hint, value);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
for (const item of value) {
|
|
59
|
+
if (typeof item === 'string') {
|
|
60
|
+
assertBound(ctx, hint, item);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assertBound(
|
|
68
|
+
ctx: {
|
|
69
|
+
jobKey: string;
|
|
70
|
+
actionId: string;
|
|
71
|
+
scope: InstallationCompileScope;
|
|
72
|
+
},
|
|
73
|
+
hint: InstallationResourceFieldHint,
|
|
74
|
+
literal: string,
|
|
75
|
+
): void {
|
|
76
|
+
// Expression-shaped values are deferred to runtime — the literal
|
|
77
|
+
// string the compiler sees here is the expression body itself
|
|
78
|
+
// (`${{ inputs.x }}`), not a resolvable id.
|
|
79
|
+
if (literal.startsWith('${{') || literal.includes('${{')) {return;}
|
|
80
|
+
const bound = ctx.scope.boundResources[hint.kind] ?? [];
|
|
81
|
+
if (bound.length === 0) {
|
|
82
|
+
throw new WorkflowDslError(
|
|
83
|
+
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
84
|
+
`jobs.${ctx.jobKey}.with.${hint.path} references ${hint.kind}="${literal}" but installation ${ctx.scope.installationId} has no ${hint.kind} resources bound — connect one before dispatching`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (!bound.includes(literal)) {
|
|
88
|
+
throw new WorkflowDslError(
|
|
89
|
+
WorkflowErrorCode.DSL_SEMANTIC_INVALID,
|
|
90
|
+
`jobs.${ctx.jobKey}.with.${hint.path} references ${hint.kind}="${literal}" which is NOT bound to installation ${ctx.scope.installationId} (action ${ctx.actionId} declares x-installation-resource kind="${hint.kind}"). Bound: [${bound.join(', ')}]`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readPath(
|
|
96
|
+
obj: Readonly<Record<string, unknown>>,
|
|
97
|
+
path: string,
|
|
98
|
+
): unknown {
|
|
99
|
+
if (path === '') {return obj;}
|
|
100
|
+
const segments = path.split('.');
|
|
101
|
+
let cursor: unknown = obj;
|
|
102
|
+
for (const seg of segments) {
|
|
103
|
+
if (!cursor || typeof cursor !== 'object') {return undefined;}
|
|
104
|
+
// Array-element path uses `[*]` — treat as "any element", returning
|
|
105
|
+
// the array itself so the caller iterates. The hint walker emits
|
|
106
|
+
// these for `items.x-installation-resource` shapes.
|
|
107
|
+
if (seg === '[*]') {
|
|
108
|
+
if (!Array.isArray(cursor)) {return undefined;}
|
|
109
|
+
return cursor;
|
|
110
|
+
}
|
|
111
|
+
cursor = (cursor as Record<string, unknown>)[seg];
|
|
112
|
+
}
|
|
113
|
+
return cursor;
|
|
114
|
+
}
|