@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,172 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ActionExecutionKind,
|
|
3
|
-
Briefcase,
|
|
4
|
-
TaskQueueName,
|
|
5
|
-
TriggerPayload,
|
|
6
|
-
WalletContents,
|
|
7
|
-
WorkflowRef,
|
|
8
|
-
} from '@xemahq/kernel-contracts/workflow';
|
|
9
|
-
import type { ActionManifest, WorkflowDocument } from '../types';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Everything the compiler needs beyond the workflow document itself. The
|
|
13
|
-
* engine gathers these inputs before calling `compileWorkflow(...)`:
|
|
14
|
-
*
|
|
15
|
-
* - `workflow` — validated WorkflowDocument (pass through
|
|
16
|
-
* `validateWorkflowDocument` first).
|
|
17
|
-
* - `workflowRef` — slug/revision/content-contract identity, resolved from the DB.
|
|
18
|
-
* - `trigger` — the payload that fired this run.
|
|
19
|
-
* - `resolvedRefs` — map of `uses:` strings to resolved refs, populated
|
|
20
|
-
* by looking up each job's `uses:` in the action-manifest registry
|
|
21
|
-
* (for `xema/...`) or the reusable-workflow registry (for
|
|
22
|
-
* `xema://workflow/...`).
|
|
23
|
-
* - `workflowDefinitionContentHash` — content hash of the immutable source revision the
|
|
24
|
-
* compiler is consuming. Emitted into CompiledRun for audit.
|
|
25
|
-
*/
|
|
26
|
-
export interface CompileInput {
|
|
27
|
-
readonly workflow: WorkflowDocument;
|
|
28
|
-
readonly workflowRef: WorkflowRef;
|
|
29
|
-
readonly trigger: TriggerPayload;
|
|
30
|
-
readonly resolvedRefs: Readonly<Record<string, ResolvedRef>>;
|
|
31
|
-
/**
|
|
32
|
-
* Map of authored literal agent ref → immutable Agent revision metadata.
|
|
33
|
-
* The engine resolves each exact ref through llm-registry-api before
|
|
34
|
-
* compilation. Literal refs are therefore compile-time selectors only:
|
|
35
|
-
* CompiledRun stores the immutable revision id + content hash.
|
|
36
|
-
* Expression-shaped refs (e.g. `${{ inputs.agentSlug }}`) are skipped
|
|
37
|
-
* — they resolve at dispatch and the activity's `AGENT_NOT_REGISTERED`
|
|
38
|
-
* preflight catches them at runtime.
|
|
39
|
-
*
|
|
40
|
-
* Pass an empty map (or omit) to opt out of agent validation (e.g.
|
|
41
|
-
* for the `preview-raw` endpoint when the engine cannot reach the
|
|
42
|
-
* registry, or for tests that don't exercise the registry).
|
|
43
|
-
*/
|
|
44
|
-
readonly resolvedAgents?: Readonly<Record<string, ResolvedAgentMeta>>;
|
|
45
|
-
/**
|
|
46
|
-
* Map of literal deliverable-spec refs (used in `with.deliverableSpecRef`
|
|
47
|
-
* and the `produces:` array) to the spec metadata the compiler needs
|
|
48
|
-
* for validation. The engine pre-fetches this from
|
|
49
|
-
* deliverable-specs-api. Same literal-vs-expression rule as
|
|
50
|
-
* `resolvedAgents`. Same opt-out semantics: empty / omitted = skip.
|
|
51
|
-
*/
|
|
52
|
-
readonly resolvedDeliverableSpecs?: Readonly<
|
|
53
|
-
Record<string, ResolvedDeliverableSpec>
|
|
54
|
-
>;
|
|
55
|
-
readonly workflowDefinitionContentHash: string;
|
|
56
|
-
/**
|
|
57
|
-
* Contents of every wallet referenced under `requires.wallets`, plus
|
|
58
|
-
* any additional wallets a dispatch caller asked for. The compiler
|
|
59
|
-
* unions the entries to derive `CompiledRun.requiredVariables` and to
|
|
60
|
-
* validate `${{ vars.X }}` / `${{ secrets.X }}` references. When
|
|
61
|
-
* omitted, wallet-derived validation is skipped — used by
|
|
62
|
-
* `preview-raw` during authoring where the engine can't reach the
|
|
63
|
-
* variable store.
|
|
64
|
-
*/
|
|
65
|
-
readonly resolvedWallets?: Readonly<Record<string, WalletContents>>;
|
|
66
|
-
/**
|
|
67
|
-
* Installation scope of this dispatch — present when the workflow is
|
|
68
|
-
* dispatched from a biome installation (webhook trigger gated on
|
|
69
|
-
* the installation, manual dispatch from the installation's UX, a
|
|
70
|
-
* scheduled trigger tied to the installation). Absent for system /
|
|
71
|
-
* org-wide dispatches that don't belong to any installation.
|
|
72
|
-
*
|
|
73
|
-
* The compiler's installation-resource validator uses this scope to
|
|
74
|
-
* resolve `x-installation-resource: { kind: ... }` references in
|
|
75
|
-
* action input schemas against the installation's bound resources
|
|
76
|
-
* (wallets / repos / channels / …). When `installationScope` is
|
|
77
|
-
* undefined, the validator skips installation-bound fields — system
|
|
78
|
-
* workflows can still reference wallet ids, but compile-time
|
|
79
|
-
* binding-set validation only happens for biome-installed runs.
|
|
80
|
-
*/
|
|
81
|
-
readonly installationScope?: InstallationCompileScope;
|
|
82
|
-
/**
|
|
83
|
-
* When true, the compiler substitutes type-appropriate sentinel values
|
|
84
|
-
* for missing required inputs instead of throwing. Intended only for
|
|
85
|
-
* the `preview-raw` endpoint (Monaco editor live feedback) where the
|
|
86
|
-
* caller deliberately omits real input values during YAML authoring.
|
|
87
|
-
* Never set this on real run compilation paths.
|
|
88
|
-
*/
|
|
89
|
-
readonly previewMode?: boolean;
|
|
90
|
-
/**
|
|
91
|
-
* Run-scoped Briefcase the dispatch caller attached to this run.
|
|
92
|
-
* Synthesized server-side from the StartRun request body (uploads /
|
|
93
|
-
* references / vars / mcpTools) and threaded into the CompiledRun so
|
|
94
|
-
* the run's snapshot captures the dispatch payload alongside its
|
|
95
|
-
* inputs and trigger. Absent for runs without dispatch-attached
|
|
96
|
-
* context (compiler treats absent and `emptyBriefcase()` identically
|
|
97
|
-
* downstream).
|
|
98
|
-
*/
|
|
99
|
-
readonly briefcase?: Briefcase;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Installation context the engine pre-fetches at dispatch time. Each
|
|
104
|
-
* `boundResources` entry lists the resource ids the installation is
|
|
105
|
-
* bound to for a single `InstallationResourceKind` — the compiler
|
|
106
|
-
* checks every literal value in a `x-installation-resource` field
|
|
107
|
-
* against the matching kind's set and rejects unbound values.
|
|
108
|
-
*
|
|
109
|
-
* The engine populates this from biome-host-api right before calling
|
|
110
|
-
* the compiler. Passing an empty array for a kind means "no resources
|
|
111
|
-
* bound" — every literal value for that kind fails validation.
|
|
112
|
-
*/
|
|
113
|
-
export interface InstallationCompileScope {
|
|
114
|
-
readonly installationId: string;
|
|
115
|
-
readonly orgId: string;
|
|
116
|
-
readonly projectId: string;
|
|
117
|
-
readonly biomeId: string;
|
|
118
|
-
readonly boundResources: Readonly<Record<string, readonly string[]>>;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Minimal agent metadata the compiler keeps for each resolved agent,
|
|
123
|
-
* keyed by bare slug (the slug segment of a `with.agentRef`). The engine
|
|
124
|
-
* fetches the full row from llm-registry-api but the compiler only needs
|
|
125
|
-
* the existence proof and the snapshot hash (so a future `CompiledRun`
|
|
126
|
-
* can carry it for cache-invalidation matching). Extend with more fields
|
|
127
|
-
* only when the compiler actually consumes them.
|
|
128
|
-
*/
|
|
129
|
-
export interface ResolvedAgentMeta {
|
|
130
|
-
readonly slug: string;
|
|
131
|
-
readonly agentRevisionId: string;
|
|
132
|
-
readonly agentContentHash: string;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Minimal deliverable-spec metadata the compiler keeps for each literal
|
|
137
|
-
* `deliverableSpecRef`. Same minimality rule as `ResolvedAgentMeta`.
|
|
138
|
-
*
|
|
139
|
-
* Optional `topLevelKeys` enables compile-time field validation for
|
|
140
|
-
* expressions of shape
|
|
141
|
-
* `${{ job.outputs.deliverable.content.value.<field> }}` — when
|
|
142
|
-
* populated, the DSL asserts `<field>` is one of these. The engine
|
|
143
|
-
* extracts the keys from the spec's `zodSchemaSource` (Zod) or
|
|
144
|
-
* `content`'s `properties` (JSON Schema) at registry-fetch time. Specs
|
|
145
|
-
* whose kind has no known shape (e.g. CUSTOM, ENDPOINT_FETCH) leave it
|
|
146
|
-
* undefined and the validator skips them.
|
|
147
|
-
*/
|
|
148
|
-
export interface ResolvedDeliverableSpec {
|
|
149
|
-
readonly ref: string;
|
|
150
|
-
readonly slug: string;
|
|
151
|
-
readonly snapshotHash: string;
|
|
152
|
-
readonly topLevelKeys?: readonly string[];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Resolved reference for a single `uses:` value. Emitted by the engine's
|
|
157
|
-
* ref-resolution layer before compilation; the compiler trusts it blindly.
|
|
158
|
-
*
|
|
159
|
-
* For action refs, `actionManifest` is the full manifest (the compiler
|
|
160
|
-
* needs retry/timeout/allowedMounts). For reusable-workflow refs,
|
|
161
|
-
* `actionManifest` is null — reusable workflows have their own compilation
|
|
162
|
-
* pass, so this outer compilation only records the pointer.
|
|
163
|
-
*/
|
|
164
|
-
export interface ResolvedRef {
|
|
165
|
-
readonly id: string;
|
|
166
|
-
readonly version: string;
|
|
167
|
-
readonly manifestSha256: string;
|
|
168
|
-
readonly executionKind: ActionExecutionKind;
|
|
169
|
-
readonly taskQueue: TaskQueueName;
|
|
170
|
-
readonly isReusableWorkflow: boolean;
|
|
171
|
-
readonly actionManifest: ActionManifest | null;
|
|
172
|
-
}
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
WorkflowErrorCode,
|
|
3
|
-
type WalletContents,
|
|
4
|
-
type WorkflowVariableRequirement,
|
|
5
|
-
} from '@xemahq/kernel-contracts/workflow';
|
|
6
|
-
import { WorkflowDslError } from '../errors';
|
|
7
|
-
import {
|
|
8
|
-
compileExpression,
|
|
9
|
-
ExpressionNodeKind,
|
|
10
|
-
extractInterpolations,
|
|
11
|
-
type ExpressionNode,
|
|
12
|
-
} from '../expression';
|
|
13
|
-
import { ExpressionRoot } from '../expression/ast';
|
|
14
|
-
import { stripInterpolation } from '../expression/interpolation';
|
|
15
|
-
import type { WorkflowDocument } from '../types';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Compile-time handling of `requires.wallets` and the `vars.*` /
|
|
19
|
-
* `secrets.*` expression namespaces.
|
|
20
|
-
*
|
|
21
|
-
* The compiler trusts the engine to pre-fetch the contents of every
|
|
22
|
-
* wallet listed in `workflow.requires.wallets` (plus any additional
|
|
23
|
-
* wallets the dispatch caller asked for) and pass them as the
|
|
24
|
-
* `resolvedWallets` map. Two responsibilities here, both fail-fast:
|
|
25
|
-
*
|
|
26
|
-
* 1. `WORKFLOW_WALLET_NOT_FOUND` — every wallet name declared under
|
|
27
|
-
* `requires.wallets` must have an entry in `resolvedWallets`.
|
|
28
|
-
* Missing entry = the engine couldn't find the wallet for the
|
|
29
|
-
* workflow's project/org scope.
|
|
30
|
-
*
|
|
31
|
-
* 2. `DSL_EXPRESSION_INVALID` — every `${{ vars.X }}` / `${{ secrets.X }}`
|
|
32
|
-
* member access must resolve to a key in the union of the resolved
|
|
33
|
-
* wallets' contents (modulo the YAML-static `vars:` block which can
|
|
34
|
-
* also satisfy a `vars.X` lookup). Catches typos / stale references
|
|
35
|
-
* at compile time instead of a runtime evaluator failure.
|
|
36
|
-
*
|
|
37
|
-
* The function returns the canonical flat list emitted into
|
|
38
|
-
* `CompiledRun.requiredVariables` — the union of every wallet's
|
|
39
|
-
* `(key, isSecret)` entries with duplicates collapsed (last writer wins
|
|
40
|
-
* for `isSecret` if two wallets disagree; the engine surfaces the
|
|
41
|
-
* conflict at fetch time with a different code).
|
|
42
|
-
*/
|
|
43
|
-
export function resolveWalletRequirements(
|
|
44
|
-
doc: WorkflowDocument,
|
|
45
|
-
resolvedWallets: Readonly<Record<string, WalletContents>> | undefined,
|
|
46
|
-
): readonly WorkflowVariableRequirement[] {
|
|
47
|
-
const declaredWallets = doc.requires?.wallets ?? [];
|
|
48
|
-
|
|
49
|
-
if (resolvedWallets === undefined) {
|
|
50
|
-
// Engine opted out (preview-raw / tests). Skip both checks and
|
|
51
|
-
// emit an empty requirements list — `${{ vars.X }}` / `${{ secrets.X }}`
|
|
52
|
-
// references that don't satisfy the YAML-static fallback will still
|
|
53
|
-
// surface at runtime, which is the documented contract for opt-out.
|
|
54
|
-
return Object.freeze([]);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const missingWallets: string[] = [];
|
|
58
|
-
for (const name of declaredWallets) {
|
|
59
|
-
if (!Object.hasOwn(resolvedWallets, name)) {
|
|
60
|
-
missingWallets.push(name);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (missingWallets.length > 0) {
|
|
64
|
-
throw new WorkflowDslError(
|
|
65
|
-
WorkflowErrorCode.WORKFLOW_WALLET_NOT_FOUND,
|
|
66
|
-
`Wallet(s) declared in requires.wallets are not available for this project/org scope: ${missingWallets.join(', ')}`,
|
|
67
|
-
{ missing: missingWallets },
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const merged = new Map<string, WorkflowVariableRequirement>();
|
|
72
|
-
for (const name of declaredWallets) {
|
|
73
|
-
const wallet = resolvedWallets[name];
|
|
74
|
-
if (!wallet) {
|
|
75
|
-
// `declaredWallets` is derived from the same `resolvedWallets`
|
|
76
|
-
// map upstream, so a missing entry here would be an internal
|
|
77
|
-
// invariant violation — surface it instead of silently producing
|
|
78
|
-
// an empty merge.
|
|
79
|
-
throw new Error(
|
|
80
|
-
`Internal: declaredWallets references unresolved wallet '${name}'.`,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
for (const entry of wallet.entries) {
|
|
84
|
-
merged.set(entry.key, {
|
|
85
|
-
name: entry.key,
|
|
86
|
-
secret: entry.isSecret,
|
|
87
|
-
optional: false,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return Object.freeze([...merged.values()]);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Walk every `${{ ... }}` expression body in the workflow and, for each
|
|
96
|
-
* `vars.<NAME>` / `secrets.<NAME>` member access, verify the referenced
|
|
97
|
-
* name is satisfiable from the resolved wallets (or, for `vars.*` only,
|
|
98
|
-
* from the YAML-static `vars:` block).
|
|
99
|
-
*/
|
|
100
|
-
export function validateVariableReferences(
|
|
101
|
-
doc: WorkflowDocument,
|
|
102
|
-
requiredVariables: readonly WorkflowVariableRequirement[],
|
|
103
|
-
resolvedWallets: Readonly<Record<string, WalletContents>> | undefined,
|
|
104
|
-
): void {
|
|
105
|
-
if (resolvedWallets === undefined) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
const staticVarNames = new Set(Object.keys(doc.vars ?? {}));
|
|
109
|
-
const allowedVars = new Set<string>(staticVarNames);
|
|
110
|
-
const allowedSecrets = new Set<string>();
|
|
111
|
-
for (const r of requiredVariables) {
|
|
112
|
-
if (r.secret) {
|
|
113
|
-
allowedSecrets.add(r.name);
|
|
114
|
-
} else {
|
|
115
|
-
allowedVars.add(r.name);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
for (const { source, pathLabel } of collectAllExpressions(doc)) {
|
|
120
|
-
const ast = compileExpression(source);
|
|
121
|
-
walkVariableAccesses(ast, (root, name) => {
|
|
122
|
-
if (root === ExpressionRoot.VARS && !allowedVars.has(name)) {
|
|
123
|
-
throw new WorkflowDslError(
|
|
124
|
-
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
125
|
-
`Expression at ${pathLabel} reads vars.${name}, but '${name}' is not provided by workflow.vars or any wallet in requires.wallets.`,
|
|
126
|
-
{ pathLabel, root, name, source },
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
if (root === ExpressionRoot.SECRETS && !allowedSecrets.has(name)) {
|
|
130
|
-
throw new WorkflowDslError(
|
|
131
|
-
WorkflowErrorCode.DSL_EXPRESSION_INVALID,
|
|
132
|
-
`Expression at ${pathLabel} reads secrets.${name}, but '${name}' is not provided by any wallet in requires.wallets (or the wallet entry is not marked secret).`,
|
|
133
|
-
{ pathLabel, root, name, source },
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
interface CollectedExpression {
|
|
141
|
-
readonly source: string;
|
|
142
|
-
readonly pathLabel: string;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function collectAllExpressions(doc: WorkflowDocument): CollectedExpression[] {
|
|
146
|
-
const out: CollectedExpression[] = [];
|
|
147
|
-
for (const [jobKey, job] of Object.entries(doc.jobs)) {
|
|
148
|
-
for (const expr of extractInterpolations(job.with, ['jobs', jobKey, 'with'])) {
|
|
149
|
-
out.push({ source: expr.source, pathLabel: expr.path.join('.') });
|
|
150
|
-
}
|
|
151
|
-
for (const expr of extractInterpolations(job.outputs, ['jobs', jobKey, 'outputs'])) {
|
|
152
|
-
out.push({ source: expr.source, pathLabel: expr.path.join('.') });
|
|
153
|
-
}
|
|
154
|
-
if (typeof job.if === 'string' && job.if.length > 0) {
|
|
155
|
-
out.push({ source: stripInterpolation(job.if), pathLabel: `jobs.${jobKey}.if` });
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return out;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Visits every `<root>.<name>` member access in the AST and invokes
|
|
163
|
-
* `onAccess(root, name)`. Other access shapes are ignored — the caller
|
|
164
|
-
* validates only the first hop, which is sufficient because both
|
|
165
|
-
* `vars.*` and `secrets.*` are string-keyed flat maps.
|
|
166
|
-
*/
|
|
167
|
-
function walkVariableAccesses(
|
|
168
|
-
node: ExpressionNode,
|
|
169
|
-
onAccess: (root: ExpressionRoot, name: string) => void,
|
|
170
|
-
): void {
|
|
171
|
-
switch (node.kind) {
|
|
172
|
-
case ExpressionNodeKind.MEMBER: {
|
|
173
|
-
const target = node.target;
|
|
174
|
-
if (
|
|
175
|
-
target.kind === ExpressionNodeKind.IDENTIFIER &&
|
|
176
|
-
(target.name === ExpressionRoot.VARS ||
|
|
177
|
-
target.name === ExpressionRoot.SECRETS)
|
|
178
|
-
) {
|
|
179
|
-
onAccess(target.name as ExpressionRoot, node.property);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
walkVariableAccesses(target, onAccess);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
case ExpressionNodeKind.INDEX:
|
|
186
|
-
walkVariableAccesses(node.target, onAccess);
|
|
187
|
-
walkVariableAccesses(node.index, onAccess);
|
|
188
|
-
return;
|
|
189
|
-
case ExpressionNodeKind.CALL:
|
|
190
|
-
for (const arg of node.args) {
|
|
191
|
-
walkVariableAccesses(arg, onAccess);
|
|
192
|
-
}
|
|
193
|
-
return;
|
|
194
|
-
case ExpressionNodeKind.UNARY_NOT:
|
|
195
|
-
walkVariableAccesses(node.operand, onAccess);
|
|
196
|
-
return;
|
|
197
|
-
case ExpressionNodeKind.BINARY_EQ:
|
|
198
|
-
case ExpressionNodeKind.BINARY_NEQ:
|
|
199
|
-
case ExpressionNodeKind.BINARY_AND:
|
|
200
|
-
case ExpressionNodeKind.BINARY_OR:
|
|
201
|
-
walkVariableAccesses(node.left, onAccess);
|
|
202
|
-
walkVariableAccesses(node.right, onAccess);
|
|
203
|
-
return;
|
|
204
|
-
case ExpressionNodeKind.IDENTIFIER:
|
|
205
|
-
case ExpressionNodeKind.LITERAL:
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Typed failure raised when a deliverable spec's Zod source cannot be
|
|
3
|
-
* introspected UNAMBIGUOUSLY. Distinct from the `null` return of the
|
|
4
|
-
* extractors, which means "this source has no introspectable shape at
|
|
5
|
-
* all" — a spec kind or body we simply cannot read. This error means the
|
|
6
|
-
* opposite: the source DOES declare a shape, but the spec has not said
|
|
7
|
-
* WHICH one is the contract, so any answer would be a guess.
|
|
8
|
-
*
|
|
9
|
-
* Callers MUST branch on `.reason` — free-form message matching is
|
|
10
|
-
* forbidden so handling stays exhaustive (same convention as
|
|
11
|
-
* `WorkflowDslError.code`).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/** Closed set of introspection failure reasons. */
|
|
15
|
-
export enum DeliverableSpecIntrospectionFailure {
|
|
16
|
-
/**
|
|
17
|
-
* The source declares two or more top-level `z.object({ … })` schemas
|
|
18
|
-
* and the spec did not name which one is the contract. Resolving this
|
|
19
|
-
* by position ("the first one") is guesswork: a schema hoisted above
|
|
20
|
-
* the contract purely so the contract can reference it would silently
|
|
21
|
-
* become the contract.
|
|
22
|
-
*/
|
|
23
|
-
AmbiguousSchema = 'AMBIGUOUS_SCHEMA',
|
|
24
|
-
/**
|
|
25
|
-
* The spec named a contract export (`zodSchemaExport`) that the source
|
|
26
|
-
* does not declare as a top-level `z.object({ … })` — a typo, a rename,
|
|
27
|
-
* or an unbalanced literal the scanner could not close.
|
|
28
|
-
*/
|
|
29
|
-
ExportNotFound = 'EXPORT_NOT_FOUND',
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class DeliverableSpecIntrospectionError extends Error {
|
|
33
|
-
readonly reason: DeliverableSpecIntrospectionFailure;
|
|
34
|
-
readonly details: Readonly<Record<string, unknown>>;
|
|
35
|
-
|
|
36
|
-
constructor(
|
|
37
|
-
reason: DeliverableSpecIntrospectionFailure,
|
|
38
|
-
message: string,
|
|
39
|
-
details: Readonly<Record<string, unknown>> = {},
|
|
40
|
-
) {
|
|
41
|
-
super(message);
|
|
42
|
-
this.name = 'DeliverableSpecIntrospectionError';
|
|
43
|
-
this.reason = reason;
|
|
44
|
-
this.details = details;
|
|
45
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
toJSON(): Readonly<Record<string, unknown>> {
|
|
49
|
-
return {
|
|
50
|
-
name: this.name,
|
|
51
|
-
reason: this.reason,
|
|
52
|
-
message: this.message,
|
|
53
|
-
details: this.details,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Narrowing helper. */
|
|
59
|
-
export function isDeliverableSpecIntrospectionError(
|
|
60
|
-
err: unknown,
|
|
61
|
-
): err is DeliverableSpecIntrospectionError {
|
|
62
|
-
return err instanceof DeliverableSpecIntrospectionError;
|
|
63
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Static introspection of deliverable-spec content to recover the
|
|
3
|
-
* top-level keys an agent's output object MUST expose. The compiler uses
|
|
4
|
-
* these keys to validate workflow expressions of shape
|
|
5
|
-
* `${{ job.outputs.deliverable.content.value.<field> }}` against the
|
|
6
|
-
* spec the producing job declared via `with.deliverableSpecRef` —
|
|
7
|
-
* catching "the YAML reads `.changeUnits` but the spec has
|
|
8
|
-
* `.handoffPackage`" at compile time, not at runtime when projection
|
|
9
|
-
* blows up. The same keys are the ONLY content gate the harvester has
|
|
10
|
-
* for a `ZOD_SCHEMA` spec (nothing anywhere evaluates the Zod schema),
|
|
11
|
-
* so a wrong key set silently rejects correct agent output.
|
|
12
|
-
*
|
|
13
|
-
* Two extractors:
|
|
14
|
-
* - `extractZodTopLevelObjectKeys`: reads the field names of the
|
|
15
|
-
* `z.object({ … })` literal a spec designates as its contract, out of
|
|
16
|
-
* TypeScript Zod source. Same scanner the deliverable-specs-api
|
|
17
|
-
* ZodSchemaHandler uses for harvest-time validation; single source of
|
|
18
|
-
* truth lives here so the DSL compiler and the harvester agree.
|
|
19
|
-
* - `extractJsonSchemaTopLevelKeys`: pulls keys from the `properties`
|
|
20
|
-
* object of a JSON Schema document.
|
|
21
|
-
*
|
|
22
|
-
* Both return `null` when a source has NO introspectable shape, so
|
|
23
|
-
* callers can skip the field check (preview / specs without introspectable
|
|
24
|
-
* shape). `null` never means "we guessed" — an unresolvable-but-present
|
|
25
|
-
* shape raises `DeliverableSpecIntrospectionError` instead.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import {
|
|
29
|
-
DeliverableSpecIntrospectionError,
|
|
30
|
-
DeliverableSpecIntrospectionFailure,
|
|
31
|
-
} from './deliverable-spec-introspection-error';
|
|
32
|
-
import { scanTopLevelZodObjectDeclarations } from './deliverable-spec-source-scan';
|
|
33
|
-
|
|
34
|
-
import type { TopLevelZodObjectDeclaration } from './deliverable-spec-source-scan';
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Return the top-level field identifiers of the `z.object({ … })` literal
|
|
38
|
-
* that IS this spec's contract.
|
|
39
|
-
*
|
|
40
|
-
* Which literal that is comes from the spec, not from source order:
|
|
41
|
-
*
|
|
42
|
-
* - `exportName` given → the literal bound to that name. Absent from
|
|
43
|
-
* the source ⇒ `EXPORT_NOT_FOUND`, never a fallback to another
|
|
44
|
-
* schema. A spec that names a contract it does not have is broken,
|
|
45
|
-
* and guessing would reintroduce exactly the bug this parameter
|
|
46
|
-
* exists to remove.
|
|
47
|
-
* - `exportName` omitted, source declares exactly ONE top-level
|
|
48
|
-
* literal → that one. There is nothing to choose between, so the
|
|
49
|
-
* spec is not asked to restate it.
|
|
50
|
-
* - `exportName` omitted, source declares TWO OR MORE →
|
|
51
|
-
* `AMBIGUOUS_SCHEMA`. This is the case the predecessor resolved by
|
|
52
|
-
* taking the first literal in the file, which silently picked an
|
|
53
|
-
* element/helper schema hoisted above the contract that references
|
|
54
|
-
* it. There is no correct positional answer; the spec must say.
|
|
55
|
-
* - source declares NONE → `null` (no introspectable shape).
|
|
56
|
-
*
|
|
57
|
-
* Deliberately a scanner, not a Zod evaluation: this runs in the DSL
|
|
58
|
-
* compiler, a CI gate, and a request-scoped API handler, none of which
|
|
59
|
-
* may transpile or execute spec-authored TypeScript.
|
|
60
|
-
*
|
|
61
|
-
* @throws {DeliverableSpecIntrospectionError} on a named export that is
|
|
62
|
-
* absent, or on an unnamed contract in a multi-schema source.
|
|
63
|
-
*/
|
|
64
|
-
export function extractZodTopLevelObjectKeys(
|
|
65
|
-
source: string,
|
|
66
|
-
exportName?: string | null,
|
|
67
|
-
): readonly string[] | null {
|
|
68
|
-
if (!source || source.trim().length === 0) return null;
|
|
69
|
-
const declarations = scanTopLevelZodObjectDeclarations(source);
|
|
70
|
-
const named = exportName?.trim() ?? '';
|
|
71
|
-
|
|
72
|
-
if (named.length > 0) {
|
|
73
|
-
const match = declarations.find((d) => d.exportName === named);
|
|
74
|
-
if (!match) {
|
|
75
|
-
throw new DeliverableSpecIntrospectionError(
|
|
76
|
-
DeliverableSpecIntrospectionFailure.ExportNotFound,
|
|
77
|
-
`deliverable spec names '${named}' as its Zod contract, but the source declares no top-level \`z.object({ … })\` bound to that name. ` +
|
|
78
|
-
`${describeDeclarations(declarations)} Fix the spec's \`zodSchemaExport\`, or check the literal's braces are balanced.`,
|
|
79
|
-
{ exportName: named, available: availableNames(declarations) },
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
return match.keys;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (declarations.length === 0) return null;
|
|
86
|
-
if (declarations.length === 1) return declarations[0]!.keys;
|
|
87
|
-
|
|
88
|
-
throw new DeliverableSpecIntrospectionError(
|
|
89
|
-
DeliverableSpecIntrospectionFailure.AmbiguousSchema,
|
|
90
|
-
`deliverable spec source declares ${declarations.length} top-level \`z.object({ … })\` schemas and the spec does not say which one is the contract. ` +
|
|
91
|
-
`${describeDeclarations(declarations)} Set \`zodSchemaExport\` on the spec to the contract's export name.`,
|
|
92
|
-
{ available: availableNames(declarations) },
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Pull the top-level field names from a JSON Schema document. Accepts
|
|
98
|
-
* either a parsed object or a raw JSON string. Honors `properties` —
|
|
99
|
-
* the conventional shape — and ignores `patternProperties` /
|
|
100
|
-
* `additionalProperties` / Draft 2020 `unevaluatedProperties` because
|
|
101
|
-
* those don't enumerate concrete keys an author can reference by name.
|
|
102
|
-
*
|
|
103
|
-
* Returns `null` when the schema is unparseable or has no `properties`
|
|
104
|
-
* map.
|
|
105
|
-
*/
|
|
106
|
-
export function extractJsonSchemaTopLevelKeys(
|
|
107
|
-
source: string | object,
|
|
108
|
-
): readonly string[] | null {
|
|
109
|
-
let parsed: unknown;
|
|
110
|
-
if (typeof source === 'string') {
|
|
111
|
-
if (source.trim().length === 0) return null;
|
|
112
|
-
try {
|
|
113
|
-
parsed = JSON.parse(source);
|
|
114
|
-
} catch {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
} else {
|
|
118
|
-
parsed = source;
|
|
119
|
-
}
|
|
120
|
-
if (!isPlainObject(parsed)) return null;
|
|
121
|
-
const properties = (parsed as Record<string, unknown>)['properties'];
|
|
122
|
-
if (!isPlainObject(properties)) return null;
|
|
123
|
-
const keys = Object.keys(properties);
|
|
124
|
-
return Object.freeze(keys);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
128
|
-
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function availableNames(
|
|
132
|
-
declarations: readonly TopLevelZodObjectDeclaration[],
|
|
133
|
-
): readonly string[] {
|
|
134
|
-
return declarations
|
|
135
|
-
.map((d) => d.exportName)
|
|
136
|
-
.filter((n): n is string => n !== null);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function describeDeclarations(
|
|
140
|
-
declarations: readonly TopLevelZodObjectDeclaration[],
|
|
141
|
-
): string {
|
|
142
|
-
const names = availableNames(declarations);
|
|
143
|
-
if (names.length === 0) {
|
|
144
|
-
return 'The source declares no NAMED top-level schema.';
|
|
145
|
-
}
|
|
146
|
-
return `Top-level schemas in this source: ${names.join(', ')}.`;
|
|
147
|
-
}
|