@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,497 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// ── Payload reach-in validation (plan §C.1) ──
|
|
3
|
+
//
|
|
4
|
+
// Workflow YAML supports two flavours of `needs.<X>.outputs.<name>.<field>`:
|
|
5
|
+
//
|
|
6
|
+
// 1. Envelope-field access — `<field>` is one of
|
|
7
|
+
// {artifactId, versionId, version, hash, type, title}. Free; the
|
|
8
|
+
// runtime reads it off the ArtifactRef without fetching bytes.
|
|
9
|
+
// Already works structurally via `readProperty` on the ref object.
|
|
10
|
+
//
|
|
11
|
+
// 2. Payload reach-in — `<field>` is a property on the bound
|
|
12
|
+
// deliverable spec's JSON Schema. Requires the runtime to
|
|
13
|
+
// pre-fetch the artifact's parsed payload into the per-step
|
|
14
|
+
// `payloadCache`. The compiler validates the path against the
|
|
15
|
+
// bound spec at compile time so authors get a clean DSL error
|
|
16
|
+
// instead of a runtime "Unknown property" surfacing as
|
|
17
|
+
// `DSL_EXPRESSION_INVALID`.
|
|
18
|
+
//
|
|
19
|
+
// This module owns family #2's compile-time checks. The actual runtime
|
|
20
|
+
// pre-fetch is the worker's responsibility — see the plan section on
|
|
21
|
+
// "Compile-time resolution (chosen approach)" — and is gated by the
|
|
22
|
+
// engine wiring spec metadata into `ResolvedDeliverableSpec`. Until
|
|
23
|
+
// that wiring lands for every spec, this validator gracefully skips
|
|
24
|
+
// when it cannot prove typing (the runtime then fails fast via
|
|
25
|
+
// `DSL_PAYLOAD_PARSE_ERROR` if the author insisted on a payload field
|
|
26
|
+
// without a typed binding).
|
|
27
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
isArtifactRefEnvelopeField,
|
|
31
|
+
WorkflowErrorCode,
|
|
32
|
+
type PayloadReachInPin,
|
|
33
|
+
} from '@xemahq/kernel-contracts/workflow';
|
|
34
|
+
import { WorkflowDslError } from '../errors';
|
|
35
|
+
import {
|
|
36
|
+
compileExpression,
|
|
37
|
+
ExpressionNodeKind,
|
|
38
|
+
stripInterpolation,
|
|
39
|
+
type ExpressionNode,
|
|
40
|
+
extractInterpolations,
|
|
41
|
+
} from '../expression';
|
|
42
|
+
import type { WorkflowDocument } from '../types';
|
|
43
|
+
import type { ResolvedDeliverableSpec, ResolvedRef } from './types';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Lookup helper: given an upstream job (by key) and an output name,
|
|
47
|
+
* return the deliverable-spec ref the upstream activity manifest pins
|
|
48
|
+
* for that output, or `null` when the manifest declares no per-output
|
|
49
|
+
* binding for it. The compiler prefers this over `with.deliverableSpecRef`
|
|
50
|
+
* — per-output bindings are activity-declared contracts; the job-level
|
|
51
|
+
* `with.deliverableSpecRef` is the workflow-author chosen runtime spec
|
|
52
|
+
* (dynamic, e.g. for the agent activity). They can coexist on the same
|
|
53
|
+
* job; the per-output binding wins for outputs it covers.
|
|
54
|
+
*/
|
|
55
|
+
function lookupOutputBindingSpecRef(
|
|
56
|
+
upstream: WorkflowDocument['jobs'][string],
|
|
57
|
+
outputName: string,
|
|
58
|
+
resolvedRefs: Readonly<Record<string, ResolvedRef>>,
|
|
59
|
+
): string | null {
|
|
60
|
+
const ref = resolvedRefs[upstream.uses];
|
|
61
|
+
const bindings = ref?.actionManifest?.spec.outputBindings;
|
|
62
|
+
if (!bindings) return null;
|
|
63
|
+
const binding = bindings[outputName];
|
|
64
|
+
if (!binding) return null;
|
|
65
|
+
const specRef = binding.deliverableSpecRef;
|
|
66
|
+
if (typeof specRef !== 'string' || specRef.length === 0) return null;
|
|
67
|
+
return specRef;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Walk every authored expression and field-check
|
|
72
|
+
* `needs.<X>.outputs.<name>.<field>` reach-ins.
|
|
73
|
+
*
|
|
74
|
+
* Decision table for each `<name>.<field>`:
|
|
75
|
+
* • `<field>` is an ArtifactRef envelope field → OK (no payload fetch).
|
|
76
|
+
* • Upstream activity manifest declares `outputBindings.<name>.deliverableSpecRef`
|
|
77
|
+
* (plan §B per-output binding) → use that spec; otherwise fall back
|
|
78
|
+
* to the upstream job's `with.deliverableSpecRef`. Then:
|
|
79
|
+
* - resolved spec has `topLevelKeys` and `<field>` is one → OK.
|
|
80
|
+
* - resolved spec has `topLevelKeys` and `<field>` is NOT one →
|
|
81
|
+
* `DSL_FIELD_NOT_IN_SCHEMA`.
|
|
82
|
+
* • Neither per-output binding NOR `with.deliverableSpecRef` is set,
|
|
83
|
+
* upstream has no `outputs:` projection, and no expression-shaped
|
|
84
|
+
* ref → `DSL_FIELD_NOT_TYPED`.
|
|
85
|
+
* • Otherwise (custom `outputs:` projection, expression-shaped ref,
|
|
86
|
+
* unknown spec, non-introspectable kind) → skip; the runtime
|
|
87
|
+
* evaluator + `payloadCache` will fail fast via
|
|
88
|
+
* `DSL_PAYLOAD_PARSE_ERROR` if the path is ultimately invalid.
|
|
89
|
+
*
|
|
90
|
+
* The validator runs only when `resolvedSpecs` is non-empty — same
|
|
91
|
+
* opt-out convention as the rest of the compiler (preview-raw passes
|
|
92
|
+
* an empty map).
|
|
93
|
+
*/
|
|
94
|
+
export function validatePayloadReachIns(
|
|
95
|
+
doc: WorkflowDocument,
|
|
96
|
+
resolvedSpecs: Readonly<Record<string, ResolvedDeliverableSpec>>,
|
|
97
|
+
resolvedRefs: Readonly<Record<string, ResolvedRef>>,
|
|
98
|
+
): void {
|
|
99
|
+
if (Object.keys(resolvedSpecs).length === 0) return;
|
|
100
|
+
|
|
101
|
+
for (const [jobKey, job] of Object.entries(doc.jobs)) {
|
|
102
|
+
const expressions = collectJobExpressions(jobKey, job);
|
|
103
|
+
for (const { source, fieldPath } of expressions) {
|
|
104
|
+
const ast = compileExpression(source);
|
|
105
|
+
walkPayloadReachIns(ast, (access) => {
|
|
106
|
+
validateReachIn(
|
|
107
|
+
doc,
|
|
108
|
+
jobKey,
|
|
109
|
+
fieldPath,
|
|
110
|
+
access,
|
|
111
|
+
resolvedSpecs,
|
|
112
|
+
resolvedRefs,
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const callOutputs = doc.on.workflow_call?.outputs;
|
|
119
|
+
if (callOutputs) {
|
|
120
|
+
for (const [outputName, expr] of Object.entries(callOutputs)) {
|
|
121
|
+
const ast = compileExpression(stripInterpolation(expr));
|
|
122
|
+
walkPayloadReachIns(ast, (access) => {
|
|
123
|
+
validateReachIn(
|
|
124
|
+
doc,
|
|
125
|
+
'<workflow_call.outputs>',
|
|
126
|
+
`on.workflow_call.outputs.${outputName}`,
|
|
127
|
+
access,
|
|
128
|
+
resolvedSpecs,
|
|
129
|
+
resolvedRefs,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Walk every reach-in for a single job and emit one pin per
|
|
138
|
+
* `(upstreamJobKey, specRef)` pair. Pins drive the runtime spec-version
|
|
139
|
+
* assertion (plan §C.1 risks-and-mitigations) — the worker pre-fetches
|
|
140
|
+
* the artifact and refuses to proceed when its `schemaVersion` doesn't
|
|
141
|
+
* match the pin's `specVersion`.
|
|
142
|
+
*
|
|
143
|
+
* Returns `[]` when the job has no resolvable reach-ins (no upstream
|
|
144
|
+
* spec, expression-shaped ref, or projected upstream `outputs:`).
|
|
145
|
+
*/
|
|
146
|
+
export function collectPayloadReachInsForJob(
|
|
147
|
+
doc: WorkflowDocument,
|
|
148
|
+
jobKey: string,
|
|
149
|
+
job: WorkflowDocument['jobs'][string],
|
|
150
|
+
resolvedSpecs: Readonly<Record<string, ResolvedDeliverableSpec>>,
|
|
151
|
+
resolvedRefs: Readonly<Record<string, ResolvedRef>>,
|
|
152
|
+
): readonly PayloadReachInPin[] {
|
|
153
|
+
if (Object.keys(resolvedSpecs).length === 0) return [];
|
|
154
|
+
|
|
155
|
+
const pins: PayloadReachInPin[] = [];
|
|
156
|
+
const seen = new Set<string>();
|
|
157
|
+
|
|
158
|
+
const expressions = collectJobExpressions(jobKey, job);
|
|
159
|
+
for (const { source, fieldPath } of expressions) {
|
|
160
|
+
let ast;
|
|
161
|
+
try {
|
|
162
|
+
ast = compileExpression(source);
|
|
163
|
+
} catch {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
walkPayloadReachIns(ast, (access) => {
|
|
167
|
+
const pin = resolveReachInPin(
|
|
168
|
+
doc,
|
|
169
|
+
fieldPath,
|
|
170
|
+
access,
|
|
171
|
+
resolvedSpecs,
|
|
172
|
+
resolvedRefs,
|
|
173
|
+
);
|
|
174
|
+
if (pin === null) return;
|
|
175
|
+
const dedupeKey = `${pin.upstreamJobKey}|${pin.outputName}|${pin.specRef}`;
|
|
176
|
+
if (seen.has(dedupeKey)) return;
|
|
177
|
+
seen.add(dedupeKey);
|
|
178
|
+
pins.push(pin);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return pins;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resolve one reach-in to a pin entry, returning `null` when the pin
|
|
187
|
+
* cannot be statically materialized (e.g. expression-shaped specRef,
|
|
188
|
+
* upstream has a custom `outputs:` projection, spec not in the resolved
|
|
189
|
+
* map). The validator emits the user-facing error path; this collector
|
|
190
|
+
* is silent on un-resolvable reach-ins because the validator already
|
|
191
|
+
* surfaced them.
|
|
192
|
+
*/
|
|
193
|
+
function resolveReachInPin(
|
|
194
|
+
doc: WorkflowDocument,
|
|
195
|
+
fieldPath: string,
|
|
196
|
+
access: PayloadReachIn,
|
|
197
|
+
resolvedSpecs: Readonly<Record<string, ResolvedDeliverableSpec>>,
|
|
198
|
+
resolvedRefs: Readonly<Record<string, ResolvedRef>>,
|
|
199
|
+
): PayloadReachInPin | null {
|
|
200
|
+
const upstream = doc.jobs[access.upstreamJobKey];
|
|
201
|
+
if (!upstream) return null;
|
|
202
|
+
if (upstream.outputs && Object.keys(upstream.outputs).length > 0) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Prefer per-output binding from the upstream activity manifest
|
|
207
|
+
// (plan §B). Falls back to the job-level `with.deliverableSpecRef`
|
|
208
|
+
// when no per-output binding is declared OR when the reach-in's
|
|
209
|
+
// outputName is one of the indexed sentinels.
|
|
210
|
+
let specRef: string | null = null;
|
|
211
|
+
let outputName: string | null = null;
|
|
212
|
+
if (
|
|
213
|
+
access.outputName !== '<indexed>' &&
|
|
214
|
+
access.outputName !== '<byKey>'
|
|
215
|
+
) {
|
|
216
|
+
const perOutputRef = lookupOutputBindingSpecRef(
|
|
217
|
+
upstream,
|
|
218
|
+
access.outputName,
|
|
219
|
+
resolvedRefs,
|
|
220
|
+
);
|
|
221
|
+
if (perOutputRef !== null) {
|
|
222
|
+
specRef = perOutputRef;
|
|
223
|
+
outputName = access.outputName;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (specRef === null) {
|
|
227
|
+
const withMap = upstream.with as Record<string, unknown> | undefined;
|
|
228
|
+
const fromWith = withMap?.['deliverableSpecRef'];
|
|
229
|
+
if (typeof fromWith !== 'string' || fromWith.length === 0) return null;
|
|
230
|
+
if (fromWith.includes('${{')) return null;
|
|
231
|
+
specRef = fromWith;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const spec = resolvedSpecs[specRef];
|
|
235
|
+
if (!spec) return null;
|
|
236
|
+
const at = specRef.lastIndexOf('@');
|
|
237
|
+
const specVersion = at >= 0 ? specRef.slice(at + 1) : '';
|
|
238
|
+
if (specVersion.length === 0) return null;
|
|
239
|
+
return {
|
|
240
|
+
fieldPath,
|
|
241
|
+
upstreamJobKey: access.upstreamJobKey,
|
|
242
|
+
outputName,
|
|
243
|
+
specRef,
|
|
244
|
+
specVersion,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
interface CollectedExpression {
|
|
249
|
+
readonly source: string;
|
|
250
|
+
readonly fieldPath: string;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function collectJobExpressions(
|
|
254
|
+
jobKey: string,
|
|
255
|
+
job: WorkflowDocument['jobs'][string],
|
|
256
|
+
): readonly CollectedExpression[] {
|
|
257
|
+
const out: CollectedExpression[] = [];
|
|
258
|
+
if (job.if !== undefined) {
|
|
259
|
+
out.push({ source: stripInterpolation(job.if), fieldPath: `${jobKey}.if` });
|
|
260
|
+
}
|
|
261
|
+
if (job.with) {
|
|
262
|
+
for (const ext of extractInterpolations(job.with, [jobKey, 'with'])) {
|
|
263
|
+
out.push({ source: ext.source, fieldPath: ext.path.join('.') });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (job.outputs) {
|
|
267
|
+
for (const [name, expr] of Object.entries(job.outputs)) {
|
|
268
|
+
out.push({
|
|
269
|
+
source: stripInterpolation(expr),
|
|
270
|
+
fieldPath: `${jobKey}.outputs.${name}`,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (job.strategy && 'dynamic' in job.strategy) {
|
|
275
|
+
out.push({
|
|
276
|
+
source: stripInterpolation(job.strategy.dynamic.from),
|
|
277
|
+
fieldPath: `${jobKey}.strategy.dynamic.from`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
interface PayloadReachIn {
|
|
284
|
+
readonly upstreamJobKey: string;
|
|
285
|
+
readonly outputName: string;
|
|
286
|
+
readonly field: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function walkPayloadReachIns(
|
|
290
|
+
node: ExpressionNode,
|
|
291
|
+
visit: (access: PayloadReachIn) => void,
|
|
292
|
+
): void {
|
|
293
|
+
const match = matchPayloadReachIn(node);
|
|
294
|
+
if (match !== null) {
|
|
295
|
+
visit(match);
|
|
296
|
+
// Continue recursion so nested expressions inside index nodes are
|
|
297
|
+
// also walked — e.g. byKey[needs.Y.outputs.foo.bar] would carry
|
|
298
|
+
// its own reach-in.
|
|
299
|
+
}
|
|
300
|
+
switch (node.kind) {
|
|
301
|
+
case ExpressionNodeKind.MEMBER:
|
|
302
|
+
walkPayloadReachIns(node.target, visit);
|
|
303
|
+
return;
|
|
304
|
+
case ExpressionNodeKind.INDEX:
|
|
305
|
+
walkPayloadReachIns(node.target, visit);
|
|
306
|
+
walkPayloadReachIns(node.index, visit);
|
|
307
|
+
return;
|
|
308
|
+
case ExpressionNodeKind.CALL:
|
|
309
|
+
// Skip `fromJSON(...)` and `toJSON(...)` arguments — those are
|
|
310
|
+
// the documented escape hatch; the author opted out of typed
|
|
311
|
+
// reach-in validation by wrapping the ref.
|
|
312
|
+
for (const arg of node.args) walkPayloadReachIns(arg, visit);
|
|
313
|
+
return;
|
|
314
|
+
case ExpressionNodeKind.UNARY_NOT:
|
|
315
|
+
walkPayloadReachIns(node.operand, visit);
|
|
316
|
+
return;
|
|
317
|
+
case ExpressionNodeKind.BINARY_EQ:
|
|
318
|
+
case ExpressionNodeKind.BINARY_NEQ:
|
|
319
|
+
case ExpressionNodeKind.BINARY_AND:
|
|
320
|
+
case ExpressionNodeKind.BINARY_OR:
|
|
321
|
+
walkPayloadReachIns(node.left, visit);
|
|
322
|
+
walkPayloadReachIns(node.right, visit);
|
|
323
|
+
return;
|
|
324
|
+
case ExpressionNodeKind.LITERAL:
|
|
325
|
+
case ExpressionNodeKind.IDENTIFIER:
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Match the outermost `needs.<X>.outputs.<name>.<field>` shape. Returns
|
|
332
|
+
* null if the chain doesn't match, OR if `<field>` is an envelope
|
|
333
|
+
* field (the runtime reads those off the ArtifactRef without a fetch).
|
|
334
|
+
*
|
|
335
|
+
* Also matches:
|
|
336
|
+
* needs.<X>.outputs[N].<field>
|
|
337
|
+
* needs.<X>.outputs.byKey['k'].<field>
|
|
338
|
+
*
|
|
339
|
+
* For these, `<name>` is set to a sentinel so the validator still
|
|
340
|
+
* resolves the upstream job's spec but doesn't attempt per-output
|
|
341
|
+
* name resolution (today's data model declares the spec at the
|
|
342
|
+
* job level, not per-output — see the plan's "B." section).
|
|
343
|
+
*/
|
|
344
|
+
function matchPayloadReachIn(node: ExpressionNode): PayloadReachIn | null {
|
|
345
|
+
if (node.kind !== ExpressionNodeKind.MEMBER) return null;
|
|
346
|
+
const field = node.property;
|
|
347
|
+
if (isArtifactRefEnvelopeField(field)) return null;
|
|
348
|
+
|
|
349
|
+
// Walk one level up: `<base>.<field>` where `<base>` must resolve to
|
|
350
|
+
// `needs.<X>.outputs.<name>` (or an indexed/keyed variant).
|
|
351
|
+
const base = node.target;
|
|
352
|
+
|
|
353
|
+
// Form A: needs.<X>.outputs.<name>.<field>
|
|
354
|
+
if (base.kind === ExpressionNodeKind.MEMBER) {
|
|
355
|
+
const outputName = base.property;
|
|
356
|
+
if (outputName === 'byKey') return null; // handled below
|
|
357
|
+
const outputsNode = base.target;
|
|
358
|
+
const upstreamKey = matchNeedsOutputsRoot(outputsNode);
|
|
359
|
+
if (upstreamKey !== null) {
|
|
360
|
+
return { upstreamJobKey: upstreamKey, outputName, field };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Form B: needs.<X>.outputs[N].<field> OR needs.<X>.outputs.byKey['k'].<field>
|
|
365
|
+
if (base.kind === ExpressionNodeKind.INDEX) {
|
|
366
|
+
const indexed = base.target;
|
|
367
|
+
// outputs[N]
|
|
368
|
+
const upstreamFromOutputs = matchNeedsOutputsRoot(indexed);
|
|
369
|
+
if (upstreamFromOutputs !== null) {
|
|
370
|
+
return {
|
|
371
|
+
upstreamJobKey: upstreamFromOutputs,
|
|
372
|
+
outputName: '<indexed>',
|
|
373
|
+
field,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
// outputs.byKey['k']
|
|
377
|
+
if (
|
|
378
|
+
indexed.kind === ExpressionNodeKind.MEMBER &&
|
|
379
|
+
indexed.property === 'byKey'
|
|
380
|
+
) {
|
|
381
|
+
const upstreamFromByKey = matchNeedsOutputsRoot(indexed.target);
|
|
382
|
+
if (upstreamFromByKey !== null) {
|
|
383
|
+
return {
|
|
384
|
+
upstreamJobKey: upstreamFromByKey,
|
|
385
|
+
outputName: '<byKey>',
|
|
386
|
+
field,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Match the chain `needs.<X>.outputs`. Returns the upstream job key, or
|
|
397
|
+
* null if the node isn't that shape.
|
|
398
|
+
*/
|
|
399
|
+
function matchNeedsOutputsRoot(node: ExpressionNode): string | null {
|
|
400
|
+
if (node.kind !== ExpressionNodeKind.MEMBER) return null;
|
|
401
|
+
if (node.property !== 'outputs') return null;
|
|
402
|
+
const needsRoot = node.target;
|
|
403
|
+
if (needsRoot.kind !== ExpressionNodeKind.MEMBER) return null;
|
|
404
|
+
const upstreamKey = needsRoot.property;
|
|
405
|
+
const ident = needsRoot.target;
|
|
406
|
+
if (ident.kind !== ExpressionNodeKind.IDENTIFIER) return null;
|
|
407
|
+
if (ident.name !== 'needs') return null;
|
|
408
|
+
return upstreamKey;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function validateReachIn(
|
|
412
|
+
doc: WorkflowDocument,
|
|
413
|
+
consumerJobKey: string,
|
|
414
|
+
fieldPath: string,
|
|
415
|
+
access: PayloadReachIn,
|
|
416
|
+
resolvedSpecs: Readonly<Record<string, ResolvedDeliverableSpec>>,
|
|
417
|
+
resolvedRefs: Readonly<Record<string, ResolvedRef>>,
|
|
418
|
+
): void {
|
|
419
|
+
const upstream = doc.jobs[access.upstreamJobKey];
|
|
420
|
+
if (!upstream) {
|
|
421
|
+
// Topological-sort step already raises a clearer error for unknown
|
|
422
|
+
// upstreams; don't double-report here.
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// If the upstream declares an `outputs:` projection, it re-shapes the
|
|
427
|
+
// exposed namespace. We can't statically introspect arbitrary author
|
|
428
|
+
// projections, so skip — the projection itself is expression-validated
|
|
429
|
+
// upstream.
|
|
430
|
+
if (upstream.outputs && Object.keys(upstream.outputs).length > 0) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Per-output binding from the upstream activity manifest (plan §B)
|
|
435
|
+
// wins over the job-level `with.deliverableSpecRef` when present.
|
|
436
|
+
const perOutputBinding =
|
|
437
|
+
access.outputName === '<indexed>' || access.outputName === '<byKey>'
|
|
438
|
+
? null
|
|
439
|
+
: lookupOutputBindingSpecRef(upstream, access.outputName, resolvedRefs);
|
|
440
|
+
|
|
441
|
+
const withMap = upstream.with as Record<string, unknown> | undefined;
|
|
442
|
+
const specRef = perOutputBinding ?? withMap?.['deliverableSpecRef'];
|
|
443
|
+
|
|
444
|
+
// Expression-shaped spec → unknowable at compile time. Runtime
|
|
445
|
+
// surfaces `DSL_PAYLOAD_PARSE_ERROR` if reach-in is ultimately wrong.
|
|
446
|
+
if (typeof specRef === 'string' && specRef.includes('${{')) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (typeof specRef !== 'string' || specRef.length === 0) {
|
|
451
|
+
throw new WorkflowDslError(
|
|
452
|
+
WorkflowErrorCode.DSL_FIELD_NOT_TYPED,
|
|
453
|
+
`${fieldPath}: 'needs.${access.upstreamJobKey}.outputs.${access.outputName}.${access.field}' reaches into the payload of an output that has no schema-bearing deliverable spec. Either bind a typed 'with.deliverableSpecRef' on '${access.upstreamJobKey}' or use the 'fromJSON(...)' escape hatch.`,
|
|
454
|
+
{
|
|
455
|
+
consumerJobKey,
|
|
456
|
+
upstreamJobKey: access.upstreamJobKey,
|
|
457
|
+
outputName: access.outputName,
|
|
458
|
+
field: access.field,
|
|
459
|
+
fieldPath,
|
|
460
|
+
},
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const spec = resolvedSpecs[specRef];
|
|
465
|
+
if (!spec) {
|
|
466
|
+
// Engine couldn't resolve the spec (e.g. registry unreachable) —
|
|
467
|
+
// the existing literal-reference validator already emits
|
|
468
|
+
// `DSL_UNKNOWN_DELIVERABLE_SPEC` for this case; don't double-report.
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (!spec.topLevelKeys || spec.topLevelKeys.length === 0) {
|
|
472
|
+
// Spec kind doesn't support static field introspection
|
|
473
|
+
// (DOCUMENT_TEMPLATE, STRUCTURED_JSON, CUSTOM, …). Defer to runtime.
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (spec.topLevelKeys.includes(access.field)) {
|
|
477
|
+
// Valid reach-in. The runtime is responsible for pre-fetching this
|
|
478
|
+
// artifact's payload into the per-step `payloadCache` before the
|
|
479
|
+
// expression evaluates — see the plan's runtime-resolver scaffold.
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const knownPreview = spec.topLevelKeys.slice(0, 12).join(', ');
|
|
484
|
+
throw new WorkflowDslError(
|
|
485
|
+
WorkflowErrorCode.DSL_FIELD_NOT_IN_SCHEMA,
|
|
486
|
+
`${fieldPath}: 'needs.${access.upstreamJobKey}.outputs.${access.outputName}.${access.field}' is not a top-level field on deliverable spec '${spec.ref}' (bound by '${access.upstreamJobKey}.with.deliverableSpecRef'). Known fields: [${knownPreview}].`,
|
|
487
|
+
{
|
|
488
|
+
consumerJobKey,
|
|
489
|
+
upstreamJobKey: access.upstreamJobKey,
|
|
490
|
+
outputName: access.outputName,
|
|
491
|
+
field: access.field,
|
|
492
|
+
fieldPath,
|
|
493
|
+
specRef: spec.ref,
|
|
494
|
+
knownFields: [...spec.topLevelKeys],
|
|
495
|
+
},
|
|
496
|
+
);
|
|
497
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PermissionResource,
|
|
3
|
+
PermissionScope,
|
|
4
|
+
WorkflowErrorCode,
|
|
5
|
+
} from '@xemahq/kernel-contracts/workflow';
|
|
6
|
+
import { WorkflowDslError } from '../errors';
|
|
7
|
+
import type { WorkflowPermissions } from '../types';
|
|
8
|
+
|
|
9
|
+
/** Scope strength ordering: stronger scopes dominate weaker ones. */
|
|
10
|
+
const SCOPE_RANK: Readonly<Record<PermissionScope, number>> = Object.freeze({
|
|
11
|
+
[PermissionScope.NONE]: 0,
|
|
12
|
+
[PermissionScope.READ]: 1,
|
|
13
|
+
[PermissionScope.LIMITED]: 2,
|
|
14
|
+
[PermissionScope.WRITE]: 3,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/** Zero permissions — the platform-safe default when nothing is authored. */
|
|
18
|
+
export const ZERO_PERMISSIONS: Readonly<Record<PermissionResource, PermissionScope>> =
|
|
19
|
+
Object.freeze({
|
|
20
|
+
[PermissionResource.REPOS]: PermissionScope.NONE,
|
|
21
|
+
[PermissionResource.KB]: PermissionScope.NONE,
|
|
22
|
+
[PermissionResource.BACKLOG]: PermissionScope.NONE,
|
|
23
|
+
[PermissionResource.INTEGRATIONS]: PermissionScope.NONE,
|
|
24
|
+
[PermissionResource.ARTIFACTS]: PermissionScope.NONE,
|
|
25
|
+
[PermissionResource.MEMORY]: PermissionScope.NONE,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/** Fill an authored permission map with explicit NONE values for unset resources. */
|
|
29
|
+
export function normalizePermissions(
|
|
30
|
+
authored: WorkflowPermissions | undefined,
|
|
31
|
+
): Readonly<Record<PermissionResource, PermissionScope>> {
|
|
32
|
+
const base: Record<PermissionResource, PermissionScope> = { ...ZERO_PERMISSIONS };
|
|
33
|
+
if (!authored) return base;
|
|
34
|
+
for (const resource of Object.values(PermissionResource)) {
|
|
35
|
+
const scope = authored[resource];
|
|
36
|
+
if (scope !== undefined) {
|
|
37
|
+
base[resource] = scope;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return base;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Verify that `job` does not request stronger permissions than `workflow`.
|
|
45
|
+
* Fails fast with DSL_PERMISSION_ESCALATION on any escalation — rule 9
|
|
46
|
+
* (clean architecture), rule 2 (fail-fast).
|
|
47
|
+
*/
|
|
48
|
+
export function assertJobPermissionsFit(
|
|
49
|
+
jobPermissions: Readonly<Record<PermissionResource, PermissionScope>>,
|
|
50
|
+
workflowPermissions: Readonly<Record<PermissionResource, PermissionScope>>,
|
|
51
|
+
jobKey: string,
|
|
52
|
+
): void {
|
|
53
|
+
for (const resource of Object.values(PermissionResource)) {
|
|
54
|
+
const jobScope = jobPermissions[resource];
|
|
55
|
+
const workflowScope = workflowPermissions[resource];
|
|
56
|
+
if (SCOPE_RANK[jobScope] > SCOPE_RANK[workflowScope]) {
|
|
57
|
+
throw new WorkflowDslError(
|
|
58
|
+
WorkflowErrorCode.DSL_PERMISSION_ESCALATION,
|
|
59
|
+
`Job '${jobKey}' requests ${jobScope} on ${resource} but workflow grants only ${workflowScope}.`,
|
|
60
|
+
{ jobKey, resource, jobScope, workflowScope },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CompiledDefaults,
|
|
3
|
+
CompiledRetryPolicy,
|
|
4
|
+
} from '@xemahq/kernel-contracts/workflow';
|
|
5
|
+
import type {
|
|
6
|
+
ActionManifestRetryDefaults,
|
|
7
|
+
ActionManifestTimeoutDefaults,
|
|
8
|
+
WorkflowDefaults,
|
|
9
|
+
WorkflowRetryDeclaration,
|
|
10
|
+
} from '../types';
|
|
11
|
+
import { parseDurationMs } from '../duration';
|
|
12
|
+
|
|
13
|
+
// Default retry applied when neither the workflow nor the action declares
|
|
14
|
+
// one. Deliberately conservative — one attempt, no backoff — so authors
|
|
15
|
+
// opt into retry explicitly.
|
|
16
|
+
const PLATFORM_DEFAULT_RETRY: CompiledRetryPolicy = {
|
|
17
|
+
maxAttempts: 1,
|
|
18
|
+
initialIntervalMs: 1_000,
|
|
19
|
+
backoffCoefficient: 2.0,
|
|
20
|
+
maximumIntervalMs: 60_000,
|
|
21
|
+
nonRetryableErrorTypes: [],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Default job timeout: 1 hour. Agent activities that need more declare it
|
|
25
|
+
// explicitly in their action manifest or in the job's timeout field.
|
|
26
|
+
const PLATFORM_DEFAULT_TIMEOUT_MS = 3_600_000;
|
|
27
|
+
|
|
28
|
+
/** Merge workflow-level defaults against platform baseline. */
|
|
29
|
+
export function resolveWorkflowDefaults(
|
|
30
|
+
authored: WorkflowDefaults | undefined,
|
|
31
|
+
): CompiledDefaults {
|
|
32
|
+
return {
|
|
33
|
+
retry: resolveRetry(authored?.retry, PLATFORM_DEFAULT_RETRY),
|
|
34
|
+
timeoutMs: authored?.timeout !== undefined
|
|
35
|
+
? parseDurationMs(authored.timeout)
|
|
36
|
+
: PLATFORM_DEFAULT_TIMEOUT_MS,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Merge job-level retry against workflow-level + action-manifest-level
|
|
42
|
+
* defaults. Merge order: action manifest → workflow → job (most specific
|
|
43
|
+
* wins). Explicit; no "if null, skip" silent fallbacks.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveJobRetry(
|
|
46
|
+
workflowDefault: CompiledRetryPolicy,
|
|
47
|
+
actionDefault: ActionManifestRetryDefaults | null,
|
|
48
|
+
jobOverride: WorkflowRetryDeclaration | undefined,
|
|
49
|
+
): CompiledRetryPolicy {
|
|
50
|
+
const actionBaseline = actionDefault
|
|
51
|
+
? fromActionManifestRetry(actionDefault)
|
|
52
|
+
: workflowDefault;
|
|
53
|
+
|
|
54
|
+
return resolveRetry(jobOverride, actionBaseline);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve job-level timeout. Merge order: action manifest → workflow → job.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveJobTimeout(
|
|
61
|
+
workflowDefaultMs: number,
|
|
62
|
+
actionDefault: ActionManifestTimeoutDefaults | null,
|
|
63
|
+
jobOverride: string | undefined,
|
|
64
|
+
): number {
|
|
65
|
+
if (jobOverride !== undefined) {
|
|
66
|
+
return parseDurationMs(jobOverride);
|
|
67
|
+
}
|
|
68
|
+
if (actionDefault) {
|
|
69
|
+
return parseDurationMs(actionDefault.startToClose);
|
|
70
|
+
}
|
|
71
|
+
return workflowDefaultMs;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveRetry(
|
|
75
|
+
override: WorkflowRetryDeclaration | undefined,
|
|
76
|
+
baseline: CompiledRetryPolicy,
|
|
77
|
+
): CompiledRetryPolicy {
|
|
78
|
+
if (!override) return baseline;
|
|
79
|
+
return {
|
|
80
|
+
maxAttempts: override.maxAttempts ?? baseline.maxAttempts,
|
|
81
|
+
initialIntervalMs:
|
|
82
|
+
override.initialInterval !== undefined
|
|
83
|
+
? parseDurationMs(override.initialInterval)
|
|
84
|
+
: baseline.initialIntervalMs,
|
|
85
|
+
backoffCoefficient:
|
|
86
|
+
override.backoffCoefficient ?? baseline.backoffCoefficient,
|
|
87
|
+
maximumIntervalMs:
|
|
88
|
+
override.maximumInterval !== undefined
|
|
89
|
+
? parseDurationMs(override.maximumInterval)
|
|
90
|
+
: baseline.maximumIntervalMs,
|
|
91
|
+
nonRetryableErrorTypes: override.nonRetryableErrorTypes !== undefined
|
|
92
|
+
? [...override.nonRetryableErrorTypes]
|
|
93
|
+
: [...baseline.nonRetryableErrorTypes],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function fromActionManifestRetry(retry: ActionManifestRetryDefaults): CompiledRetryPolicy {
|
|
98
|
+
return {
|
|
99
|
+
maxAttempts: retry.maxAttempts,
|
|
100
|
+
initialIntervalMs: parseDurationMs(retry.initialInterval),
|
|
101
|
+
backoffCoefficient: retry.backoffCoefficient,
|
|
102
|
+
maximumIntervalMs: parseDurationMs(retry.maximumInterval),
|
|
103
|
+
nonRetryableErrorTypes: retry.nonRetryableErrorTypes !== undefined ? [...retry.nonRetryableErrorTypes] : [],
|
|
104
|
+
};
|
|
105
|
+
}
|