@atom-workflow-agent/workflow-compiler 0.1.0

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.
Files changed (36) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +30 -0
  3. package/README.zh-CN.md +30 -0
  4. package/dist/compiler/compile-workflow.d.ts +19 -0
  5. package/dist/compiler/compile-workflow.d.ts.map +1 -0
  6. package/dist/compiler/compile-workflow.js +865 -0
  7. package/dist/compiler/compile-workflow.js.map +1 -0
  8. package/dist/errors/workflow-compilation-error.d.ts +8 -0
  9. package/dist/errors/workflow-compilation-error.d.ts.map +1 -0
  10. package/dist/errors/workflow-compilation-error.js +15 -0
  11. package/dist/errors/workflow-compilation-error.js.map +1 -0
  12. package/dist/index.d.ts +4 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +4 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/normalization/canonical-json.d.ts +5 -0
  17. package/dist/normalization/canonical-json.d.ts.map +1 -0
  18. package/dist/normalization/canonical-json.js +27 -0
  19. package/dist/normalization/canonical-json.js.map +1 -0
  20. package/dist/validation/control-graph.d.ts +19 -0
  21. package/dist/validation/control-graph.d.ts.map +1 -0
  22. package/dist/validation/control-graph.js +215 -0
  23. package/dist/validation/control-graph.js.map +1 -0
  24. package/dist/validation/graph.d.ts +7 -0
  25. package/dist/validation/graph.d.ts.map +1 -0
  26. package/dist/validation/graph.js +92 -0
  27. package/dist/validation/graph.js.map +1 -0
  28. package/dist/validation/json-schema.d.ts +27 -0
  29. package/dist/validation/json-schema.d.ts.map +1 -0
  30. package/dist/validation/json-schema.js +345 -0
  31. package/dist/validation/json-schema.js.map +1 -0
  32. package/dist/validation/references.d.ts +32 -0
  33. package/dist/validation/references.d.ts.map +1 -0
  34. package/dist/validation/references.js +40 -0
  35. package/dist/validation/references.js.map +1 -0
  36. package/package.json +50 -0
