blueprint-extractor-mcp 6.3.1 → 7.0.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 (56) hide show
  1. package/dist/compactor.js +54 -0
  2. package/dist/helpers/blueprint-dsl-parser.d.ts +52 -0
  3. package/dist/helpers/blueprint-dsl-parser.js +513 -0
  4. package/dist/helpers/material-dsl-parser.d.ts +42 -0
  5. package/dist/helpers/material-dsl-parser.js +416 -0
  6. package/dist/helpers/next-step-hints.js +68 -0
  7. package/dist/helpers/property-shorthand.d.ts +1 -0
  8. package/dist/helpers/property-shorthand.js +22 -0
  9. package/dist/helpers/slot-presets.d.ts +3 -0
  10. package/dist/helpers/slot-presets.js +26 -0
  11. package/dist/helpers/tool-results.d.ts +1 -1
  12. package/dist/helpers/tool-results.js +2 -55
  13. package/dist/helpers/widget-class-aliases.d.ts +2 -0
  14. package/dist/helpers/widget-class-aliases.js +15 -0
  15. package/dist/helpers/widget-diff-parser.d.ts +22 -0
  16. package/dist/helpers/widget-diff-parser.js +194 -0
  17. package/dist/helpers/widget-dsl-parser.d.ts +18 -0
  18. package/dist/helpers/widget-dsl-parser.js +474 -0
  19. package/dist/helpers/widget-recipe-formatter.d.ts +5 -0
  20. package/dist/helpers/widget-recipe-formatter.js +167 -0
  21. package/dist/helpers/widget-recipe-parser.d.ts +12 -0
  22. package/dist/helpers/widget-recipe-parser.js +212 -0
  23. package/dist/helpers/widget-utils.d.ts +15 -0
  24. package/dist/helpers/widget-utils.js +69 -0
  25. package/dist/register-server-tools.js +12 -0
  26. package/dist/schemas/tool-inputs.d.ts +20 -20
  27. package/dist/schemas/tool-inputs.js +13 -13
  28. package/dist/schemas/tool-results.d.ts +84 -84
  29. package/dist/server-config.js +7 -1
  30. package/dist/tool-surface-manager.js +4 -1
  31. package/dist/tools/analysis-tools.js +5 -5
  32. package/dist/tools/animation-authoring.js +18 -18
  33. package/dist/tools/automation-runs.js +9 -9
  34. package/dist/tools/blueprint-authoring.js +46 -17
  35. package/dist/tools/commonui-button-style.js +11 -11
  36. package/dist/tools/composite-tools.js +3 -3
  37. package/dist/tools/composite-workflows.d.ts +10 -0
  38. package/dist/tools/composite-workflows.js +426 -0
  39. package/dist/tools/data-and-input.js +24 -24
  40. package/dist/tools/extraction.js +41 -30
  41. package/dist/tools/import-jobs.js +5 -5
  42. package/dist/tools/material-authoring.js +53 -20
  43. package/dist/tools/material-instance.js +14 -14
  44. package/dist/tools/project-control.js +39 -39
  45. package/dist/tools/project-intelligence.js +6 -6
  46. package/dist/tools/recipe-tools.d.ts +10 -0
  47. package/dist/tools/recipe-tools.js +205 -0
  48. package/dist/tools/schema-and-ai-authoring.js +40 -40
  49. package/dist/tools/tables-and-curves.js +27 -27
  50. package/dist/tools/utility-tools.js +2 -2
  51. package/dist/tools/widget-animation-authoring.js +9 -9
  52. package/dist/tools/widget-extraction.js +31 -7
  53. package/dist/tools/widget-structure.js +266 -106
  54. package/dist/tools/widget-verification.js +19 -19
  55. package/dist/tools/window-ui.js +10 -10
  56. package/package.json +2 -2
