@smithers-orchestrator/graph 0.16.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.
package/src/extract.js ADDED
@@ -0,0 +1,616 @@
1
+ import { isAbsolute, resolve as resolvePath } from "node:path";
2
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
3
+ /** @typedef {import("./TaskDescriptor.ts").TaskDescriptor} TaskDescriptor */
4
+ /** @typedef {import("./XmlNode.ts").XmlNode} XmlNode */
5
+ /** @typedef {import("./ExtractOptions.ts").ExtractOptions} ExtractOptions */
6
+ /** @typedef {import("./HostNode.ts").HostNode} HostNode */
7
+ /** @typedef {import("./WorkflowGraph.ts").WorkflowGraph} WorkflowGraph */
8
+
9
+ const DEFAULT_MERGE_QUEUE_CONCURRENCY = 1;
10
+ const WORKTREE_EMPTY_PATH_ERROR = "<Worktree> requires a non-empty path prop";
11
+ const DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS = 300_000;
12
+ const DEFAULT_SANDBOX_TASK_HEARTBEAT_TIMEOUT_MS = 300_000;
13
+ /**
14
+ * @param {string} prefix
15
+ * @param {readonly number[]} path
16
+ * @returns {string}
17
+ */
18
+ function stablePathId(prefix, path) {
19
+ if (path.length === 0)
20
+ return `${prefix}:root`;
21
+ return `${prefix}:${path.join(".")}`;
22
+ }
23
+ /**
24
+ * @param {unknown} explicitId
25
+ * @param {string} prefix
26
+ * @param {readonly number[]} path
27
+ * @returns {string}
28
+ */
29
+ function resolveStableId(explicitId, prefix, path) {
30
+ if (typeof explicitId === "string" && explicitId.trim().length > 0) {
31
+ return explicitId;
32
+ }
33
+ return stablePathId(prefix, path);
34
+ }
35
+ /**
36
+ * @param {unknown} value
37
+ * @returns {value is import("zod").ZodObject<any>}
38
+ */
39
+ function isZodObject(value) {
40
+ return Boolean(value && typeof value === "object" && "shape" in value);
41
+ }
42
+ /**
43
+ * @param {unknown} value
44
+ * @returns {string | undefined}
45
+ */
46
+ function maybeTableName(value) {
47
+ if (!value || typeof value !== "object")
48
+ return undefined;
49
+ const symbols = Object.getOwnPropertySymbols(value);
50
+ for (const symbol of symbols) {
51
+ const key = String(symbol);
52
+ if (key.includes("drizzle") || key.includes("Name")) {
53
+ const symbolValue = value[symbol];
54
+ if (typeof symbolValue === "string" && symbolValue.length > 0) {
55
+ return symbolValue;
56
+ }
57
+ }
58
+ }
59
+ const named = value.name;
60
+ return typeof named === "string" && named.length > 0 ? named : undefined;
61
+ }
62
+ /**
63
+ * @param {Record<string, unknown>} raw
64
+ * @returns {{ outputTable: unknown | null; outputTableName: string; outputRef: import("zod").ZodObject<any> | undefined; outputSchema: import("zod").ZodObject<any> | undefined; }}
65
+ */
66
+ function resolveOutput(raw) {
67
+ const outputRaw = raw.output;
68
+ if (!outputRaw) {
69
+ return {
70
+ outputTable: null,
71
+ outputTableName: "",
72
+ outputRef: undefined,
73
+ outputSchema: undefined,
74
+ };
75
+ }
76
+ const outputRef = isZodObject(outputRaw) ? outputRaw : undefined;
77
+ const tableName = typeof outputRaw === "string" ? outputRaw : maybeTableName(outputRaw) ?? "";
78
+ const outputTable = outputRef ? null : typeof outputRaw === "string" ? null : outputRaw;
79
+ const outputSchema = isZodObject(raw.outputSchema) ? raw.outputSchema : outputRef;
80
+ return {
81
+ outputTable,
82
+ outputTableName: tableName,
83
+ outputRef,
84
+ outputSchema,
85
+ };
86
+ }
87
+ /**
88
+ * @param {Record<string, unknown>} raw
89
+ * @returns {number | null}
90
+ */
91
+ function parseHeartbeatTimeoutMs(raw) {
92
+ const candidate = typeof raw.heartbeatTimeoutMs === "number"
93
+ ? raw.heartbeatTimeoutMs
94
+ : typeof raw.heartbeatTimeout === "number"
95
+ ? raw.heartbeatTimeout
96
+ : null;
97
+ if (candidate == null || !Number.isFinite(candidate) || candidate <= 0) {
98
+ return null;
99
+ }
100
+ return Math.floor(candidate);
101
+ }
102
+ /**
103
+ * @param {Record<string, unknown>} raw
104
+ */
105
+ function resolveRetryConfig(raw) {
106
+ const noRetry = Boolean(raw.noRetry);
107
+ const continueOnFail = Boolean(raw.continueOnFail);
108
+ const hasExplicitRetries = typeof raw.retries === "number" && !Number.isNaN(raw.retries);
109
+ const hasExplicitRetryPolicy = Boolean(raw.retryPolicy && typeof raw.retryPolicy === "object");
110
+ const defaultNoRetryForContinueOnFail = continueOnFail && !hasExplicitRetries && !hasExplicitRetryPolicy;
111
+ const retries = noRetry || defaultNoRetryForContinueOnFail
112
+ ? 0
113
+ : hasExplicitRetries
114
+ ? raw.retries
115
+ : Infinity;
116
+ const retryPolicy = hasExplicitRetryPolicy
117
+ ? raw.retryPolicy
118
+ : retries > 0
119
+ ? { backoff: "exponential", initialDelayMs: 1000 }
120
+ : undefined;
121
+ return { retries, retryPolicy };
122
+ }
123
+ /**
124
+ * @param {HostNode} node
125
+ * @returns {XmlNode}
126
+ */
127
+ function toXmlNode(node) {
128
+ if (node.kind === "text") {
129
+ return { kind: "text", text: node.text };
130
+ }
131
+ const element = {
132
+ kind: "element",
133
+ tag: node.tag,
134
+ props: node.props ?? {},
135
+ children: node.children.map(toXmlNode),
136
+ };
137
+ return element;
138
+ }
139
+ /**
140
+ * @param {ExtractOptions | undefined} opts
141
+ * @param {string} id
142
+ * @returns {number}
143
+ */
144
+ function getRalphIteration(opts, id) {
145
+ const map = opts?.ralphIterations;
146
+ const fallback = typeof opts?.defaultIteration === "number" ? opts.defaultIteration : 0;
147
+ if (!map)
148
+ return fallback;
149
+ if (map instanceof Map) {
150
+ return map.get(id) ?? fallback;
151
+ }
152
+ const value = map[id];
153
+ return typeof value === "number" ? value : fallback;
154
+ }
155
+ /**
156
+ * @param {readonly { readonly ralphId: string; readonly iteration: number }[]} loopStack
157
+ * @returns {string}
158
+ */
159
+ function buildLoopScope(loopStack) {
160
+ if (loopStack.length === 0)
161
+ return "";
162
+ return `@@${loopStack.map((entry) => `${entry.ralphId}=${entry.iteration}`).join(",")}`;
163
+ }
164
+ /**
165
+ * @param {unknown} value
166
+ * @returns {string[] | undefined}
167
+ */
168
+ function strings(value) {
169
+ if (!Array.isArray(value))
170
+ return undefined;
171
+ const filtered = value.filter((entry) => typeof entry === "string");
172
+ return filtered.length > 0 ? filtered : undefined;
173
+ }
174
+ /**
175
+ * @param {unknown} value
176
+ * @returns {Record<string, string> | undefined}
177
+ */
178
+ function needs(value) {
179
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
180
+ return undefined;
181
+ }
182
+ const filtered = Object.entries(value).filter((entry) => typeof entry[1] === "string");
183
+ return filtered.length > 0 ? Object.fromEntries(filtered) : undefined;
184
+ }
185
+ /**
186
+ * @param {unknown} value
187
+ * @returns {TaskDescriptor["approvalOptions"]}
188
+ */
189
+ function approvalOptions(value) {
190
+ if (!Array.isArray(value))
191
+ return undefined;
192
+ const options = value
193
+ .filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)))
194
+ .map((entry) => ({
195
+ key: typeof entry.key === "string" ? entry.key : "",
196
+ label: typeof entry.label === "string" ? entry.label : "",
197
+ ...(typeof entry.summary === "string" ? { summary: entry.summary } : {}),
198
+ ...(entry.metadata &&
199
+ typeof entry.metadata === "object" &&
200
+ !Array.isArray(entry.metadata)
201
+ ? { metadata: entry.metadata }
202
+ : {}),
203
+ }))
204
+ .filter((entry) => entry.key.length > 0 && entry.label.length > 0);
205
+ return options.length > 0 ? options : undefined;
206
+ }
207
+ /**
208
+ * @param {unknown} value
209
+ * @returns {TaskDescriptor["approvalAutoApprove"]}
210
+ */
211
+ function approvalAutoApprove(value) {
212
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
213
+ return undefined;
214
+ }
215
+ const raw = value;
216
+ return {
217
+ ...(typeof raw.after === "number" ? { after: raw.after } : {}),
218
+ ...(typeof raw.audit === "boolean" ? { audit: raw.audit } : {}),
219
+ ...(typeof raw.conditionMet === "boolean"
220
+ ? { conditionMet: raw.conditionMet }
221
+ : {}),
222
+ ...(typeof raw.revertOnMet === "boolean"
223
+ ? { revertOnMet: raw.revertOnMet }
224
+ : {}),
225
+ };
226
+ }
227
+ /**
228
+ * @param {"parallel" | "merge-queue"} tag
229
+ * @param {Record<string, unknown>} raw
230
+ * @param {readonly number[]} path
231
+ * @param {readonly { readonly id: string; readonly max?: number }[]} stack
232
+ */
233
+ function pushGroup(tag, raw, path, stack) {
234
+ const id = resolveStableId(raw.id, tag, path);
235
+ const parsed = Number(raw.maxConcurrency);
236
+ const rawMax = Number.isFinite(parsed) ? Math.floor(parsed) : undefined;
237
+ let max;
238
+ if (tag === "merge-queue") {
239
+ max = Math.max(1, rawMax ?? DEFAULT_MERGE_QUEUE_CONCURRENCY);
240
+ }
241
+ else if (rawMax == null || rawMax <= 0) {
242
+ max = undefined;
243
+ }
244
+ else {
245
+ max = rawMax;
246
+ }
247
+ return [...stack, { id, max }];
248
+ }
249
+ /**
250
+ * @param {Record<string, unknown>} raw
251
+ * @param {string} kind
252
+ * @returns {string}
253
+ */
254
+ function requireTaskId(raw, kind) {
255
+ if (!raw.id || typeof raw.id !== "string") {
256
+ throw new SmithersError("TASK_ID_REQUIRED", `${kind} id is required and must be a string.`);
257
+ }
258
+ return raw.id;
259
+ }
260
+ /**
261
+ * @param {Record<string, unknown>} raw
262
+ * @param {string} nodeId
263
+ * @param {string} kind
264
+ */
265
+ function requireOutput(raw, nodeId, kind) {
266
+ if (!raw.output) {
267
+ throw new SmithersError("TASK_MISSING_OUTPUT", `${kind} ${nodeId} is missing output.`, { nodeId });
268
+ }
269
+ }
270
+ /**
271
+ * @param {HostNode | null} root
272
+ * @param {ExtractOptions} [opts]
273
+ * @returns {WorkflowGraph}
274
+ */
275
+ export function extractGraph(root, opts) {
276
+ if (!root) {
277
+ return { xml: null, tasks: [], mountedTaskIds: [] };
278
+ }
279
+ const tasks = [];
280
+ const mountedTaskIds = [];
281
+ const seen = new Set();
282
+ const seenRalph = new Set();
283
+ const seenWorktree = new Set();
284
+ const seenSaga = new Set();
285
+ const seenTcf = new Set();
286
+ let ordinal = 0;
287
+ /**
288
+ * @param {Record<string, unknown>} raw
289
+ * @param {string} nodeId
290
+ * @param {Omit<TaskDescriptor, "ordinal" | "nodeId">} descriptor
291
+ */
292
+ function addDescriptor(raw, nodeId, descriptor) {
293
+ if (seen.has(nodeId)) {
294
+ throw new SmithersError("DUPLICATE_ID", `Duplicate ${String(raw.__smithersKind ?? "Task")} id detected: ${nodeId}`, { id: nodeId });
295
+ }
296
+ seen.add(nodeId);
297
+ tasks.push({ nodeId, ordinal: ordinal++, ...descriptor });
298
+ mountedTaskIds.push(`${nodeId}::${descriptor.iteration}`);
299
+ }
300
+ /**
301
+ * @param {HostNode} node
302
+ * @param {{ readonly path: readonly number[]; readonly iteration: number; readonly ralphId?: string; readonly parentIsRalph: boolean; readonly parallelStack: readonly { readonly id: string; readonly max?: number }[]; readonly worktreeStack: readonly { readonly id: string; readonly path: string; readonly branch?: string; readonly baseBranch?: string; }[]; readonly loopStack: readonly { readonly ralphId: string; readonly iteration: number }[]; }} ctx
303
+ */
304
+ function walk(node, ctx) {
305
+ if (node.kind === "text")
306
+ return;
307
+ const raw = node.rawProps ?? {};
308
+ let iteration = ctx.iteration;
309
+ let ralphId = ctx.ralphId;
310
+ let loopStack = ctx.loopStack;
311
+ let nextParallelStack = ctx.parallelStack;
312
+ let nextWorktreeStack = ctx.worktreeStack;
313
+ if (node.tag === "smithers:ralph") {
314
+ if (ctx.parentIsRalph) {
315
+ throw new SmithersError("NESTED_LOOP", "Nested <Ralph> is not supported.");
316
+ }
317
+ const logicalId = resolveStableId(raw.id, "ralph", ctx.path);
318
+ const id = logicalId + buildLoopScope(loopStack);
319
+ if (seenRalph.has(id)) {
320
+ throw new SmithersError("DUPLICATE_ID", `Duplicate Ralph id detected: ${id}`, { kind: "ralph", id });
321
+ }
322
+ seenRalph.add(id);
323
+ ralphId = id;
324
+ iteration = getRalphIteration(opts, id);
325
+ loopStack = [...loopStack, { ralphId: logicalId, iteration }];
326
+ }
327
+ if (node.tag === "smithers:parallel") {
328
+ nextParallelStack = pushGroup("parallel", raw, ctx.path, ctx.parallelStack);
329
+ }
330
+ if (node.tag === "smithers:merge-queue") {
331
+ nextParallelStack = pushGroup("merge-queue", raw, ctx.path, nextParallelStack);
332
+ }
333
+ if (node.tag === "smithers:worktree") {
334
+ const id = resolveStableId(raw.id, "worktree", ctx.path);
335
+ if (seenWorktree.has(id)) {
336
+ throw new SmithersError("DUPLICATE_ID", `Duplicate Worktree id detected: ${id}`, { kind: "worktree", id });
337
+ }
338
+ seenWorktree.add(id);
339
+ const pathVal = String(raw.path ?? "").trim();
340
+ if (!pathVal) {
341
+ throw new SmithersError("WORKTREE_EMPTY_PATH", WORKTREE_EMPTY_PATH_ERROR);
342
+ }
343
+ const base = typeof opts?.baseRootDir === "string" && opts.baseRootDir.length > 0
344
+ ? opts.baseRootDir
345
+ : process.cwd();
346
+ nextWorktreeStack = [
347
+ ...ctx.worktreeStack,
348
+ {
349
+ id,
350
+ path: isAbsolute(pathVal) ? resolvePath(pathVal) : resolvePath(base, pathVal),
351
+ ...(raw.branch ? { branch: String(raw.branch) } : {}),
352
+ ...(raw.baseBranch ? { baseBranch: String(raw.baseBranch) } : {}),
353
+ },
354
+ ];
355
+ }
356
+ const ancestorScope = loopStack.length > 1 ? buildLoopScope(loopStack.slice(0, -1)) : "";
357
+ const parallelGroup = nextParallelStack[nextParallelStack.length - 1];
358
+ const topWorktree = nextWorktreeStack[nextWorktreeStack.length - 1];
359
+ const common = {
360
+ iteration,
361
+ ralphId,
362
+ worktreeId: topWorktree?.id,
363
+ worktreePath: topWorktree?.path,
364
+ worktreeBranch: topWorktree?.branch,
365
+ worktreeBaseBranch: topWorktree?.baseBranch,
366
+ dependsOn: strings(raw.dependsOn),
367
+ needs: needs(raw.needs),
368
+ parallelGroupId: parallelGroup?.id,
369
+ parallelMaxConcurrency: parallelGroup?.max,
370
+ };
371
+ if (node.tag === "smithers:subflow") {
372
+ const logicalNodeId = requireTaskId(raw, "Subflow");
373
+ const mode = raw.__smithersSubflowMode ?? raw.mode ?? "childRun";
374
+ if (mode !== "inline") {
375
+ const nodeId = logicalNodeId + ancestorScope;
376
+ requireOutput(raw, nodeId, "Subflow");
377
+ const { retries, retryPolicy } = resolveRetryConfig(raw);
378
+ const output = resolveOutput(raw);
379
+ addDescriptor(raw, nodeId, {
380
+ ...common,
381
+ ...output,
382
+ needsApproval: false,
383
+ skipIf: Boolean(raw.skipIf),
384
+ retries,
385
+ retryPolicy,
386
+ timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : null,
387
+ heartbeatTimeoutMs: parseHeartbeatTimeoutMs(raw),
388
+ continueOnFail: Boolean(raw.continueOnFail),
389
+ cachePolicy: raw.cache && typeof raw.cache === "object"
390
+ ? raw.cache
391
+ : undefined,
392
+ label: typeof raw.label === "string" ? raw.label : undefined,
393
+ meta: {
394
+ ...(raw.meta && typeof raw.meta === "object" && !Array.isArray(raw.meta)
395
+ ? raw.meta
396
+ : {}),
397
+ __subflow: true,
398
+ __subflowMode: mode,
399
+ __subflowInput: raw.__smithersSubflowInput,
400
+ __subflowWorkflow: raw.__smithersSubflowWorkflow,
401
+ },
402
+ });
403
+ }
404
+ }
405
+ if (node.tag === "smithers:sandbox") {
406
+ const logicalNodeId = requireTaskId(raw, "Sandbox");
407
+ const nodeId = logicalNodeId + ancestorScope;
408
+ requireOutput(raw, nodeId, "Sandbox");
409
+ const { retries, retryPolicy } = resolveRetryConfig(raw);
410
+ const output = resolveOutput(raw);
411
+ const runtime = raw.__smithersSandboxRuntime ?? raw.runtime ?? "bubblewrap";
412
+ addDescriptor(raw, nodeId, {
413
+ ...common,
414
+ ...output,
415
+ needsApproval: false,
416
+ skipIf: Boolean(raw.skipIf),
417
+ retries,
418
+ retryPolicy,
419
+ timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : null,
420
+ heartbeatTimeoutMs: parseHeartbeatTimeoutMs(raw) ?? DEFAULT_SANDBOX_TASK_HEARTBEAT_TIMEOUT_MS,
421
+ continueOnFail: Boolean(raw.continueOnFail),
422
+ cachePolicy: raw.cache && typeof raw.cache === "object"
423
+ ? raw.cache
424
+ : undefined,
425
+ label: typeof raw.label === "string" ? raw.label : undefined,
426
+ meta: {
427
+ ...(raw.meta && typeof raw.meta === "object" && !Array.isArray(raw.meta)
428
+ ? raw.meta
429
+ : {}),
430
+ __sandbox: true,
431
+ __sandboxRuntime: runtime,
432
+ __sandboxInput: raw.__smithersSandboxInput ?? raw.input,
433
+ },
434
+ });
435
+ return;
436
+ }
437
+ if (node.tag === "smithers:wait-for-event") {
438
+ const logicalNodeId = requireTaskId(raw, "WaitForEvent");
439
+ const nodeId = logicalNodeId + ancestorScope;
440
+ requireOutput(raw, nodeId, "WaitForEvent");
441
+ const output = resolveOutput(raw);
442
+ const onTimeout = raw.__smithersOnTimeout ?? raw.onTimeout ?? "fail";
443
+ addDescriptor(raw, nodeId, {
444
+ ...common,
445
+ ...output,
446
+ needsApproval: false,
447
+ waitAsync: Boolean(raw.waitAsync),
448
+ skipIf: Boolean(raw.skipIf),
449
+ retries: 0,
450
+ timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : null,
451
+ heartbeatTimeoutMs: parseHeartbeatTimeoutMs(raw),
452
+ continueOnFail: onTimeout === "continue" || onTimeout === "skip",
453
+ label: typeof raw.label === "string" ? raw.label : undefined,
454
+ meta: {
455
+ ...(raw.meta && typeof raw.meta === "object" && !Array.isArray(raw.meta)
456
+ ? raw.meta
457
+ : {}),
458
+ __waitForEvent: true,
459
+ __eventName: raw.__smithersEventName ?? raw.event,
460
+ __correlationId: raw.__smithersCorrelationId ?? raw.correlationId,
461
+ __onTimeout: onTimeout,
462
+ },
463
+ });
464
+ }
465
+ if (node.tag === "smithers:timer") {
466
+ const logicalNodeId = requireTaskId(raw, "Timer");
467
+ if (logicalNodeId.length > 256) {
468
+ throw new SmithersError("INVALID_INPUT", `Timer id must be 256 characters or fewer (received ${logicalNodeId.length}).`, { nodeId: logicalNodeId, maxLength: 256 });
469
+ }
470
+ const nodeId = logicalNodeId + ancestorScope;
471
+ const duration = typeof (raw.__smithersTimerDuration ?? raw.duration) === "string"
472
+ ? String(raw.__smithersTimerDuration ?? raw.duration).trim()
473
+ : "";
474
+ const untilRaw = raw.__smithersTimerUntil ?? raw.until;
475
+ const until = typeof untilRaw === "string"
476
+ ? untilRaw.trim()
477
+ : untilRaw instanceof Date
478
+ ? untilRaw.toISOString()
479
+ : "";
480
+ const hasDuration = duration.length > 0;
481
+ const hasUntil = until.length > 0;
482
+ if ((hasDuration ? 1 : 0) + (hasUntil ? 1 : 0) !== 1) {
483
+ throw new SmithersError("INVALID_INPUT", `Timer ${nodeId} must define exactly one of duration or until.`, { nodeId, duration: raw.duration, until: raw.until });
484
+ }
485
+ if (raw.every !== undefined) {
486
+ throw new SmithersError("INVALID_INPUT", `Timer ${nodeId} uses every=, but recurring timers are not supported yet.`, { nodeId, every: raw.every });
487
+ }
488
+ addDescriptor(raw, nodeId, {
489
+ ...common,
490
+ outputTable: null,
491
+ outputTableName: "",
492
+ outputRef: undefined,
493
+ outputSchema: undefined,
494
+ needsApproval: false,
495
+ skipIf: Boolean(raw.skipIf),
496
+ retries: 0,
497
+ timeoutMs: null,
498
+ heartbeatTimeoutMs: null,
499
+ continueOnFail: false,
500
+ label: typeof raw.label === "string" ? raw.label : `timer:${nodeId}`,
501
+ meta: {
502
+ ...(raw.meta && typeof raw.meta === "object" && !Array.isArray(raw.meta)
503
+ ? raw.meta
504
+ : {}),
505
+ __timer: true,
506
+ __timerType: hasDuration ? "duration" : "absolute",
507
+ ...(hasDuration ? { __timerDuration: duration } : {}),
508
+ ...(hasUntil ? { __timerUntil: until } : {}),
509
+ },
510
+ });
511
+ }
512
+ if (node.tag === "smithers:saga") {
513
+ const id = resolveStableId(raw.id, "saga", ctx.path);
514
+ if (seenSaga.has(id)) {
515
+ throw new SmithersError("DUPLICATE_ID", `Duplicate Saga id detected: ${id}`, { kind: "saga", id });
516
+ }
517
+ seenSaga.add(id);
518
+ }
519
+ if (node.tag === "smithers:try-catch-finally") {
520
+ const id = resolveStableId(raw.id, "tcf", ctx.path);
521
+ if (seenTcf.has(id)) {
522
+ throw new SmithersError("DUPLICATE_ID", `Duplicate TryCatchFinally id detected: ${id}`, { kind: "try-catch-finally", id });
523
+ }
524
+ seenTcf.add(id);
525
+ }
526
+ if (node.tag === "smithers:task") {
527
+ const logicalNodeId = requireTaskId(raw, "Task");
528
+ const nodeId = logicalNodeId + ancestorScope;
529
+ requireOutput(raw, nodeId, "Task");
530
+ const output = resolveOutput(raw);
531
+ const approvalMode = raw.approvalMode === "decision" ||
532
+ raw.approvalMode === "select" ||
533
+ raw.approvalMode === "rank"
534
+ ? raw.approvalMode
535
+ : "gate";
536
+ const approvalOnDeny = raw.approvalOnDeny === "continue" ||
537
+ raw.approvalOnDeny === "skip" ||
538
+ raw.approvalOnDeny === "fail"
539
+ ? raw.approvalOnDeny
540
+ : undefined;
541
+ const { retries, retryPolicy } = resolveRetryConfig(raw);
542
+ const kind = raw.__smithersKind;
543
+ const isAgent = kind === "agent" || Boolean(raw.agent);
544
+ const isCompute = kind === "compute" && typeof raw.__smithersComputeFn === "function";
545
+ const parsedHeartbeatTimeoutMs = parseHeartbeatTimeoutMs(raw);
546
+ const heartbeatTimeoutMs = parsedHeartbeatTimeoutMs ??
547
+ (isAgent ? DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS : null);
548
+ const prompt = isAgent ? String(raw.children ?? "") : undefined;
549
+ if (prompt === "[object Object]") {
550
+ throw new SmithersError("MDX_PRELOAD_INACTIVE", `Task "${logicalNodeId}" prompt resolved to [object Object].`);
551
+ }
552
+ addDescriptor(raw, nodeId, {
553
+ ...common,
554
+ ...output,
555
+ needsApproval: Boolean(raw.needsApproval),
556
+ waitAsync: Boolean(raw.waitAsync),
557
+ approvalMode,
558
+ approvalOnDeny,
559
+ approvalOptions: approvalOptions(raw.approvalOptions),
560
+ approvalAllowedScopes: strings(raw.approvalAllowedScopes),
561
+ approvalAllowedUsers: strings(raw.approvalAllowedUsers),
562
+ approvalAutoApprove: approvalAutoApprove(raw.approvalAutoApprove),
563
+ skipIf: Boolean(raw.skipIf),
564
+ retries,
565
+ retryPolicy,
566
+ timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : null,
567
+ heartbeatTimeoutMs,
568
+ continueOnFail: Boolean(raw.continueOnFail),
569
+ cachePolicy: raw.cache && typeof raw.cache === "object"
570
+ ? raw.cache
571
+ : undefined,
572
+ agent: raw.agent,
573
+ prompt,
574
+ staticPayload: isAgent || isCompute
575
+ ? undefined
576
+ : (raw.__smithersPayload ?? raw.__payload ?? raw.children),
577
+ computeFn: isCompute
578
+ ? raw.__smithersComputeFn
579
+ : undefined,
580
+ label: typeof raw.label === "string" ? raw.label : undefined,
581
+ meta: raw.meta && typeof raw.meta === "object" && !Array.isArray(raw.meta)
582
+ ? raw.meta
583
+ : undefined,
584
+ scorers: raw.scorers && typeof raw.scorers === "object" && !Array.isArray(raw.scorers)
585
+ ? raw.scorers
586
+ : undefined,
587
+ memoryConfig: raw.memory && typeof raw.memory === "object" && !Array.isArray(raw.memory)
588
+ ? raw.memory
589
+ : undefined,
590
+ });
591
+ }
592
+ let elementIndex = 0;
593
+ for (const child of node.children) {
594
+ const nextPath = child.kind === "element" ? [...ctx.path, elementIndex++] : ctx.path;
595
+ walk(child, {
596
+ path: nextPath,
597
+ iteration,
598
+ ralphId,
599
+ parentIsRalph: node.tag === "smithers:ralph",
600
+ parallelStack: nextParallelStack,
601
+ worktreeStack: nextWorktreeStack,
602
+ loopStack,
603
+ });
604
+ }
605
+ }
606
+ walk(root, {
607
+ path: [],
608
+ iteration: 0,
609
+ parentIsRalph: false,
610
+ parallelStack: [],
611
+ worktreeStack: [],
612
+ loopStack: [],
613
+ });
614
+ return { xml: toXmlNode(root), tasks, mountedTaskIds };
615
+ }
616
+ export const extractFromHost = extractGraph;