@@ -0,0 +1,865 @@
1
+ import { AtomManifestSchema, classifyReferenceLikeString, RuntimePolicySchema, WorkflowReferenceSchema, WorkflowSpecSchema, atomKey, } from "@atom-workflow-agent/contracts";
2
+ import { WorkflowCompilationError, sortValidationErrors } from "../errors/workflow-compilation-error.js";
3
+ import { canonicalize, contentHash, freezeDeep } from "../normalization/canonical-json.js";
4
+ import { validateControlGraph } from "../validation/control-graph.js";
5
+ import { validateGraph } from "../validation/graph.js";
6
+ import { childSchemaForProperty, inferSchema, itemSchema, requiredProperties, requiredPropertiesForValue, resolveSchemaPath, schemaPathGuaranteed, schemaAcceptsLiteral, schemasCompatible, schemaTypes, } from "../validation/json-schema.js";
7
+ import { isReferenceCandidate, parseWorkflowReference, valueAtPath, } from "../validation/references.js";
8
+ function zodPath(path, prefix = "$.") {
9
+ let result = prefix.endsWith(".") ? prefix.slice(0, -1) : prefix;
10
+ for (const segment of path) {
11
+ if (typeof segment === "number")
12
+ result += `[${segment}]`;
13
+ else if (typeof segment === "string" && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment))
14
+ result += `.${segment}`;
15
+ else
16
+ result += `[${JSON.stringify(String(segment))}]`;
17
+ }
18
+ return result;
19
+ }
20
+ function schemaErrors(raw) {
21
+ let candidate = raw;
22
+ if (typeof raw === "string") {
23
+ try {
24
+ candidate = JSON.parse(raw);
25
+ }
26
+ catch (error) {
27
+ return { errors: [{ code: "WORKFLOW_JSON_INVALID", path: "$", message: error instanceof Error ? error.message : "Workflow JSON could not be parsed" }] };
28
+ }
29
+ }
30
+ const result = WorkflowSpecSchema.safeParse(candidate);
31
+ if (result.success)
32
+ return { spec: result.data, errors: [] };
33
+ return {
34
+ errors: result.error.issues.map((issue) => ({
35
+ code: "WORKFLOW_SCHEMA_INVALID",
36
+ path: zodPath(issue.path),
37
+ message: issue.message,
38
+ })),
39
+ };
40
+ }
41
+ function parsePolicy(input) {
42
+ const result = RuntimePolicySchema.safeParse(input ?? {});
43
+ if (result.success)
44
+ return result.data;
45
+ return result.error.issues.map((issue) => ({
46
+ code: "RUNTIME_POLICY_INVALID",
47
+ path: zodPath(issue.path, "$.runtimePolicy."),
48
+ message: issue.message,
49
+ }));
50
+ }
51
+ function buildCatalog(entries, errors) {
52
+ const catalog = new Map();
53
+ entries.forEach((entry, index) => {
54
+ const parsed = AtomManifestSchema.safeParse(entry.manifest);
55
+ if (!parsed.success) {
56
+ errors.push({ code: "ATOM_CATALOG_INVALID", path: `$.atomCatalog[${index}].manifest`, message: "Atom catalog contains an invalid Manifest" });
57
+ return;
58
+ }
59
+ const key = atomKey(parsed.data);
60
+ if (catalog.has(key)) {
61
+ errors.push({ code: "ATOM_CATALOG_DUPLICATE", path: `$.atomCatalog[${index}]`, message: `Atom catalog contains a duplicate exact version: ${key}`, atomId: parsed.data.id });
62
+ return;
63
+ }
64
+ catalog.set(key, { manifest: parsed.data, enabled: entry.enabled });
65
+ });
66
+ return catalog;
67
+ }
68
+ function splitAtomReference(reference) {
69
+ const separator = reference.lastIndexOf("@");
70
+ return { id: reference.slice(0, separator), version: reference.slice(separator + 1) };
71
+ }
72
+ function mergePolicy(manifest, node, policy) {
73
+ const retryOverride = node.policy?.retry;
74
+ const retry = {
75
+ maxAttempts: retryOverride?.maxAttempts ?? manifest.defaults.retry.maxAttempts,
76
+ strategy: retryOverride?.strategy ?? manifest.defaults.retry.strategy,
77
+ baseDelayMs: retryOverride?.baseDelayMs ?? manifest.defaults.retry.baseDelayMs,
78
+ maxDelayMs: retryOverride?.maxDelayMs ?? manifest.defaults.retry.maxDelayMs,
79
+ jitter: retryOverride?.jitter ?? manifest.defaults.retry.jitter,
80
+ };
81
+ const cacheOverride = node.policy?.cache;
82
+ const rawCache = {
83
+ enabled: cacheOverride?.enabled ?? manifest.defaults.cache.enabled,
84
+ ...(cacheOverride?.ttlMs !== undefined
85
+ ? { ttlMs: cacheOverride.ttlMs }
86
+ : manifest.defaults.cache.ttlMs !== undefined ? { ttlMs: manifest.defaults.cache.ttlMs } : {}),
87
+ };
88
+ const attemptTimeoutMs = node.policy?.attemptTimeoutMs ?? manifest.defaults.timeoutMs ?? null;
89
+ const lifecycleTickTimeoutMs = node.policy?.lifecycleTickTimeoutMs
90
+ ?? node.policy?.attemptTimeoutMs
91
+ ?? manifest.defaults.timeoutMs
92
+ ?? null;
93
+ return {
94
+ attemptTimeoutMs,
95
+ lifecycleTickTimeoutMs,
96
+ maxLifecycleTicks: node.policy?.maxLifecycleTicks ?? policy.maxLifecycleTicks,
97
+ retry,
98
+ cache: rawCache.enabled ? rawCache : { enabled: false },
99
+ };
100
+ }
101
+ function validateNodePolicies(spec, catalog, policy, errors) {
102
+ const resolved = new Map();
103
+ if (spec.nodes.length > policy.maxNodes) {
104
+ errors.push({ code: "NODE_LIMIT_EXCEEDED", path: "$.nodes", message: `Workflow has ${spec.nodes.length} nodes but Runtime Policy allows ${policy.maxNodes}`, expected: policy.maxNodes, received: spec.nodes.length });
105
+ }
106
+ spec.nodes.forEach((node, index) => {
107
+ const path = `$.nodes[${index}]`;
108
+ if (node.dependsOn.length > policy.maxDependenciesPerNode) {
109
+ errors.push({ code: "DEPENDENCY_LIMIT_EXCEEDED", path: `${path}.dependsOn`, message: "Node dependency count exceeds Runtime Policy", expected: policy.maxDependenciesPerNode, received: node.dependsOn.length });
110
+ }
111
+ if (node.kind === "merge")
112
+ return;
113
+ const entry = catalog.get(node.atom);
114
+ const atomReference = splitAtomReference(node.atom);
115
+ if (!entry) {
116
+ errors.push({ code: "ATOM_NOT_FOUND", path: `${path}.atom`, message: `No registered Atom matches exact reference: ${node.atom}`, atomId: atomReference.id, received: node.atom });
117
+ return;
118
+ }
119
+ if (!entry.enabled) {
120
+ errors.push({ code: "ATOM_UNAVAILABLE", path: `${path}.atom`, message: `Atom is registered but not enabled: ${node.atom}`, atomId: atomReference.id, received: node.atom });
121
+ return;
122
+ }
123
+ const manifest = entry.manifest;
124
+ if (manifest.status === "deprecated" && !policy.allowDeprecatedAtoms) {
125
+ errors.push({ code: "DEPRECATED_ATOM_FORBIDDEN", path: `${path}.atom`, message: `Deprecated Atom is forbidden by Runtime Policy: ${node.atom}`, atomId: manifest.id });
126
+ }
127
+ const effectivePolicy = mergePolicy(manifest, node, policy);
128
+ if (policy.maxAttemptDurationMs !== null
129
+ && (effectivePolicy.attemptTimeoutMs === null || effectivePolicy.attemptTimeoutMs > policy.maxAttemptDurationMs)) {
130
+ errors.push({ code: "ATTEMPT_TIMEOUT_LIMIT_EXCEEDED", path: `${path}.policy.attemptTimeoutMs`, message: "Attempt timeout exceeds Runtime Policy", expected: policy.maxAttemptDurationMs, received: effectivePolicy.attemptTimeoutMs });
131
+ }
132
+ const tickExceedsRuntime = policy.maxLifecycleTickTimeoutMs !== null
133
+ && (effectivePolicy.lifecycleTickTimeoutMs === null || effectivePolicy.lifecycleTickTimeoutMs > policy.maxLifecycleTickTimeoutMs);
134
+ const tickExceedsAttempt = effectivePolicy.attemptTimeoutMs !== null
135
+ && (effectivePolicy.lifecycleTickTimeoutMs === null || effectivePolicy.lifecycleTickTimeoutMs > effectivePolicy.attemptTimeoutMs);
136
+ if (tickExceedsRuntime || tickExceedsAttempt) {
137
+ const limits = [policy.maxLifecycleTickTimeoutMs, effectivePolicy.attemptTimeoutMs]
138
+ .filter((value) => value !== null);
139
+ errors.push({ code: "LIFECYCLE_TICK_TIMEOUT_LIMIT_EXCEEDED", path: `${path}.policy.lifecycleTickTimeoutMs`, message: "Lifecycle tick timeout exceeds its Runtime or Attempt budget", expected: limits.length > 0 ? Math.min(...limits) : null, received: effectivePolicy.lifecycleTickTimeoutMs });
140
+ }
141
+ if (effectivePolicy.maxLifecycleTicks > policy.maxLifecycleTicks) {
142
+ errors.push({ code: "LIFECYCLE_TICK_LIMIT_EXCEEDED", path: `${path}.policy.maxLifecycleTicks`, message: "Lifecycle tick count exceeds Runtime Policy", expected: policy.maxLifecycleTicks, received: effectivePolicy.maxLifecycleTicks });
143
+ }
144
+ if (effectivePolicy.retry.maxAttempts > policy.maxRetryAttempts) {
145
+ errors.push({ code: "RETRY_LIMIT_EXCEEDED", path: `${path}.policy.retry.maxAttempts`, message: "Effective retry attempts exceed Runtime Policy", expected: policy.maxRetryAttempts, received: effectivePolicy.retry.maxAttempts });
146
+ }
147
+ if (effectivePolicy.retry.baseDelayMs > policy.maxRetryDelayMs || effectivePolicy.retry.maxDelayMs > policy.maxRetryDelayMs) {
148
+ errors.push({ code: "RETRY_DELAY_LIMIT_EXCEEDED", path: `${path}.policy.retry`, message: "Effective retry delay exceeds Runtime Policy", expected: policy.maxRetryDelayMs, received: effectivePolicy.retry.maxDelayMs });
149
+ }
150
+ if (effectivePolicy.retry.maxDelayMs < effectivePolicy.retry.baseDelayMs) {
151
+ errors.push({ code: "RETRY_POLICY_INVALID", path: `${path}.policy.retry.maxDelayMs`, message: "Effective maxDelayMs must be greater than or equal to baseDelayMs", expected: effectivePolicy.retry.baseDelayMs, received: effectivePolicy.retry.maxDelayMs });
152
+ }
153
+ if (!manifest.idempotent && effectivePolicy.retry.maxAttempts > 1) {
154
+ errors.push({ code: "NON_IDEMPOTENT_RETRY_FORBIDDEN", path: `${path}.policy.retry.maxAttempts`, message: "A non-idempotent Atom cannot be automatically retried", atomId: manifest.id, expected: 1, received: effectivePolicy.retry.maxAttempts });
155
+ }
156
+ if (effectivePolicy.cache.enabled && effectivePolicy.cache.ttlMs !== undefined && effectivePolicy.cache.ttlMs > policy.maxCacheTtlMs) {
157
+ errors.push({ code: "CACHE_TTL_LIMIT_EXCEEDED", path: `${path}.policy.cache.ttlMs`, message: "Effective cache TTL exceeds Runtime Policy", expected: policy.maxCacheTtlMs, received: effectivePolicy.cache.ttlMs });
158
+ }
159
+ if (node.policy?.cache?.enabled === true && !manifest.defaults.cache.enabled && !policy.allowCacheEnableOverride) {
160
+ errors.push({ code: "CACHE_ENABLE_OVERRIDE_FORBIDDEN", path: `${path}.policy.cache.enabled`, message: "Runtime Policy does not allow enabling cache for this Atom", atomId: manifest.id });
161
+ }
162
+ const mapMaxItems = node.map?.maxItems;
163
+ if (mapMaxItems !== undefined && mapMaxItems > policy.maxMapItems) {
164
+ errors.push({ code: "MAP_LIMIT_EXCEEDED", path: `${path}.map.maxItems`, message: "Map maxItems exceeds Runtime Policy", expected: policy.maxMapItems, received: mapMaxItems });
165
+ }
166
+ if (node.map && node.map.maxConcurrency > policy.maxNodeActivations) {
167
+ errors.push({ code: "MAP_LIMIT_EXCEEDED", path: `${path}.map.maxConcurrency`, message: "Map maxConcurrency exceeds the Runtime node activation policy", expected: policy.maxNodeActivations, received: node.map.maxConcurrency });
168
+ }
169
+ if (!resolved.has(node.id))
170
+ resolved.set(node.id, { manifest, effectivePolicy });
171
+ });
172
+ return resolved;
173
+ }
174
+ function nodeOutputSchema(spec, resolvedByNode, nodeId) {
175
+ const node = spec.nodes.find((candidate) => candidate.id === nodeId);
176
+ if (!node)
177
+ return undefined;
178
+ return node.kind === "merge" ? node.outputSchema : resolvedByNode.get(nodeId)?.manifest.outputSchema;
179
+ }
180
+ function referenceErrorCode(context, fallback) {
181
+ if (context.scope === "branch-merge-input") {
182
+ return fallback.startsWith("REFERENCE") ? "BRANCH_MERGE_SOURCE_INVALID" : "BRANCH_MERGE_INPUT_MISSING";
183
+ }
184
+ return context.scope === "edge-input" ? "EDGE_INPUT_SCHEMA_MISMATCH" : fallback;
185
+ }
186
+ function resolveReference(reference, path, context) {
187
+ const parsed = parseWorkflowReference(reference);
188
+ if (parsed.kind === "workflow-input") {
189
+ const result = valueAtPath(context.spec.inputs, parsed.path);
190
+ if (!result.found) {
191
+ context.errors.push({ code: "REFERENCE_NOT_FOUND", path, message: `Workflow input reference does not exist: ${reference.$ref}`, received: reference.$ref });
192
+ return undefined;
193
+ }
194
+ return { kind: "literal", value: result.value };
195
+ }
196
+ if (parsed.kind === "map-index" || parsed.kind === "map-item") {
197
+ if (context.node.kind === "merge" || !context.node.map || !context.mapItem) {
198
+ context.errors.push({ code: "MAP_REFERENCE_OUTSIDE_MAP", path, message: `${reference.$ref} can only be used by a Map processor node`, received: reference.$ref });
199
+ return undefined;
200
+ }
201
+ if (parsed.kind === "map-index")
202
+ return { kind: "schema", schema: { type: "integer", minimum: 0 }, root: { type: "integer" }, guaranteed: true };
203
+ if (context.mapItem.kind === "literal") {
204
+ const result = valueAtPath(context.mapItem.value, parsed.path);
205
+ if (!result.found) {
206
+ context.errors.push({ code: "REFERENCE_NOT_FOUND", path, message: `Map item reference cannot be resolved: ${reference.$ref}` });
207
+ return undefined;
208
+ }
209
+ return { kind: "literal", value: result.value };
210
+ }
211
+ const nested = resolveSchemaPath(context.mapItem.schema, parsed.path, context.mapItem.root);
212
+ if (nested === undefined) {
213
+ context.errors.push({ code: "REFERENCE_NOT_FOUND", path, message: `Map item Schema path does not exist: ${reference.$ref}` });
214
+ return undefined;
215
+ }
216
+ const guaranteed = context.mapItem.guaranteed && schemaPathGuaranteed(context.mapItem.schema, parsed.path, context.mapItem.root);
217
+ if (!guaranteed)
218
+ context.errors.push({
219
+ code: "REFERENCE_SOURCE_OPTIONAL", path,
220
+ message: `Reference source path is optional and may be absent at runtime: ${reference.$ref}`,
221
+ received: reference.$ref,
222
+ hint: "Omit this binding, provide a guaranteed Workflow input, or use explicit control flow that produces a required branch result",
223
+ });
224
+ return { kind: "schema", schema: nested, root: context.mapItem.root, guaranteed };
225
+ }
226
+ if (parsed.kind === "edge-source-output") {
227
+ if ((context.scope !== "edge-input" && context.scope !== "branch-merge-input" && context.scope !== "decision-select") || !context.edgeSourceNodeId) {
228
+ context.errors.push({ code: "EDGE_REFERENCE_OUTSIDE_EDGE", path, message: "node.output is only valid inside an Edge with or DecisionNode select", received: reference.$ref });
229
+ return undefined;
230
+ }
231
+ const sourceSchema = nodeOutputSchema(context.spec, context.resolvedByNode, context.edgeSourceNodeId);
232
+ const nested = sourceSchema && resolveSchemaPath(sourceSchema, parsed.path, sourceSchema);
233
+ if (nested === undefined) {
234
+ context.errors.push({ code: "REFERENCE_NOT_FOUND", path, message: `Edge source output Schema path does not exist: ${reference.$ref}` });
235
+ return undefined;
236
+ }
237
+ const guaranteed = schemaPathGuaranteed(sourceSchema, parsed.path, sourceSchema);
238
+ if (!guaranteed)
239
+ context.errors.push({
240
+ code: "REFERENCE_SOURCE_OPTIONAL", path,
241
+ message: `Reference source path is optional and may be absent at runtime: ${reference.$ref}`,
242
+ received: reference.$ref,
243
+ hint: "Omit this binding or route a required value through the selected control edge",
244
+ });
245
+ return { kind: "schema", schema: nested, root: sourceSchema, guaranteed };
246
+ }
247
+ if (parsed.kind === "activation-output") {
248
+ if (context.scope !== "workflow-output") {
249
+ context.errors.push({ code: "WORKFLOW_OUTPUT_REFERENCE_INVALID", path, message: "Activation selectors are only valid in Workflow outputs", received: reference.$ref });
250
+ return undefined;
251
+ }
252
+ const sourceSchema = nodeOutputSchema(context.spec, context.resolvedByNode, parsed.nodeId);
253
+ const nested = sourceSchema && resolveSchemaPath(sourceSchema, parsed.path, sourceSchema);
254
+ if (nested === undefined) {
255
+ context.errors.push({ code: "REFERENCE_NOT_FOUND", path, message: `Activation output Schema path does not exist: ${reference.$ref}`, received: reference.$ref });
256
+ return undefined;
257
+ }
258
+ const sourceNode = context.spec.nodes.find((node) => node.id === parsed.nodeId);
259
+ if (sourceNode?.kind !== "merge" && sourceNode?.map) {
260
+ const schema = { type: "array", items: nested };
261
+ return { kind: "schema", schema, root: schema, guaranteed: true };
262
+ }
263
+ const guaranteed = schemaPathGuaranteed(sourceSchema, parsed.path, sourceSchema);
264
+ if (!guaranteed)
265
+ context.errors.push({
266
+ code: "REFERENCE_SOURCE_OPTIONAL", path,
267
+ message: `Workflow output references an optional source path: ${reference.$ref}`,
268
+ received: reference.$ref,
269
+ hint: "Publish a guaranteed output or merge branch-local values into a required result field",
270
+ });
271
+ return { kind: "schema", schema: nested, root: sourceSchema, guaranteed };
272
+ }
273
+ const outputSchema = nodeOutputSchema(context.spec, context.resolvedByNode, parsed.nodeId);
274
+ if (!outputSchema) {
275
+ context.errors.push({ code: "REFERENCE_NOT_FOUND", path, message: `Dependency output does not exist: ${reference.$ref}`, received: reference.$ref });
276
+ return undefined;
277
+ }
278
+ // Edge overlays are evaluated while routing the source activation. Their
279
+ // dependency references therefore belong to the edge source's lineage, not
280
+ // to the target node that will receive the resolved overlay.
281
+ const targetCanRead = parsed.nodeId === context.node.id || context.graph.ancestors.get(context.node.id)?.has(parsed.nodeId);
282
+ const edgeSourceCanRead = (context.scope === "edge-input" || context.scope === "branch-merge-input") && context.edgeSourceNodeId !== undefined
283
+ && (parsed.nodeId === context.edgeSourceNodeId || context.graph.ancestors.get(context.edgeSourceNodeId)?.has(parsed.nodeId));
284
+ if (!targetCanRead && !edgeSourceCanRead) {
285
+ context.errors.push({ code: "REFERENCE_NOT_DEPENDENCY", path, message: `Output must come from an explicit dependency activation: ${reference.$ref}`, received: parsed.nodeId });
286
+ }
287
+ const nested = resolveSchemaPath(outputSchema, parsed.path, outputSchema);
288
+ if (nested === undefined) {
289
+ context.errors.push({ code: "REFERENCE_NOT_FOUND", path, message: `Dependency output Schema path does not exist: ${reference.$ref}`, received: reference.$ref });
290
+ return undefined;
291
+ }
292
+ const sourceNode = context.spec.nodes.find((node) => node.id === parsed.nodeId);
293
+ const sourceGroup = sourceNode?.kind !== "merge" ? sourceNode?.map?.group ?? (sourceNode?.map ? sourceNode.id : undefined) : undefined;
294
+ const targetGroup = context.node.kind !== "merge" ? context.node.map?.group ?? (context.node.map ? context.node.id : undefined) : undefined;
295
+ if (sourceGroup && sourceGroup !== targetGroup) {
296
+ const schema = { type: "array", items: nested };
297
+ return { kind: "schema", schema, root: schema, guaranteed: true };
298
+ }
299
+ const guaranteed = schemaPathGuaranteed(outputSchema, parsed.path, outputSchema);
300
+ if (!guaranteed)
301
+ context.errors.push({
302
+ code: "REFERENCE_SOURCE_OPTIONAL", path,
303
+ message: `Reference source path is optional and may be absent at runtime: ${reference.$ref}`,
304
+ received: reference.$ref,
305
+ hint: "Omit this binding, provide a guaranteed Workflow input, or use explicit control flow that produces a required branch result",
306
+ });
307
+ return { kind: "schema", schema: nested, root: outputSchema, guaranteed };
308
+ }
309
+ function sourceCompatible(source, target, targetRoot) {
310
+ return source.kind === "literal"
311
+ ? schemaAcceptsLiteral(target, source.value, targetRoot)
312
+ : schemasCompatible(source.schema, target, source.root, targetRoot);
313
+ }
314
+ function validateBoundValue(value, target, targetRoot, path, context, checkRequired = true) {
315
+ if (typeof value === "string") {
316
+ const referenceKind = classifyReferenceLikeString(value);
317
+ if (referenceKind) {
318
+ context.errors.push({
319
+ code: referenceErrorCode(context, "REFERENCE_INVALID"),
320
+ path,
321
+ message: `Reference-like string cannot be used as a literal Workflow value: ${value}`,
322
+ received: value,
323
+ hint: referenceKind === "planner-draft"
324
+ ? "PlannerDraft shorthand is not valid in WorkflowSpec; use a structured {\"$ref\":\"...\"} Workflow reference"
325
+ : `Wrap the reference as {\"$ref\":${JSON.stringify(value)}}`,
326
+ });
327
+ return;
328
+ }
329
+ }
330
+ if (isReferenceCandidate(value)) {
331
+ const parsed = WorkflowReferenceSchema.safeParse(value);
332
+ if (!parsed.success) {
333
+ context.errors.push({ code: "REFERENCE_INVALID", path, message: "A binding must contain only a valid $ref", received: value });
334
+ return;
335
+ }
336
+ const source = resolveReference(parsed.data, path, context);
337
+ if (source && !sourceCompatible(source, target, targetRoot)) {
338
+ context.errors.push({ code: referenceErrorCode(context, "REFERENCE_TYPE_MISMATCH"), path, message: `Reference is incompatible with the target input Schema: ${parsed.data.$ref}`, received: parsed.data.$ref });
339
+ }
340
+ return;
341
+ }
342
+ if (value !== null && typeof value === "object" && !Array.isArray(value)
343
+ && Object.keys(value).some((key) => key.startsWith("$"))) {
344
+ context.errors.push({
345
+ code: referenceErrorCode(context, "REFERENCE_INVALID"),
346
+ path,
347
+ message: "Unsupported reference directive object inside WorkflowSpec",
348
+ received: value,
349
+ hint: "Use one structured {\"$ref\":\"...\"} Workflow reference; pass reserved-key business data through a Workflow input instead of an inline literal",
350
+ });
351
+ return;
352
+ }
353
+ if (Array.isArray(value)) {
354
+ if (!schemaTypes(target, targetRoot).has("array")) {
355
+ context.errors.push({ code: referenceErrorCode(context, "INPUT_SCHEMA_MISMATCH"), path, message: "Expected an array-compatible input", received: value });
356
+ return;
357
+ }
358
+ const nestedTarget = itemSchema(target, targetRoot) ?? true;
359
+ value.forEach((item, index) => validateBoundValue(item, nestedTarget, targetRoot, `${path}[${index}]`, context));
360
+ return;
361
+ }
362
+ if (value !== null && typeof value === "object") {
363
+ if (!schemaTypes(target, targetRoot).has("object")) {
364
+ context.errors.push({ code: referenceErrorCode(context, "INPUT_SCHEMA_MISMATCH"), path, message: "Expected an object-compatible input", received: value });
365
+ return;
366
+ }
367
+ if (checkRequired) {
368
+ for (const required of requiredPropertiesForValue(target, targetRoot, value)) {
369
+ if (!(required in value)) {
370
+ context.errors.push({ code: referenceErrorCode(context, "INPUT_REQUIRED_PROPERTY_MISSING"), path: `${path}.${required}`, message: `Required input property is missing: ${required}`, expected: required });
371
+ }
372
+ }
373
+ }
374
+ for (const [key, nested] of Object.entries(value)) {
375
+ const nestedTarget = childSchemaForProperty(target, key, targetRoot);
376
+ if (nestedTarget === undefined) {
377
+ context.errors.push({ code: referenceErrorCode(context, "INPUT_ADDITIONAL_PROPERTY_FORBIDDEN"), path: `${path}.${key}`, message: `Atom input Schema does not allow property: ${key}`, received: key });
378
+ }
379
+ else {
380
+ validateBoundValue(nested, nestedTarget, targetRoot, `${path}.${key}`, context, true);
381
+ }
382
+ }
383
+ return;
384
+ }
385
+ if (!schemaAcceptsLiteral(target, value, targetRoot)) {
386
+ context.errors.push({ code: referenceErrorCode(context, "INPUT_SCHEMA_MISMATCH"), path, message: "Literal value is incompatible with Atom input Schema", received: value });
387
+ }
388
+ }
389
+ function sourceMustBeArray(source, path, errors) {
390
+ if (source.kind === "literal") {
391
+ if (!Array.isArray(source.value)) {
392
+ errors.push({ code: "MAP_ITEMS_NOT_ARRAY", path, message: "Map items must resolve to an array", received: source.value });
393
+ return undefined;
394
+ }
395
+ const inferred = inferSchema(source.value);
396
+ return { kind: "schema", schema: inferred, root: inferred, guaranteed: true };
397
+ }
398
+ if (!schemaTypes(source.schema, source.root).has("array")) {
399
+ errors.push({ code: "MAP_ITEMS_NOT_ARRAY", path, message: "Map items Schema must be an array" });
400
+ return undefined;
401
+ }
402
+ return { kind: "schema", schema: itemSchema(source.schema, source.root) ?? true, root: source.root, guaranteed: source.guaranteed };
403
+ }
404
+ function mergeEdgeInput(base, overlay) {
405
+ if (base !== null && typeof base === "object" && !Array.isArray(base))
406
+ return { ...base, ...overlay };
407
+ return Object.keys(overlay).length === 0 ? base : { ...overlay };
408
+ }
409
+ function validateReferencesAndInputs(spec, graph, control, resolvedByNode, errors) {
410
+ const nodeById = new Map(spec.nodes.map((node) => [node.id, node]));
411
+ const indexById = new Map(spec.nodes.map((node, index) => [node.id, index]));
412
+ const entry = new Set(spec.entry);
413
+ const mapItems = new Map();
414
+ for (const node of spec.nodes) {
415
+ const nodeIndex = indexById.get(node.id);
416
+ if (node.kind === "merge") {
417
+ if (entry.has(node.id)) {
418
+ const required = requiredProperties(node.outputSchema, node.outputSchema);
419
+ if (required.length > 0)
420
+ errors.push({
421
+ code: "BRANCH_MERGE_INPUT_MISSING", path: `$.nodes[${nodeIndex}]`,
422
+ message: `Merge node ${node.id} cannot be an entry because its typed result requires incoming branch values`,
423
+ expected: [...required],
424
+ });
425
+ }
426
+ continue;
427
+ }
428
+ const resolved = resolvedByNode.get(node.id);
429
+ if (!resolved)
430
+ continue;
431
+ const baseContext = { node, nodeIndex, graph, resolvedByNode, spec, errors, scope: "node-input" };
432
+ let mapItem;
433
+ if (node.map) {
434
+ const source = resolveReference(node.map.items, `$.nodes[${nodeIndex}].map.items`, baseContext);
435
+ if (source)
436
+ mapItem = sourceMustBeArray(source, `$.nodes[${nodeIndex}].map.items`, errors);
437
+ }
438
+ if (mapItem)
439
+ mapItems.set(node.id, mapItem);
440
+ const context = mapItem ? { ...baseContext, mapItem } : baseContext;
441
+ validateBoundValue(node.input, resolved.manifest.inputSchema, resolved.manifest.inputSchema, `$.nodes[${nodeIndex}].input`, context, false);
442
+ if (entry.has(node.id)) {
443
+ validateBoundValue(node.input, resolved.manifest.inputSchema, resolved.manifest.inputSchema, `$.nodes[${nodeIndex}].input`, context, true);
444
+ }
445
+ if (node.kind === "decision") {
446
+ const selectContext = { ...context, scope: "decision-select", edgeSourceNodeId: node.id };
447
+ const source = resolveReference(node.select, `$.nodes[${nodeIndex}].select`, selectContext);
448
+ const stringSchema = { type: "string" };
449
+ if (source && !sourceCompatible(source, stringSchema, stringSchema)) {
450
+ errors.push({ code: "DECISION_SELECT_TYPE_MISMATCH", path: `$.nodes[${nodeIndex}].select`, message: "DecisionNode select must resolve to a string", received: node.select.$ref });
451
+ }
452
+ if (source?.kind === "schema" && source.schema !== true && source.schema !== false) {
453
+ const schemaObject = source.schema;
454
+ const enumValues = Array.isArray(schemaObject.enum) && schemaObject.enum.every((value) => typeof value === "string")
455
+ ? [...schemaObject.enum]
456
+ : undefined;
457
+ if (enumValues) {
458
+ const declared = Object.keys(node.branches);
459
+ const missing = enumValues.filter((value) => !declared.includes(value));
460
+ const extra = declared.filter((value) => !enumValues.includes(value));
461
+ if (missing.length > 0 || extra.length > 0) {
462
+ errors.push({
463
+ code: "DECISION_BRANCH_ENUM_MISMATCH",
464
+ path: `$.nodes[${nodeIndex}].branches`,
465
+ message: `Decision branches must exactly cover the enum selected by ${node.select.$ref}`,
466
+ atomId: resolved.manifest.id,
467
+ expected: [...enumValues].sort(),
468
+ received: [...declared].sort(),
469
+ hint: "Declare one frozen branch for every finite value of the selected Atom output field",
470
+ });
471
+ }
472
+ }
473
+ }
474
+ }
475
+ }
476
+ for (const edge of control.edges) {
477
+ for (const targetId of edge.targets) {
478
+ const target = nodeById.get(targetId);
479
+ if (!target)
480
+ continue;
481
+ const targetIndex = indexById.get(targetId);
482
+ if (target.kind === "merge") {
483
+ const context = {
484
+ node: target, nodeIndex: targetIndex, graph, resolvedByNode, spec, errors,
485
+ scope: "branch-merge-input", edgeSourceNodeId: edge.sourceNodeId,
486
+ };
487
+ validateBoundValue(edge.with, target.outputSchema, target.outputSchema, `$.nodes[${targetIndex}].incoming.${edge.id}.with`, context, true);
488
+ continue;
489
+ }
490
+ const resolved = resolvedByNode.get(targetId);
491
+ if (!resolved)
492
+ continue;
493
+ const merged = mergeEdgeInput(target.input, edge.with);
494
+ const context = {
495
+ node: target,
496
+ nodeIndex: targetIndex,
497
+ graph,
498
+ resolvedByNode,
499
+ spec,
500
+ errors,
501
+ scope: "edge-input",
502
+ edgeSourceNodeId: edge.sourceNodeId,
503
+ ...(mapItems.has(targetId) ? { mapItem: mapItems.get(targetId) } : {}),
504
+ };
505
+ const sourceIndex = indexById.get(edge.sourceNodeId);
506
+ const edgePath = edge.kind === "next" ? `$.nodes[${sourceIndex}].next` : edge.kind === "decision-default" ? `$.nodes[${sourceIndex}].default` : `$.nodes[${sourceIndex}].branches.${edge.branchId}`;
507
+ validateBoundValue(merged, resolved.manifest.inputSchema, resolved.manifest.inputSchema, `${edgePath}.with`, context, true);
508
+ }
509
+ }
510
+ const outputContextNode = spec.nodes[0];
511
+ const outputContext = {
512
+ node: outputContextNode,
513
+ nodeIndex: 0,
514
+ graph,
515
+ resolvedByNode,
516
+ spec,
517
+ errors,
518
+ scope: "workflow-output",
519
+ };
520
+ for (const [name, reference] of Object.entries(spec.outputs)) {
521
+ resolveReference(reference, `$.outputs.${name}`, outputContext);
522
+ }
523
+ }
524
+ function validateMapGroups(spec, control, errors) {
525
+ const groups = new Map();
526
+ spec.nodes.forEach((node, index) => {
527
+ if (node.kind === "merge" || !node.map)
528
+ return;
529
+ const group = node.map.group ?? node.id;
530
+ groups.set(group, [...(groups.get(group) ?? []), { node, index }]);
531
+ });
532
+ for (const [group, declarations] of groups) {
533
+ const first = declarations[0].node.map;
534
+ const expected = JSON.stringify(canonicalize({
535
+ items: first.items,
536
+ maxItems: first.maxItems,
537
+ maxConcurrency: first.maxConcurrency,
538
+ }));
539
+ for (const { node, index } of declarations.slice(1)) {
540
+ const current = node.map;
541
+ const received = JSON.stringify(canonicalize({
542
+ items: current.items,
543
+ maxItems: current.maxItems,
544
+ maxConcurrency: current.maxConcurrency,
545
+ }));
546
+ if (received !== expected)
547
+ errors.push({
548
+ code: "MAP_GROUP_CONFIG_MISMATCH",
549
+ path: `$.nodes[${index}].map`,
550
+ message: `All processor nodes in Map group ${group} must declare identical items, maxItems and maxConcurrency`,
551
+ expected: JSON.parse(expected),
552
+ received: JSON.parse(received),
553
+ });
554
+ }
555
+ const body = new Set(declarations.map(({ node }) => node.id));
556
+ const exits = control.edges.filter((edge) => body.has(edge.sourceNodeId)
557
+ && (edge.terminal !== undefined || edge.targets.some((target) => !body.has(target))));
558
+ const descriptors = new Set(exits.map((edge) => JSON.stringify(canonicalize({
559
+ targets: edge.targets.filter((target) => !body.has(target)).sort(),
560
+ terminal: edge.terminal ?? null,
561
+ with: edge.with,
562
+ maxTraversals: edge.maxTraversals ?? null,
563
+ backEdge: edge.backEdge,
564
+ }))));
565
+ if (descriptors.size > 1)
566
+ errors.push({
567
+ code: "MAP_COMPLETION_CONFLICT",
568
+ path: `$.nodes[${declarations[0].index}].map`,
569
+ message: `Map processor ${group} has conflicting completion routes; route the aggregated Map output through one following DecisionNode`,
570
+ received: [...descriptors].map((descriptor) => JSON.parse(descriptor)),
571
+ });
572
+ }
573
+ }
574
+ function normalizeIR(spec, graph, control, resolvedByNode, policy) {
575
+ const mapIdByGroup = new Map();
576
+ for (const node of spec.nodes)
577
+ if (node.kind !== "merge" && node.map) {
578
+ const group = node.map.group ?? node.id;
579
+ mapIdByGroup.set(group, `__control-map-${group}`);
580
+ }
581
+ let nodes = [...spec.nodes].sort((a, b) => a.id.localeCompare(b.id)).map((node) => {
582
+ if (node.kind === "merge")
583
+ return canonicalize({
584
+ id: node.id,
585
+ kind: "merge",
586
+ dependsOn: [...node.dependsOn].sort(),
587
+ outputSchema: node.outputSchema,
588
+ });
589
+ const resolved = resolvedByNode.get(node.id);
590
+ const { id, version } = splitAtomReference(node.atom);
591
+ const mapGroup = node.map?.group ?? (node.map ? node.id : undefined);
592
+ return canonicalize({
593
+ id: node.id,
594
+ atom: { key: node.atom, id, version, manifest: resolved.manifest },
595
+ kind: "atom",
596
+ dependsOn: [...node.dependsOn].sort(),
597
+ input: node.input,
598
+ ...(mapGroup ? { mapScope: mapIdByGroup.get(mapGroup) } : {}),
599
+ ...(node.kind === "decision" ? { decisionBranches: Object.keys(node.branches).sort() } : {}),
600
+ effectivePolicy: resolved.effectivePolicy,
601
+ });
602
+ });
603
+ // DecisionNode is authoring sugar: execute its Atom, then route through a
604
+ // pure SwitchNode. Atom output can never inject a new target.
605
+ let edges = [];
606
+ for (const edge of control.edges) {
607
+ const source = spec.nodes.find((node) => node.id === edge.sourceNodeId);
608
+ if (source.kind !== "decision") {
609
+ edges.push(canonicalize({ ...edge, kind: "next", targets: [...edge.targets].sort() }));
610
+ continue;
611
+ }
612
+ const switchId = `__control-switch-${source.id}`;
613
+ edges.push(canonicalize({
614
+ ...edge,
615
+ id: edge.kind === "decision-default" ? `${switchId}:default` : `${switchId}:case:${edge.branchId}`,
616
+ sourceNodeId: switchId,
617
+ kind: edge.kind === "decision-default" ? "switch-default" : "switch-case",
618
+ targets: [...edge.targets].sort(),
619
+ }));
620
+ }
621
+ for (const source of spec.nodes.filter((node) => node.kind === "decision")) {
622
+ const switchId = `__control-switch-${source.id}`;
623
+ const selectRef = source.select.$ref === "node.output"
624
+ ? `dependencies.${source.id}.output`
625
+ : `dependencies.${source.id}.output.${source.select.$ref.split(".").slice(2).join(".")}`;
626
+ nodes.push(canonicalize({
627
+ id: switchId,
628
+ kind: "switch",
629
+ dependsOn: [source.id],
630
+ select: { $ref: selectRef },
631
+ caseIds: Object.keys(source.branches).sort(),
632
+ }));
633
+ edges.push(canonicalize({
634
+ id: `${source.id}:to-switch`, sourceNodeId: source.id, kind: "next",
635
+ targets: [switchId], with: {}, backEdge: false,
636
+ }));
637
+ }
638
+ // Make an actual parallel fan-in an explicit JoinNode. `dependsOn` also
639
+ // contains data dependencies, so it is not sufficient evidence that every
640
+ // incoming control path must arrive. A bounded revisit can put an entire
641
+ // parallel region on graph-theoretic back edges, though, so `backEdge` alone
642
+ // cannot distinguish an OR re-entry from the branches of one AND fan-out.
643
+ //
644
+ // Correlate those regions from the frozen fan-out edge: every branch must
645
+ // reach exactly one distinct direct arrival at the target without first
646
+ // passing through the target. This keeps unrelated revisit edges as OR
647
+ // paths while giving loop-local parallel work the same durable JoinNode and
648
+ // fork-lineage semantics as a forward-only parallel block.
649
+ for (const target of nodes.filter((node) => node.kind === "atom" && node.dependsOn.length > 1)) {
650
+ const forwardIncoming = edges.filter((edge) => !edge.backEdge && edge.targets.includes(target.id));
651
+ const forwardSources = [...new Set(forwardIncoming.map((edge) => edge.sourceNodeId))].sort();
652
+ const logicalSource = (sourceNodeId) => sourceNodeId.startsWith("__control-switch-")
653
+ ? sourceNodeId.slice("__control-switch-".length)
654
+ : sourceNodeId;
655
+ const sourceIsDependency = (sourceNodeId) => target.dependsOn.includes(logicalSource(sourceNodeId));
656
+ const reachesWithoutLeavingTarget = (start, sought) => {
657
+ const pending = [start];
658
+ const seen = new Set();
659
+ while (pending.length > 0) {
660
+ const current = pending.pop();
661
+ if (current === sought)
662
+ return true;
663
+ if (current === target.id || seen.has(current))
664
+ continue;
665
+ seen.add(current);
666
+ for (const edge of edges)
667
+ if (edge.sourceNodeId === current)
668
+ pending.push(...edge.targets);
669
+ }
670
+ return false;
671
+ };
672
+ const directIncoming = edges.filter((edge) => edge.targets.includes(target.id));
673
+ const groups = new Map();
674
+ for (const fork of edges.filter((edge) => edge.targets.length > 1 && !edge.targets.includes(target.id))) {
675
+ const arrivals = [];
676
+ let complete = true;
677
+ for (const branchEntry of fork.targets) {
678
+ const candidates = directIncoming.filter((incoming) => sourceIsDependency(incoming.sourceNodeId)
679
+ && reachesWithoutLeavingTarget(branchEntry, incoming.sourceNodeId));
680
+ if (candidates.length !== 1) {
681
+ complete = false;
682
+ break;
683
+ }
684
+ arrivals.push(candidates[0]);
685
+ }
686
+ const unique = [...new Map(arrivals.map((edge) => [edge.id, edge])).values()]
687
+ .sort((left, right) => left.id.localeCompare(right.id));
688
+ if (complete && unique.length === fork.targets.length && unique.length > 1) {
689
+ groups.set(unique.map((edge) => edge.id).join("|"), unique);
690
+ }
691
+ }
692
+ if (forwardSources.length > 1 && forwardSources.every(sourceIsDependency)) {
693
+ const group = [...forwardIncoming].sort((left, right) => left.id.localeCompare(right.id));
694
+ groups.set(group.map((edge) => edge.id).join("|"), group);
695
+ }
696
+ const claimedEdgeIds = new Set();
697
+ const joinGroups = [...groups.values()]
698
+ .sort((left, right) => right.length - left.length || left[0].id.localeCompare(right[0].id))
699
+ .filter((group) => {
700
+ if (group.some((edge) => claimedEdgeIds.has(edge.id)))
701
+ return false;
702
+ group.forEach((edge) => claimedEdgeIds.add(edge.id));
703
+ return true;
704
+ })
705
+ .sort((left, right) => left[0].id.localeCompare(right[0].id));
706
+ joinGroups.forEach((group, groupIndex) => {
707
+ const joinId = joinGroups.length === 1
708
+ ? `__control-join-${target.id}`
709
+ : `__control-join-${target.id}-${groupIndex + 1}`;
710
+ const joinedEdgeIds = new Set(group.map((edge) => edge.id));
711
+ edges = edges.map((edge) => joinedEdgeIds.has(edge.id)
712
+ ? canonicalize({ ...edge, targets: edge.targets.map((id) => id === target.id ? joinId : id).sort() })
713
+ : edge);
714
+ const sources = [...new Set(group.map((edge) => edge.sourceNodeId))].sort();
715
+ nodes.push(canonicalize({
716
+ id: joinId,
717
+ kind: "join",
718
+ dependsOn: sources,
719
+ dependencies: sources,
720
+ incomingEdgeIds: [],
721
+ ...(target.mapScope ? { mapScope: target.mapScope } : {}),
722
+ }));
723
+ edges.push(canonicalize({
724
+ id: `${joinId}:next`, sourceNodeId: joinId, kind: "join-next",
725
+ targets: [target.id], with: {}, backEdge: false,
726
+ }));
727
+ });
728
+ }
729
+ let entry = [...spec.entry];
730
+ for (const [group, mapId] of [...mapIdByGroup.entries()].sort(([left], [right]) => left.localeCompare(right))) {
731
+ const declarations = spec.nodes.filter((node) => node.kind !== "merge" && (node.map?.group ?? (node.map ? node.id : undefined)) === group);
732
+ const config = declarations[0].map;
733
+ const bodyNodeIds = new Set(nodes.filter((node) => node.mapScope === mapId).map((node) => node.id));
734
+ const bodyEntries = new Set();
735
+ for (const nodeId of bodyNodeIds) {
736
+ if (entry.includes(nodeId) || edges.some((edge) => !bodyNodeIds.has(edge.sourceNodeId) && edge.targets.includes(nodeId)))
737
+ bodyEntries.add(nodeId);
738
+ }
739
+ if (bodyEntries.size === 0) {
740
+ for (const nodeId of bodyNodeIds) {
741
+ if (!edges.some((edge) => bodyNodeIds.has(edge.sourceNodeId) && edge.targets.includes(nodeId)))
742
+ bodyEntries.add(nodeId);
743
+ }
744
+ }
745
+ const exits = edges.filter((edge) => bodyNodeIds.has(edge.sourceNodeId)
746
+ && (edge.terminal !== undefined || edge.targets.some((target) => !bodyNodeIds.has(target))));
747
+ const completionDescriptors = new Map();
748
+ for (const exit of exits) {
749
+ const descriptor = canonicalize({
750
+ targets: exit.targets.filter((target) => !bodyNodeIds.has(target)).sort(),
751
+ ...(exit.terminal ? { terminal: exit.terminal } : {}),
752
+ with: exit.with,
753
+ ...(exit.maxTraversals !== undefined ? { maxTraversals: exit.maxTraversals } : {}),
754
+ backEdge: exit.backEdge,
755
+ });
756
+ completionDescriptors.set(JSON.stringify(descriptor), exit);
757
+ }
758
+ edges = edges.map((edge) => {
759
+ if (bodyNodeIds.has(edge.sourceNodeId)
760
+ && (edge.terminal !== undefined || edge.targets.some((target) => !bodyNodeIds.has(target)))) {
761
+ return canonicalize({
762
+ id: `${edge.id}:map-return`, sourceNodeId: edge.sourceNodeId, kind: "map-return",
763
+ targets: [], with: {}, backEdge: false,
764
+ });
765
+ }
766
+ if (!bodyNodeIds.has(edge.sourceNodeId) && edge.targets.some((target) => bodyNodeIds.has(target))) {
767
+ return canonicalize({ ...edge, targets: [...new Set(edge.targets.map((target) => bodyNodeIds.has(target) ? mapId : target))].sort() });
768
+ }
769
+ return edge;
770
+ });
771
+ entry = [...new Set(entry.map((nodeId) => bodyNodeIds.has(nodeId) ? mapId : nodeId))];
772
+ const maxItems = config.maxItems;
773
+ const maxConcurrency = Math.min(config.maxConcurrency, maxItems, policy.maxNodeActivations);
774
+ nodes.push(canonicalize({
775
+ id: mapId,
776
+ kind: "map",
777
+ dependsOn: [],
778
+ items: config.items,
779
+ maxItems,
780
+ maxConcurrency,
781
+ bodyEntries: [...bodyEntries].sort(),
782
+ bodyNodeIds: [...bodyNodeIds].sort(),
783
+ bodyExitNodeIds: [...new Set(exits.map((edge) => edge.sourceNodeId))].sort(),
784
+ }));
785
+ edges.push(canonicalize({
786
+ id: `${mapId}:enter`, sourceNodeId: mapId, kind: "map-enter",
787
+ targets: [...bodyEntries].sort(), with: {}, backEdge: false,
788
+ }));
789
+ let completionIndex = 0;
790
+ for (const original of completionDescriptors.values()) {
791
+ edges.push(canonicalize({
792
+ id: `${mapId}:complete:${completionIndex++}`,
793
+ sourceNodeId: mapId,
794
+ kind: "map-complete",
795
+ targets: original.targets.filter((target) => !bodyNodeIds.has(target)).sort(),
796
+ ...(original.terminal ? { terminal: original.terminal } : {}),
797
+ with: original.with,
798
+ ...(original.maxTraversals !== undefined ? { maxTraversals: original.maxTraversals } : {}),
799
+ backEdge: original.backEdge,
800
+ }));
801
+ }
802
+ }
803
+ edges = [...edges].sort((left, right) => left.id.localeCompare(right.id));
804
+ nodes = nodes.map((node) => node.kind === "join" ? canonicalize({
805
+ ...node,
806
+ incomingEdgeIds: edges.filter((edge) => edge.targets.includes(node.id)).map((edge) => edge.id).sort(),
807
+ }) : node);
808
+ nodes = [...nodes].sort((left, right) => left.id.localeCompare(right.id));
809
+ const payload = canonicalize({
810
+ schemaVersion: "1.3",
811
+ name: spec.name,
812
+ inputs: spec.inputs,
813
+ nodes,
814
+ outputs: spec.outputs,
815
+ control: {
816
+ entry: [...entry].sort(),
817
+ terminals: [...new Set(edges.flatMap((edge) => edge.terminal ? [edge.terminal] : []))].sort(),
818
+ edges,
819
+ },
820
+ readiness: {
821
+ dependencyStages: graph.stages.map((stage) => [...stage].sort()),
822
+ forks: edges.filter((edge) => edge.targets.length > 1).map((edge) => ({ edgeId: edge.id, targets: edge.targets })),
823
+ joins: nodes.filter((node) => node.kind === "join")
824
+ .map((node) => ({ nodeId: node.id, dependencies: node.dependencies })),
825
+ },
826
+ activationIdentity: { strategy: "workflow-revision-node-activation-index-v1" },
827
+ effectiveRuntimePolicy: {
828
+ maxEdgeTraversals: policy.maxEdgeTraversals,
829
+ maxWorkflowTransitions: policy.maxWorkflowTransitions,
830
+ maxNodeActivations: policy.maxNodeActivations,
831
+ maxMapItems: policy.maxMapItems,
832
+ },
833
+ });
834
+ return freezeDeep({ ...payload, hash: contentHash(payload) });
835
+ }
836
+ export function compileWorkflow(raw, options) {
837
+ const parsed = schemaErrors(raw);
838
+ if (!parsed.spec)
839
+ throw new WorkflowCompilationError(sortValidationErrors(parsed.errors));
840
+ const policyResult = parsePolicy(options.runtimePolicy);
841
+ if (Array.isArray(policyResult))
842
+ throw new WorkflowCompilationError(sortValidationErrors(policyResult));
843
+ const errors = [];
844
+ const catalog = buildCatalog(options.atomCatalog, errors);
845
+ const resolvedByNode = validateNodePolicies(parsed.spec, catalog, policyResult, errors);
846
+ const graph = validateGraph(parsed.spec.nodes, errors);
847
+ const control = validateControlGraph(parsed.spec, policyResult, errors);
848
+ validateMapGroups(parsed.spec, control, errors);
849
+ if (graph)
850
+ validateReferencesAndInputs(parsed.spec, graph, control, resolvedByNode, errors);
851
+ if (errors.length > 0)
852
+ throw new WorkflowCompilationError(sortValidationErrors(errors));
853
+ return normalizeIR(parsed.spec, graph, control, resolvedByNode, policyResult);
854
+ }
855
+ export function validateWorkflow(raw, options) {
856
+ try {
857
+ return { success: true, ir: compileWorkflow(raw, options) };
858
+ }
859
+ catch (error) {
860
+ if (error instanceof WorkflowCompilationError)
861
+ return { success: false, errors: error.errors };
862
+ throw error;
863
+ }
864
+ }
865
+ //# sourceMappingURL=compile-workflow.js.map