package/dist/compactor.js CHANGED
@@ -21,6 +21,47 @@ function deleteEmptyArrayField(object, key) {
21
21
  function shouldStripField(key, patterns) {
22
22
  return patterns.some((pattern) => pattern.test(key));
23
23
  }
24
+ function stripNullsInPlace(value) {
25
+ if (Array.isArray(value)) {
26
+ for (const entry of value)
27
+ stripNullsInPlace(entry);
28
+ return;
29
+ }
30
+ if (!isPlainObject(value))
31
+ return;
32
+ for (const key of Object.keys(value)) {
33
+ if (value[key] === null) {
34
+ delete value[key];
35
+ continue;
36
+ }
37
+ stripNullsInPlace(value[key]);
38
+ }
39
+ }
40
+ const WIDGET_DEFAULT_STRIP_PATTERNS = [
41
+ ['bIsVariable', false],
42
+ ['Visibility', 'Visible'],
43
+ ['Visibility', 'SelfHitTestInvisible'],
44
+ ['RenderOpacity', 1.0],
45
+ ['RenderOpacity', 1],
46
+ ['IsEnabled', true],
47
+ ];
48
+ function stripWidgetDefaultsInPlace(value) {
49
+ if (Array.isArray(value)) {
50
+ for (const entry of value)
51
+ stripWidgetDefaultsInPlace(entry);
52
+ return;
53
+ }
54
+ if (!isPlainObject(value))
55
+ return;
56
+ for (const [field, defaultValue] of WIDGET_DEFAULT_STRIP_PATTERNS) {
57
+ if (field in value && value[field] === defaultValue) {
58
+ delete value[field];
59
+ }
60
+ }
61
+ for (const key of Object.keys(value)) {
62
+ stripWidgetDefaultsInPlace(value[key]);
63
+ }
64
+ }
24
65
  function stripFieldsInPlace(value, fieldPatterns) {
25
66
  if (Array.isArray(value)) {
26
67
  for (const entry of value) {
@@ -46,6 +87,7 @@ export function stripFields(data, fieldPatterns) {
46
87
  return data;
47
88
  }
48
89
  export function compactGenericExtraction(data) {
90
+ stripNullsInPlace(data);
49
91
  return stripFields(data, GUID_AND_POSITION_PATTERNS);
50
92
  }
51
93
  /**
@@ -58,6 +100,7 @@ export function compactGenericExtraction(data) {
58
100
  export function compactBlueprint(data) {
59
101
  if (!isPlainObject(data))
60
102
  return data;
103
+ stripNullsInPlace(data);
61
104
  const root = data;
62
105
  const bp = root.blueprint;
63
106
  if (!bp)
@@ -79,6 +122,14 @@ export function compactBlueprint(data) {
79
122
  export function compactWidgetBlueprint(data) {
80
123
  if (!isPlainObject(data))
81
124
  return data;
125
+ // Strip nulls and widget defaults inside sub-trees, not at data root
126
+ // (top-level nulls like rootWidget: null carry semantic meaning)
127
+ for (const key of Object.keys(data)) {
128
+ if (data[key] !== null) {
129
+ stripNullsInPlace(data[key]);
130
+ }
131
+ }
132
+ stripWidgetDefaultsInPlace(data);
82
133
  const rootWidget = data.rootWidget;
83
134
  if (isPlainObject(rootWidget)) {
84
135
  compactWidgetNode(rootWidget);
@@ -104,6 +155,7 @@ export function compactWidgetBlueprint(data) {
104
155
  export function compactBehaviorTree(data) {
105
156
  if (!isPlainObject(data))
106
157
  return data;
158
+ stripNullsInPlace(data);
107
159
  delete data.schemaVersion;
108
160
  const behaviorTree = data.behaviorTree;
109
161
  if (!isPlainObject(behaviorTree)) {
@@ -118,6 +170,7 @@ export function compactBehaviorTree(data) {
118
170
  export function compactStateTree(data) {
119
171
  if (!isPlainObject(data))
120
172
  return data;
173
+ stripNullsInPlace(data);
121
174
  delete data.schemaVersion;
122
175
  const stateTree = data.stateTree;
123
176
  if (!isPlainObject(stateTree)) {
@@ -140,6 +193,7 @@ export function compactMaterial(data) {
140
193
  if (!isPlainObject(data)) {
141
194
  return data;
142
195
  }
196
+ stripNullsInPlace(data);
143
197
  const graph = findMaterialGraphRoot(data);
144
198
  if (!graph) {
145
199
  return compactGenericExtraction(data);
@@ -0,0 +1,52 @@
1
+ export interface BlueprintDslNode {
2
+ /** Node type: function call, event, variable get/set, branch, cast, macro, literal. */
3
+ type: 'event' | 'call' | 'variable_get' | 'variable_set' | 'branch' | 'cast' | 'macro' | 'literal';
4
+ /** The function/event/variable name. */
5
+ name: string;
6
+ /** Target object (e.g., "PC" in "PC.GetPawn"). */
7
+ target?: string;
8
+ /** Cast target class. */
9
+ castTo?: string;
10
+ /** Alias for the output (e.g., "as PC"). */
11
+ alias?: string;
12
+ /** Arguments (for function calls). */
13
+ args?: Record<string, unknown>;
14
+ /** Connections to next nodes (exec pins). */
15
+ then?: BlueprintDslNode[];
16
+ /** Named output branches (for Branch, Switch, etc.). */
17
+ branches?: Record<string, BlueprintDslNode[]>;
18
+ /** Comparison condition (for `condition ? Branch`). */
19
+ condition?: {
20
+ left: string;
21
+ operator: string;
22
+ right: string;
23
+ };
24
+ }
25
+ export interface BlueprintDslGraph {
26
+ graphName: string;
27
+ nodes: BlueprintDslNode[];
28
+ }
29
+ export interface BlueprintDslResult {
30
+ graphs: BlueprintDslGraph[];
31
+ warnings: string[];
32
+ }
33
+ export interface PayloadNode {
34
+ nodeClass: string;
35
+ tempId: string;
36
+ [key: string]: unknown;
37
+ }
38
+ export interface PayloadConnection {
39
+ fromNode: string;
40
+ fromPin: string;
41
+ toNode: string;
42
+ toPin: string;
43
+ }
44
+ export interface PayloadGraph {
45
+ graphName: string;
46
+ nodes: PayloadNode[];
47
+ connections: PayloadConnection[];
48
+ }
49
+ export declare function parseBlueprintDsl(dsl: string): BlueprintDslResult;
50
+ export declare function blueprintDslToPayload(graphs: BlueprintDslGraph[]): {
51
+ functionGraphs: PayloadGraph[];
52
+ };
@@ -0,0 +1,513 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Blueprint Graph DSL Parser
3
+ //
4
+ // Converts a pseudocode-style DSL into an intermediate representation that
5
+ // maps to the `upsert_function_graphs` payload for `modify_blueprint_graphs`.
6
+ //
7
+ // The DSL is a high-level intent description — the UE subsystem handles
8
+ // actual node creation, UUID assignment, and pin resolution.
9
+ // ---------------------------------------------------------------------------
10
+ // ---------------------------------------------------------------------------
11
+ // Main entry point — Parser
12
+ // ---------------------------------------------------------------------------
13
+ export function parseBlueprintDsl(dsl) {
14
+ const warnings = [];
15
+ const rawLines = dsl.split(/\r?\n/);
16
+ const graphs = [];
17
+ let currentGraph = null;
18
+ let branchContext = null;
19
+ for (let lineIdx = 0; lineIdx < rawLines.length; lineIdx++) {
20
+ const lineNum = lineIdx + 1;
21
+ const raw = rawLines[lineIdx];
22
+ const trimmed = raw.trimEnd();
23
+ // Skip blank lines and comments
24
+ if (trimmed.length === 0)
25
+ continue;
26
+ const stripped = trimmed.trimStart();
27
+ if (stripped.startsWith('#') || stripped.startsWith('//'))
28
+ continue;
29
+ // Measure indentation
30
+ const normalized = trimmed.replace(/\t/g, ' ');
31
+ const strippedNorm = normalized.trimStart();
32
+ const leadingSpaces = normalized.length - strippedNorm.length;
33
+ // Graph header: line ending with `:` at indent level 0
34
+ if (leadingSpaces === 0 && stripped.endsWith(':') && !stripped.includes('->')) {
35
+ const graphName = stripped.slice(0, -1).trim();
36
+ if (graphName.length === 0) {
37
+ warnings.push(`Line ${lineNum}: empty graph name`);
38
+ continue;
39
+ }
40
+ currentGraph = { graphName, nodes: [] };
41
+ graphs.push(currentGraph);
42
+ branchContext = null;
43
+ continue;
44
+ }
45
+ // If no graph header yet, create a default EventGraph
46
+ if (!currentGraph) {
47
+ currentGraph = { graphName: 'EventGraph', nodes: [] };
48
+ graphs.push(currentGraph);
49
+ }
50
+ // Check for branch label lines: `True ->`, `False ->`, `Default ->`
51
+ const branchLabelMatch = stripped.match(/^(\w+)\s*->\s*$/);
52
+ if (branchLabelMatch && branchContext) {
53
+ branchContext.currentLabel = branchLabelMatch[1];
54
+ continue;
55
+ }
56
+ // Check for inline branch labels: `True -> NodeChain`
57
+ const inlineBranchMatch = stripped.match(/^(\w+)\s*->\s+(.+)$/);
58
+ if (inlineBranchMatch && branchContext && leadingSpaces > branchContext.indent) {
59
+ const label = inlineBranchMatch[1];
60
+ const chainStr = inlineBranchMatch[2];
61
+ const chain = parseChain(chainStr, lineNum, warnings);
62
+ if (chain.length > 0) {
63
+ const branchNode = branchContext.node;
64
+ if (!branchNode.branches)
65
+ branchNode.branches = {};
66
+ if (!branchNode.branches[label])
67
+ branchNode.branches[label] = [];
68
+ branchNode.branches[label].push(...chain);
69
+ }
70
+ continue;
71
+ }
72
+ // Check for branch continuation (indented lines after a branch label)
73
+ if (branchContext && branchContext.currentLabel && leadingSpaces > branchContext.indent) {
74
+ const chain = parseChain(stripped, lineNum, warnings);
75
+ if (chain.length > 0) {
76
+ const branchNode = branchContext.node;
77
+ if (!branchNode.branches)
78
+ branchNode.branches = {};
79
+ const label = branchContext.currentLabel;
80
+ if (!branchNode.branches[label])
81
+ branchNode.branches[label] = [];
82
+ branchNode.branches[label].push(...chain);
83
+ }
84
+ continue;
85
+ }
86
+ // Reset branch context if we're back at or above the branch indent level
87
+ if (branchContext && leadingSpaces <= branchContext.indent) {
88
+ branchContext = null;
89
+ }
90
+ // Parse the line as a chain of nodes
91
+ const chain = parseChain(stripped, lineNum, warnings);
92
+ if (chain.length === 0)
93
+ continue;
94
+ // Check if the last node in the chain is a branch
95
+ const lastNode = chain[chain.length - 1];
96
+ if (lastNode.type === 'branch') {
97
+ branchContext = {
98
+ node: lastNode,
99
+ indent: leadingSpaces,
100
+ currentLabel: null,
101
+ };
102
+ }
103
+ // Wire chain: each node's `then` points to the next
104
+ for (let i = 0; i < chain.length - 1; i++) {
105
+ chain[i].then = [chain[i + 1]];
106
+ }
107
+ // Add root of chain to current graph
108
+ currentGraph.nodes.push(chain[0]);
109
+ }
110
+ // If no graphs were produced at all (empty or comment-only input), return empty
111
+ return { graphs, warnings };
112
+ }
113
+ // ---------------------------------------------------------------------------
114
+ // Chain parser — handles `A -> B -> C` chains within a single line
115
+ // ---------------------------------------------------------------------------
116
+ function parseChain(line, lineNum, warnings) {
117
+ // Strip trailing ` ->` — this signals continuation on the next indented line
118
+ // but has no effect on the current chain (the connection is implicit).
119
+ const cleaned = line.replace(/\s*->\s*$/, '');
120
+ // Split on ` -> ` (with surrounding spaces) but preserve content inside parens
121
+ const segments = splitChainArrow(cleaned);
122
+ const nodes = [];
123
+ for (const segment of segments) {
124
+ const trimmed = segment.trim();
125
+ if (trimmed.length === 0)
126
+ continue;
127
+ const node = parseNodeExpression(trimmed, lineNum, warnings);
128
+ if (node) {
129
+ nodes.push(node);
130
+ }
131
+ }
132
+ return nodes;
133
+ }
134
+ /**
135
+ * Split a line on ` -> ` tokens, but NOT inside parentheses.
136
+ * E.g. `A(x, y) -> B -> C(a -> b)` splits into [`A(x, y)`, `B`, `C(a -> b)`].
137
+ */
138
+ function splitChainArrow(line) {
139
+ const segments = [];
140
+ let depth = 0;
141
+ let start = 0;
142
+ for (let i = 0; i < line.length; i++) {
143
+ const ch = line[i];
144
+ if (ch === '(') {
145
+ depth++;
146
+ continue;
147
+ }
148
+ if (ch === ')') {
149
+ depth--;
150
+ continue;
151
+ }
152
+ if (depth === 0 && i + 3 < line.length
153
+ && line[i] === ' ' && line[i + 1] === '-' && line[i + 2] === '>' && line[i + 3] === ' ') {
154
+ segments.push(line.slice(start, i));
155
+ start = i + 4; // skip ` -> `
156
+ i += 3;
157
+ }
158
+ }
159
+ segments.push(line.slice(start));
160
+ return segments;
161
+ }
162
+ // ---------------------------------------------------------------------------
163
+ // Node expression parser — single node token
164
+ // ---------------------------------------------------------------------------
165
+ function parseNodeExpression(expr, lineNum, warnings) {
166
+ let remaining = expr;
167
+ // Extract trailing `as Alias`
168
+ let alias;
169
+ const asMatch = remaining.match(/\s+as\s+(\w+)\s*$/);
170
+ if (asMatch) {
171
+ alias = asMatch[1];
172
+ remaining = remaining.slice(0, asMatch.index).trim();
173
+ }
174
+ // Check for condition pattern: `left operator right ? Branch`
175
+ const conditionMatch = remaining.match(/^(\S+)\s*(>|<|>=|<=|==|!=)\s*(\S+)\s*\?\s*Branch$/);
176
+ if (conditionMatch) {
177
+ return {
178
+ type: 'branch',
179
+ name: 'Branch',
180
+ condition: {
181
+ left: conditionMatch[1],
182
+ operator: conditionMatch[2],
183
+ right: conditionMatch[3],
184
+ },
185
+ alias,
186
+ };
187
+ }
188
+ // Check for Event prefix: `Event BeginPlay`
189
+ const eventMatch = remaining.match(/^Event\s+(\w+)$/);
190
+ if (eventMatch) {
191
+ return {
192
+ type: 'event',
193
+ name: eventMatch[1],
194
+ alias,
195
+ };
196
+ }
197
+ // Check for CastTo pattern: `CastTo(ClassName)`
198
+ const castMatch = remaining.match(/^CastTo\(([^)]+)\)$/);
199
+ if (castMatch) {
200
+ return {
201
+ type: 'cast',
202
+ name: 'CastTo',
203
+ castTo: castMatch[1].trim(),
204
+ alias,
205
+ };
206
+ }
207
+ // Check for Set variable: `Set VarName = value`
208
+ const setMatch = remaining.match(/^Set\s+(\w+)\s*=\s*(.+)$/);
209
+ if (setMatch) {
210
+ return {
211
+ type: 'variable_set',
212
+ name: setMatch[1],
213
+ args: { value: parseArgValue(setMatch[2].trim()) },
214
+ alias,
215
+ };
216
+ }
217
+ // Check for function call with target: `Target.FunctionName(args)` or `Target.FunctionName`
218
+ const targetCallMatch = remaining.match(/^(\w+)\.(\w+)(?:\(([^)]*)\))?$/);
219
+ if (targetCallMatch) {
220
+ const target = targetCallMatch[1];
221
+ const funcName = targetCallMatch[2];
222
+ const argsStr = targetCallMatch[3];
223
+ const node = {
224
+ type: 'call',
225
+ name: funcName,
226
+ target,
227
+ alias,
228
+ };
229
+ if (argsStr !== undefined && argsStr.trim().length > 0) {
230
+ node.args = parseArgs(argsStr, lineNum, warnings);
231
+ }
232
+ return node;
233
+ }
234
+ // Check for plain function call: `FunctionName(args)`
235
+ const callMatch = remaining.match(/^(\w+)\(([^)]*)\)$/);
236
+ if (callMatch) {
237
+ const funcName = callMatch[1];
238
+ const argsStr = callMatch[2];
239
+ const node = {
240
+ type: 'call',
241
+ name: funcName,
242
+ alias,
243
+ };
244
+ if (argsStr.trim().length > 0) {
245
+ node.args = parseArgs(argsStr, lineNum, warnings);
246
+ }
247
+ return node;
248
+ }
249
+ // Check for plain function call with no args: `FunctionName()` (already handled above via empty args)
250
+ // or plain identifier (variable get or parameterless function)
251
+ const identMatch = remaining.match(/^(\w+)$/);
252
+ if (identMatch) {
253
+ return {
254
+ type: 'variable_get',
255
+ name: identMatch[1],
256
+ alias,
257
+ };
258
+ }
259
+ // Unrecognized pattern
260
+ warnings.push(`Line ${lineNum}: unrecognized node expression: "${expr}"`);
261
+ return null;
262
+ }
263
+ // ---------------------------------------------------------------------------
264
+ // Argument parsing — `key=value, key=value` or positional `value, value`
265
+ // ---------------------------------------------------------------------------
266
+ function parseArgs(argsStr, lineNum, warnings) {
267
+ const result = {};
268
+ const parts = argsStr.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
269
+ for (let i = 0; i < parts.length; i++) {
270
+ const part = parts[i];
271
+ const eqIdx = part.indexOf('=');
272
+ if (eqIdx >= 0) {
273
+ const key = part.slice(0, eqIdx).trim();
274
+ const value = part.slice(eqIdx + 1).trim();
275
+ result[key] = parseArgValue(value);
276
+ }
277
+ else {
278
+ // Positional argument — use index as key
279
+ result[`arg${i}`] = parseArgValue(part);
280
+ }
281
+ }
282
+ return result;
283
+ }
284
+ function parseArgValue(raw) {
285
+ // Quoted string
286
+ if (raw.startsWith('"') && raw.endsWith('"')) {
287
+ return raw.slice(1, -1);
288
+ }
289
+ // Boolean
290
+ if (raw === 'true')
291
+ return true;
292
+ if (raw === 'false')
293
+ return false;
294
+ // Null
295
+ if (raw === 'null')
296
+ return null;
297
+ // Number
298
+ const num = Number(raw);
299
+ if (!isNaN(num) && raw.length > 0)
300
+ return num;
301
+ // Unquoted identifier / string
302
+ return raw;
303
+ }
304
+ // ---------------------------------------------------------------------------
305
+ // DSL-to-Payload Converter
306
+ // ---------------------------------------------------------------------------
307
+ export function blueprintDslToPayload(graphs) {
308
+ const functionGraphs = [];
309
+ for (const graph of graphs) {
310
+ const ctx = {
311
+ nodes: [],
312
+ connections: [],
313
+ aliasMap: new Map(),
314
+ nextId: 0,
315
+ };
316
+ for (const rootNode of graph.nodes) {
317
+ convertNodeTree(rootNode, ctx);
318
+ }
319
+ functionGraphs.push({
320
+ graphName: graph.graphName,
321
+ nodes: ctx.nodes,
322
+ connections: ctx.connections,
323
+ });
324
+ }
325
+ return { functionGraphs };
326
+ }
327
+ function allocTempId(ctx) {
328
+ return `n${ctx.nextId++}`;
329
+ }
330
+ /**
331
+ * Convert a DSL node (and its `then` / `branches` children) into payload nodes
332
+ * and connections. Returns the tempId of the created node.
333
+ */
334
+ function convertNodeTree(node, ctx) {
335
+ const tempId = allocTempId(ctx);
336
+ const payloadNode = buildPayloadNode(node, tempId, ctx);
337
+ ctx.nodes.push(payloadNode);
338
+ // Register alias
339
+ if (node.alias) {
340
+ ctx.aliasMap.set(node.alias, tempId);
341
+ }
342
+ // Process `then` chain (exec output -> next node)
343
+ // Connection is recorded first, then recurse — this keeps connections in
344
+ // declaration order (parent -> child before child -> grandchild).
345
+ if (node.then && node.then.length > 0) {
346
+ for (const nextNode of node.then) {
347
+ const nextId = allocTempId(ctx);
348
+ ctx.connections.push({
349
+ fromNode: tempId,
350
+ fromPin: 'then',
351
+ toNode: nextId,
352
+ toPin: 'execute',
353
+ });
354
+ convertNodeTreeWithId(nextNode, nextId, ctx);
355
+ }
356
+ }
357
+ // Process branches
358
+ if (node.branches) {
359
+ for (const [label, branchNodes] of Object.entries(node.branches)) {
360
+ const pinName = mapBranchLabelToPin(label);
361
+ for (const branchNode of branchNodes) {
362
+ const branchId = allocTempId(ctx);
363
+ ctx.connections.push({
364
+ fromNode: tempId,
365
+ fromPin: pinName,
366
+ toNode: branchId,
367
+ toPin: 'execute',
368
+ });
369
+ convertNodeTreeWithId(branchNode, branchId, ctx);
370
+ }
371
+ }
372
+ }
373
+ // Process condition — add comparison node and wire it to the branch
374
+ if (node.condition) {
375
+ const condNode = buildConditionNode(node.condition, ctx);
376
+ ctx.connections.push({
377
+ fromNode: condNode.tempId,
378
+ fromPin: 'ReturnValue',
379
+ toNode: tempId,
380
+ toPin: 'Condition',
381
+ });
382
+ }
383
+ return tempId;
384
+ }
385
+ /**
386
+ * Convert a DSL node using a pre-allocated tempId.
387
+ * Used when the caller has already reserved the id for connection ordering.
388
+ */
389
+ function convertNodeTreeWithId(node, tempId, ctx) {
390
+ const payloadNode = buildPayloadNode(node, tempId, ctx);
391
+ ctx.nodes.push(payloadNode);
392
+ if (node.alias) {
393
+ ctx.aliasMap.set(node.alias, tempId);
394
+ }
395
+ if (node.then && node.then.length > 0) {
396
+ for (const nextNode of node.then) {
397
+ const nextId = allocTempId(ctx);
398
+ ctx.connections.push({
399
+ fromNode: tempId,
400
+ fromPin: 'then',
401
+ toNode: nextId,
402
+ toPin: 'execute',
403
+ });
404
+ convertNodeTreeWithId(nextNode, nextId, ctx);
405
+ }
406
+ }
407
+ if (node.branches) {
408
+ for (const [label, branchNodes] of Object.entries(node.branches)) {
409
+ const pinName = mapBranchLabelToPin(label);
410
+ for (const branchNode of branchNodes) {
411
+ const branchId = allocTempId(ctx);
412
+ ctx.connections.push({
413
+ fromNode: tempId,
414
+ fromPin: pinName,
415
+ toNode: branchId,
416
+ toPin: 'execute',
417
+ });
418
+ convertNodeTreeWithId(branchNode, branchId, ctx);
419
+ }
420
+ }
421
+ }
422
+ if (node.condition) {
423
+ const condNode = buildConditionNode(node.condition, ctx);
424
+ ctx.connections.push({
425
+ fromNode: condNode.tempId,
426
+ fromPin: 'ReturnValue',
427
+ toNode: tempId,
428
+ toPin: 'Condition',
429
+ });
430
+ }
431
+ }
432
+ function buildPayloadNode(node, tempId, ctx) {
433
+ switch (node.type) {
434
+ case 'event':
435
+ return { nodeClass: 'K2Node_Event', tempId, eventName: node.name };
436
+ case 'call': {
437
+ const pn = {
438
+ nodeClass: 'K2Node_CallFunction',
439
+ tempId,
440
+ functionName: node.name,
441
+ };
442
+ if (node.target) {
443
+ // Resolve alias to tempId if known, otherwise pass through as target name
444
+ const resolvedTarget = ctx.aliasMap.get(node.target);
445
+ pn.target = resolvedTarget ?? node.target;
446
+ }
447
+ if (node.args) {
448
+ pn.args = node.args;
449
+ }
450
+ return pn;
451
+ }
452
+ case 'cast':
453
+ return { nodeClass: 'K2Node_DynamicCast', tempId, targetClass: node.castTo };
454
+ case 'branch':
455
+ return { nodeClass: 'K2Node_IfThenElse', tempId };
456
+ case 'variable_get':
457
+ return { nodeClass: 'K2Node_VariableGet', tempId, variableName: node.name };
458
+ case 'variable_set': {
459
+ const pn = { nodeClass: 'K2Node_VariableSet', tempId, variableName: node.name };
460
+ if (node.args) {
461
+ pn.value = node.args.value;
462
+ }
463
+ return pn;
464
+ }
465
+ case 'macro':
466
+ return { nodeClass: 'K2Node_MacroInstance', tempId, macroName: node.name };
467
+ case 'literal':
468
+ return { nodeClass: 'K2Node_Literal', tempId, value: node.name };
469
+ }
470
+ }
471
+ function buildConditionNode(condition, ctx) {
472
+ const tempId = allocTempId(ctx);
473
+ const node = {
474
+ nodeClass: 'K2Node_CallFunction',
475
+ tempId,
476
+ functionName: operatorToFunctionName(condition.operator),
477
+ args: { A: resolveConditionOperand(condition.left, ctx), B: resolveConditionOperand(condition.right, ctx) },
478
+ };
479
+ ctx.nodes.push(node);
480
+ return node;
481
+ }
482
+ function resolveConditionOperand(operand, ctx) {
483
+ // Check alias map
484
+ const resolved = ctx.aliasMap.get(operand);
485
+ if (resolved)
486
+ return resolved;
487
+ // Try as number
488
+ const num = Number(operand);
489
+ if (!isNaN(num) && operand.length > 0)
490
+ return num;
491
+ // Return as-is (variable or literal reference)
492
+ return operand;
493
+ }
494
+ function operatorToFunctionName(operator) {
495
+ switch (operator) {
496
+ case '>': return 'Greater';
497
+ case '<': return 'Less';
498
+ case '>=': return 'GreaterEqual';
499
+ case '<=': return 'LessEqual';
500
+ case '==': return 'EqualEqual';
501
+ case '!=': return 'NotEqual';
502
+ default: return operator;
503
+ }
504
+ }
505
+ function mapBranchLabelToPin(label) {
506
+ // Standard if-then-else branch labels
507
+ const map = {
508
+ True: 'True',
509
+ False: 'False',
510
+ Default: 'Default',
511
+ };
512
+ return map[label] ?? label;
513
+ }