@synergenius/flow-weaver 0.28.0 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,12 +46,66 @@ export function generateInPlace(sourceCode, ast, options = {}) {
46
46
  path.resolve(nodeType.sourceLocation.file) !== path.resolve(ast.sourceFile)) {
47
47
  continue;
48
48
  }
49
+ // Skip built-in auto-injected nodes — step 1.2 handles their insertion
50
+ if (!nodeType.sourceLocation && nodeType.helperText != null) {
51
+ continue;
52
+ }
49
53
  const nodeTypeResult = replaceNodeTypeJSDoc(result, nodeType);
50
54
  if (nodeTypeResult.changed) {
51
55
  result = nodeTypeResult.code;
52
56
  hasChanges = true;
53
57
  }
54
58
  }
59
+ // Step 1.2: Insert built-in node functions that are auto-injected (no source in the file)
60
+ const usedNodeTypes = new Set(ast.instances.map((i) => i.nodeType));
61
+ const builtInNodes = ast.nodeTypes.filter((nt) => !nt.sourceLocation && nt.functionText && nt.helperText != null && usedNodeTypes.has(nt.name));
62
+ if (builtInNodes.length > 0) {
63
+ // Find insertion point: just before the workflow function's JSDoc
64
+ const workflowFnPattern = new RegExp(`(/\\*\\*[\\s\\S]*?@flowWeaver\\s+workflow[\\s\\S]*?\\*/)\\s*\\n\\s*(?:export\\s+)?(?:async\\s+)?function\\s+${ast.functionName}\\b`);
65
+ const match = result.match(workflowFnPattern);
66
+ if (match && match.index !== undefined) {
67
+ const insertionLines = [];
68
+ // Emit shared helpers once (deduplicated)
69
+ const emittedHelpers = new Set();
70
+ for (const node of builtInNodes) {
71
+ const helperText = production ? (node.helperTextProduction ?? null) : (node.helperText ?? null);
72
+ if (helperText && !emittedHelpers.has(helperText)) {
73
+ emittedHelpers.add(helperText);
74
+ insertionLines.push(helperText);
75
+ insertionLines.push('');
76
+ }
77
+ }
78
+ // Emit each built-in function with its JSDoc
79
+ for (const node of builtInNodes) {
80
+ const funcText = (production && node.functionTextProduction != null) ? node.functionTextProduction : node.functionText;
81
+ if (funcText) {
82
+ // Add JSDoc annotation so the function is recognized on re-parse
83
+ const portAnnotations = [];
84
+ portAnnotations.push('/**');
85
+ portAnnotations.push(` * @flowWeaver nodeType`);
86
+ for (const [name, port] of Object.entries(node.inputs)) {
87
+ if (name === 'execute')
88
+ continue;
89
+ const optPrefix = port.optional ? '[' : '';
90
+ const optSuffix = port.optional ? ']' : '';
91
+ portAnnotations.push(` * @input ${optPrefix}${name}${optSuffix} - ${port.label || name}`);
92
+ }
93
+ for (const [name, port] of Object.entries(node.outputs)) {
94
+ if (name === 'onSuccess' || name === 'onFailure')
95
+ continue;
96
+ portAnnotations.push(` * @output ${name} - ${port.label || name}`);
97
+ }
98
+ portAnnotations.push(' */');
99
+ insertionLines.push(portAnnotations.join('\n'));
100
+ insertionLines.push(funcText);
101
+ insertionLines.push('');
102
+ }
103
+ }
104
+ const insertionCode = insertionLines.join('\n');
105
+ result = result.slice(0, match.index) + insertionCode + '\n' + result.slice(match.index);
106
+ hasChanges = true;
107
+ }
108
+ }
55
109
  // Step 1.5: Remove orphaned nodeType functions (functions that don't match any AST nodeType)
56
110
  // When multi-workflow, consider ALL workflows' node types to avoid deleting types used by siblings
57
111
  const cleanupResult = removeOrphanedNodeTypeFunctions(result, ast, allWorkflows);
@@ -126,7 +126,9 @@ export function generateCode(ast, options) {
126
126
  // 2. relative file imports (sourceLocation file differs from workflow source)
127
127
  // 3. local nodes (same source file)
128
128
  const npmPackageNodes = ast.nodeTypes.filter((n) => n.importSource);
129
- const localNodes = ast.nodeTypes.filter((n) => !n.importSource && n.sourceLocation?.file === ast.sourceFile);
129
+ // Only include built-in nodes (no sourceLocation) that are actually used by this workflow
130
+ const referencedNodeTypes = new Set(ast.instances.map((i) => i.nodeType));
131
+ const localNodes = ast.nodeTypes.filter((n) => !n.importSource && (n.sourceLocation?.file === ast.sourceFile || (!n.sourceLocation && n.functionText && n.helperText != null && referencedNodeTypes.has(n.name))));
130
132
  const importedNodes = ast.nodeTypes.filter((n) => !n.importSource && n.sourceLocation?.file !== ast.sourceFile);
131
133
  if (importedNodes.length > 0 || npmPackageNodes.length > 0) {
132
134
  // Separate FUNCTION imports (from source) vs IMPORTED_WORKFLOW imports (from generated)
@@ -264,10 +266,26 @@ export function generateCode(ast, options) {
264
266
  if (inlineFunctions.length > 0) {
265
267
  lines.push('');
266
268
  addLine();
269
+ // Emit shared helper functions once (deduplicated across built-in nodes)
270
+ // In production mode, use helperTextProduction if set; if undefined, skip helpers entirely
271
+ // (production built-in nodes don't need mock helpers)
272
+ const emittedHelpers = new Set();
273
+ inlineFunctions.forEach((node) => {
274
+ if (node.importSource)
275
+ return;
276
+ const helperText = production ? (node.helperTextProduction ?? null) : (node.helperText ?? null);
277
+ if (helperText && !emittedHelpers.has(helperText)) {
278
+ emittedHelpers.add(helperText);
279
+ lines.push(helperText);
280
+ helperText.split('\n').forEach(() => addLine());
281
+ lines.push('');
282
+ addLine();
283
+ }
284
+ });
267
285
  inlineFunctions.forEach((node) => {
268
286
  if (node.importSource)
269
287
  return; // Never inline npm package functions
270
- const functionText = node.functionText;
288
+ const functionText = (production && node.functionTextProduction != null) ? node.functionTextProduction : node.functionText;
271
289
  if (functionText) {
272
290
  const functionWithoutDecorators = removeDecorators(functionText);
273
291
  // Add source mapping for the node function
package/dist/api/parse.js CHANGED
@@ -32,6 +32,7 @@ export async function parseWorkflow(filePath, options) {
32
32
  // Parse the file to extract nodes and workflows
33
33
  const parsed = parser.parse(filePath);
34
34
  warnings.push(...parsed.warnings);
35
+ errors.push(...parsed.errors);
35
36
  // Get available workflow names
36
37
  availableWorkflows = parsed.workflows.map((w) => w.functionName);
37
38
  // Determine which workflow to use
@@ -82,7 +83,7 @@ export async function parseWorkflow(filePath, options) {
82
83
  }
83
84
  return {
84
85
  ast: workflow,
85
- errors: [],
86
+ errors,
86
87
  warnings,
87
88
  availableWorkflows,
88
89
  allWorkflows: parsed.workflows,
@@ -259,8 +259,14 @@ export type TNodeTypeAST = {
259
259
  y?: number;
260
260
  /** Source code location */
261
261
  sourceLocation?: TSourceLocation;
262
- /** Original function source text */
262
+ /** Original function source text (main function only) */
263
263
  functionText?: string;
264
+ /** Production-optimized function source text (mock code stripped) */
265
+ functionTextProduction?: string;
266
+ /** Shared helper functions (mock helpers, utility functions) emitted once before main functions */
267
+ helperText?: string;
268
+ /** Production-optimized helper text */
269
+ helperTextProduction?: string;
264
270
  /** How success/failure branching works */
265
271
  branchingStrategy?: TBranchingStrategy;
266
272
  /** Field name for value-based branching */
@@ -0,0 +1,9 @@
1
+ /**
2
+ * DO NOT EDIT - generated by scripts/generate-built-in-registry.ts
3
+ *
4
+ * Registry of built-in node types for auto-injection into the parser.
5
+ * Generated from the actual source files in src/built-in-nodes/.
6
+ */
7
+ import type { TNodeTypeAST } from '../ast/types.js';
8
+ export declare const BUILT_IN_NODE_TYPES: TNodeTypeAST[];
9
+ //# sourceMappingURL=generated-registry.d.ts.map
@@ -0,0 +1,299 @@
1
+ /**
2
+ * DO NOT EDIT - generated by scripts/generate-built-in-registry.ts
3
+ *
4
+ * Registry of built-in node types for auto-injection into the parser.
5
+ * Generated from the actual source files in src/built-in-nodes/.
6
+ */
7
+ export const BUILT_IN_NODE_TYPES = [
8
+ {
9
+ type: 'NodeType',
10
+ name: 'delay',
11
+ functionName: 'delay',
12
+ isAsync: true,
13
+ hasSuccessPort: true,
14
+ hasFailurePort: true,
15
+ executeWhen: 'CONJUNCTION',
16
+ variant: 'FUNCTION',
17
+ inputs: {
18
+ execute: { dataType: 'STEP', label: 'Execute' },
19
+ duration: { dataType: 'STRING', label: 'Duration to sleep (e.g. "30s", "5m", "1h", "2d")', tsType: 'string' },
20
+ },
21
+ outputs: {
22
+ onSuccess: { dataType: 'STEP', label: 'On Success', isControlFlow: true },
23
+ onFailure: { dataType: 'STEP', label: 'On Failure', isControlFlow: true, failure: true },
24
+ elapsed: { dataType: 'BOOLEAN', label: 'Always true after sleep completes', tsType: 'boolean' },
25
+ },
26
+ helperText: `
27
+ function __fw_getMockConfig() {
28
+ return globalThis.__fw_mocks__;
29
+ }
30
+
31
+ function __fw_lookupMock(section, key) {
32
+ if (!section)
33
+ return undefined;
34
+ const nodeId = globalThis.__fw_current_node_id__;
35
+ if (nodeId) {
36
+ const qualified = section[\`\${nodeId}:\${key}\`];
37
+ if (qualified !== undefined)
38
+ return qualified;
39
+ }
40
+ return section[key];
41
+ }
42
+ `.trim(),
43
+ helperTextProduction: undefined,
44
+ functionText: `
45
+ async function delay(execute, duration) {
46
+ if (!execute)
47
+ return { onSuccess: false, onFailure: false, elapsed: false };
48
+ const mocks = __fw_getMockConfig();
49
+ if (mocks?.fast) {
50
+ await new Promise((resolve) => setTimeout(resolve, 1));
51
+ }
52
+ else {
53
+ const ms = __fw_parseDuration(duration);
54
+ await new Promise((resolve) => setTimeout(resolve, ms));
55
+ }
56
+ return { onSuccess: true, onFailure: false, elapsed: true };
57
+ }
58
+ function __fw_parseDuration(duration) {
59
+ const match = duration.match(/^(\\d+)(ms|s|m|h|d)$/);
60
+ if (!match)
61
+ return 0;
62
+ const [, value, unit] = match;
63
+ const multipliers = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
64
+ return parseInt(value) * (multipliers[unit] || 0);
65
+ }
66
+ `.trim(),
67
+ functionTextProduction: `
68
+ async function delay(execute, duration) {
69
+ if (!execute)
70
+ return { onSuccess: false, onFailure: false, elapsed: false };
71
+ const ms = __fw_parseDuration(duration);
72
+ await new Promise((resolve) => setTimeout(resolve, ms));
73
+ return { onSuccess: true, onFailure: false, elapsed: true };
74
+ }
75
+ function __fw_parseDuration(duration) {
76
+ const match = duration.match(/^(\\d+)(ms|s|m|h|d)$/);
77
+ if (!match)
78
+ return 0;
79
+ const [, value, unit] = match;
80
+ const multipliers = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
81
+ return parseInt(value) * (multipliers[unit] || 0);
82
+ }
83
+ `.trim(),
84
+ },
85
+ {
86
+ type: 'NodeType',
87
+ name: 'waitForEvent',
88
+ functionName: 'waitForEvent',
89
+ isAsync: true,
90
+ hasSuccessPort: true,
91
+ hasFailurePort: true,
92
+ executeWhen: 'CONJUNCTION',
93
+ variant: 'FUNCTION',
94
+ inputs: {
95
+ execute: { dataType: 'STEP', label: 'Execute' },
96
+ eventName: { dataType: 'STRING', label: 'Event name to wait for (e.g. "app/approval.received")', tsType: 'string' },
97
+ match: { dataType: 'STRING', label: 'Field to match between trigger and waited event (e.g. "data.requestId")', tsType: 'string', optional: true },
98
+ timeout: { dataType: 'STRING', label: 'Max wait time (e.g. "24h", "7d"). Empty = no timeout', tsType: 'string', optional: true },
99
+ },
100
+ outputs: {
101
+ onSuccess: { dataType: 'STEP', label: 'On Success', isControlFlow: true },
102
+ onFailure: { dataType: 'STEP', label: 'On Failure', isControlFlow: true, failure: true },
103
+ eventData: { dataType: 'OBJECT', label: 'The received event\'s data payload', tsType: 'object' },
104
+ },
105
+ helperText: `
106
+ function __fw_getMockConfig() {
107
+ return globalThis.__fw_mocks__;
108
+ }
109
+
110
+ function __fw_lookupMock(section, key) {
111
+ if (!section)
112
+ return undefined;
113
+ const nodeId = globalThis.__fw_current_node_id__;
114
+ if (nodeId) {
115
+ const qualified = section[\`\${nodeId}:\${key}\`];
116
+ if (qualified !== undefined)
117
+ return qualified;
118
+ }
119
+ return section[key];
120
+ }
121
+ `.trim(),
122
+ helperTextProduction: undefined,
123
+ functionText: `
124
+ async function waitForEvent(execute, eventName, match, timeout) {
125
+ if (!execute)
126
+ return { onSuccess: false, onFailure: false, eventData: {} };
127
+ const mocks = __fw_getMockConfig();
128
+ if (mocks) {
129
+ const mockData = __fw_lookupMock(mocks.events, eventName);
130
+ if (mockData !== undefined) {
131
+ return { onSuccess: true, onFailure: false, eventData: mockData };
132
+ }
133
+ return { onSuccess: false, onFailure: true, eventData: {} };
134
+ }
135
+ return { onSuccess: true, onFailure: false, eventData: {} };
136
+ }
137
+ `.trim(),
138
+ functionTextProduction: `
139
+ async function waitForEvent(execute, eventName, match, timeout) {
140
+ if (!execute)
141
+ return { onSuccess: false, onFailure: false, eventData: {} };
142
+ return { onSuccess: true, onFailure: false, eventData: {} };
143
+ }
144
+ `.trim(),
145
+ },
146
+ {
147
+ type: 'NodeType',
148
+ name: 'invokeWorkflow',
149
+ functionName: 'invokeWorkflow',
150
+ isAsync: true,
151
+ hasSuccessPort: true,
152
+ hasFailurePort: true,
153
+ executeWhen: 'CONJUNCTION',
154
+ variant: 'FUNCTION',
155
+ inputs: {
156
+ execute: { dataType: 'STEP', label: 'Execute' },
157
+ functionId: { dataType: 'STRING', label: 'Function ID of the workflow to invoke (e.g. "my-service/sub-workflow")', tsType: 'string' },
158
+ payload: { dataType: 'OBJECT', label: 'Data to pass as event.data to the invoked function', tsType: 'object' },
159
+ timeout: { dataType: 'STRING', label: 'Max wait time (e.g. "1h")', tsType: 'string', optional: true },
160
+ },
161
+ outputs: {
162
+ onSuccess: { dataType: 'STEP', label: 'On Success', isControlFlow: true },
163
+ onFailure: { dataType: 'STEP', label: 'On Failure', isControlFlow: true, failure: true },
164
+ result: { dataType: 'OBJECT', label: 'Return value from the invoked function', tsType: 'object' },
165
+ },
166
+ helperText: `
167
+ function __fw_getMockConfig() {
168
+ return globalThis.__fw_mocks__;
169
+ }
170
+
171
+ function __fw_lookupMock(section, key) {
172
+ if (!section)
173
+ return undefined;
174
+ const nodeId = globalThis.__fw_current_node_id__;
175
+ if (nodeId) {
176
+ const qualified = section[\`\${nodeId}:\${key}\`];
177
+ if (qualified !== undefined)
178
+ return qualified;
179
+ }
180
+ return section[key];
181
+ }
182
+ `.trim(),
183
+ helperTextProduction: undefined,
184
+ functionText: `
185
+ async function invokeWorkflow(execute, functionId, payload, timeout) {
186
+ if (!execute)
187
+ return { onSuccess: false, onFailure: false, result: {} };
188
+ const mocks = __fw_getMockConfig();
189
+ if (mocks) {
190
+ const mockResult = __fw_lookupMock(mocks.invocations, functionId);
191
+ if (mockResult !== undefined) {
192
+ return { onSuccess: true, onFailure: false, result: mockResult };
193
+ }
194
+ return { onSuccess: false, onFailure: true, result: {} };
195
+ }
196
+ const registry = globalThis.__fw_workflow_registry__;
197
+ if (registry?.[functionId]) {
198
+ try {
199
+ const result = await registry[functionId](true, payload);
200
+ return { onSuccess: true, onFailure: false, result: result ?? {} };
201
+ }
202
+ catch {
203
+ return { onSuccess: false, onFailure: true, result: {} };
204
+ }
205
+ }
206
+ return { onSuccess: true, onFailure: false, result: {} };
207
+ }
208
+ `.trim(),
209
+ functionTextProduction: `
210
+ async function invokeWorkflow(execute, functionId, payload, timeout) {
211
+ if (!execute)
212
+ return { onSuccess: false, onFailure: false, result: {} };
213
+ const registry = globalThis.__fw_workflow_registry__;
214
+ if (registry?.[functionId]) {
215
+ try {
216
+ const result = await registry[functionId](true, payload);
217
+ return { onSuccess: true, onFailure: false, result: result ?? {} };
218
+ }
219
+ catch {
220
+ return { onSuccess: false, onFailure: true, result: {} };
221
+ }
222
+ }
223
+ return { onSuccess: true, onFailure: false, result: {} };
224
+ }
225
+ `.trim(),
226
+ },
227
+ {
228
+ type: 'NodeType',
229
+ name: 'waitForAgent',
230
+ functionName: 'waitForAgent',
231
+ isAsync: true,
232
+ hasSuccessPort: true,
233
+ hasFailurePort: true,
234
+ executeWhen: 'CONJUNCTION',
235
+ variant: 'FUNCTION',
236
+ inputs: {
237
+ execute: { dataType: 'STEP', label: 'Execute' },
238
+ agentId: { dataType: 'STRING', label: 'Agent/task identifier', tsType: 'string' },
239
+ context: { dataType: 'OBJECT', label: 'Context data to send to the agent', tsType: 'object' },
240
+ prompt: { dataType: 'STRING', label: 'Message to display when requesting input', tsType: 'string', optional: true },
241
+ },
242
+ outputs: {
243
+ onSuccess: { dataType: 'STEP', label: 'On Success', isControlFlow: true },
244
+ onFailure: { dataType: 'STEP', label: 'On Failure', isControlFlow: true, failure: true },
245
+ agentResult: { dataType: 'OBJECT', label: 'Result returned by the agent', tsType: 'object' },
246
+ },
247
+ helperText: `
248
+ function __fw_getMockConfig() {
249
+ return globalThis.__fw_mocks__;
250
+ }
251
+
252
+ function __fw_lookupMock(section, key) {
253
+ if (!section)
254
+ return undefined;
255
+ const nodeId = globalThis.__fw_current_node_id__;
256
+ if (nodeId) {
257
+ const qualified = section[\`\${nodeId}:\${key}\`];
258
+ if (qualified !== undefined)
259
+ return qualified;
260
+ }
261
+ return section[key];
262
+ }
263
+ `.trim(),
264
+ helperTextProduction: undefined,
265
+ functionText: `
266
+ async function waitForAgent(execute, agentId, context, prompt) {
267
+ if (!execute)
268
+ return { onSuccess: false, onFailure: false, agentResult: {} };
269
+ const mocks = __fw_getMockConfig();
270
+ const mockResult = __fw_lookupMock(mocks?.agents, agentId);
271
+ if (mockResult !== undefined) {
272
+ return { onSuccess: true, onFailure: false, agentResult: mockResult };
273
+ }
274
+ if (mocks?.agents) {
275
+ return { onSuccess: false, onFailure: true, agentResult: {} };
276
+ }
277
+ const channel = globalThis.__fw_agent_channel__;
278
+ if (channel) {
279
+ const result = await channel.request({ agentId, context, prompt });
280
+ return { onSuccess: true, onFailure: false, agentResult: result };
281
+ }
282
+ return { onSuccess: true, onFailure: false, agentResult: {} };
283
+ }
284
+ `.trim(),
285
+ functionTextProduction: `
286
+ async function waitForAgent(execute, agentId, context, prompt) {
287
+ if (!execute)
288
+ return { onSuccess: false, onFailure: false, agentResult: {} };
289
+ const channel = globalThis.__fw_agent_channel__;
290
+ if (channel) {
291
+ const result = await channel.request({ agentId, context, prompt });
292
+ return { onSuccess: true, onFailure: false, agentResult: result };
293
+ }
294
+ return { onSuccess: true, onFailure: false, agentResult: {} };
295
+ }
296
+ `.trim(),
297
+ },
298
+ ];
299
+ //# sourceMappingURL=generated-registry.js.map
@@ -65,14 +65,13 @@ class NodeParser extends CstParser {
65
65
  this.CONSUME(LabelPrefix);
66
66
  this.CONSUME(StringLiteral, { LABEL: 'labelValue' });
67
67
  });
68
- // expr: port="value", port2="value2"
68
+ // expr: port="value", port2="value2" (comma optional between assignments)
69
69
  exprAttr = this.RULE('exprAttr', () => {
70
70
  this.CONSUME(ExprPrefix);
71
- this.AT_LEAST_ONE_SEP({
72
- SEP: Comma,
73
- DEF: () => {
74
- this.SUBRULE(this.exprAssignment);
75
- },
71
+ this.SUBRULE(this.exprAssignment);
72
+ this.MANY(() => {
73
+ this.OPTION(() => this.CONSUME(Comma));
74
+ this.SUBRULE2(this.exprAssignment);
76
75
  });
77
76
  });
78
77
  // port="value"
@@ -524,6 +523,10 @@ const visitorInstance = new NodeVisitor();
524
523
  export function parseNodeLine(input, warnings) {
525
524
  const lexResult = JSDocLexer.tokenize(input);
526
525
  if (lexResult.errors.length > 0) {
526
+ const truncatedInput = input.length > 60 ? input.substring(0, 60) + '...' : input;
527
+ warnings.push(`Failed to tokenize node line: "${truncatedInput}"\n` +
528
+ ` Error: ${lexResult.errors[0].message}\n` +
529
+ ` Expected format: @node instanceId NodeType`);
527
530
  return null;
528
531
  }
529
532
  // Check if starts with @node
@@ -5987,7 +5987,7 @@ var VERSION;
5987
5987
  var init_generated_version = __esm({
5988
5988
  "src/generated-version.ts"() {
5989
5989
  "use strict";
5990
- VERSION = "0.28.0";
5990
+ VERSION = "0.29.1";
5991
5991
  }
5992
5992
  });
5993
5993
 
@@ -6651,7 +6651,7 @@ function generateScopeFunctionClosure(scopeName, parentNodeId, parentNodeType, w
6651
6651
  lines.push(` // Execute: ${child.id} (${child.nodeType})`);
6652
6652
  if (emitDebugHooks) {
6653
6653
  const awaitHook = isAsync2 ? "await " : "";
6654
- lines.push(` let ${safeChildId}Idx: number;`);
6654
+ lines.push(` let ${safeChildId}Idx: number = -1;`);
6655
6655
  lines.push(` if (${awaitHook}__ctrl__.beforeNode('${child.id}', scopedCtx)) {`);
6656
6656
  childIndent = " ";
6657
6657
  }
@@ -14039,8 +14039,9 @@ function generateCode(ast, options) {
14039
14039
  }
14040
14040
  }
14041
14041
  const npmPackageNodes = ast.nodeTypes.filter((n7) => n7.importSource);
14042
+ const referencedNodeTypes = new Set(ast.instances.map((i) => i.nodeType));
14042
14043
  const localNodes = ast.nodeTypes.filter(
14043
- (n7) => !n7.importSource && n7.sourceLocation?.file === ast.sourceFile
14044
+ (n7) => !n7.importSource && (n7.sourceLocation?.file === ast.sourceFile || !n7.sourceLocation && n7.functionText && n7.helperText != null && referencedNodeTypes.has(n7.name))
14044
14045
  );
14045
14046
  const importedNodes = ast.nodeTypes.filter(
14046
14047
  (n7) => !n7.importSource && n7.sourceLocation?.file !== ast.sourceFile
@@ -14151,9 +14152,21 @@ function generateCode(ast, options) {
14151
14152
  if (inlineFunctions.length > 0) {
14152
14153
  lines.push("");
14153
14154
  addLine();
14155
+ const emittedHelpers = /* @__PURE__ */ new Set();
14154
14156
  inlineFunctions.forEach((node) => {
14155
14157
  if (node.importSource) return;
14156
- const functionText = node.functionText;
14158
+ const helperText = production ? node.helperTextProduction ?? null : node.helperText ?? null;
14159
+ if (helperText && !emittedHelpers.has(helperText)) {
14160
+ emittedHelpers.add(helperText);
14161
+ lines.push(helperText);
14162
+ helperText.split("\n").forEach(() => addLine());
14163
+ lines.push("");
14164
+ addLine();
14165
+ }
14166
+ });
14167
+ inlineFunctions.forEach((node) => {
14168
+ if (node.importSource) return;
14169
+ const functionText = production && node.functionTextProduction != null ? node.functionTextProduction : node.functionText;
14157
14170
  if (functionText) {
14158
14171
  const functionWithoutDecorators = removeDecorators(functionText);
14159
14172
  if (node.sourceLocation) {
@@ -15275,12 +15288,62 @@ function generateInPlace(sourceCode, ast, options = {}) {
15275
15288
  if (nodeType.sourceLocation?.file && path2.resolve(nodeType.sourceLocation.file) !== path2.resolve(ast.sourceFile)) {
15276
15289
  continue;
15277
15290
  }
15291
+ if (!nodeType.sourceLocation && nodeType.helperText != null) {
15292
+ continue;
15293
+ }
15278
15294
  const nodeTypeResult = replaceNodeTypeJSDoc(result, nodeType);
15279
15295
  if (nodeTypeResult.changed) {
15280
15296
  result = nodeTypeResult.code;
15281
15297
  hasChanges = true;
15282
15298
  }
15283
15299
  }
15300
+ const usedNodeTypes = new Set(ast.instances.map((i) => i.nodeType));
15301
+ const builtInNodes = ast.nodeTypes.filter(
15302
+ (nt2) => !nt2.sourceLocation && nt2.functionText && nt2.helperText != null && usedNodeTypes.has(nt2.name)
15303
+ );
15304
+ if (builtInNodes.length > 0) {
15305
+ const workflowFnPattern = new RegExp(
15306
+ `(/\\*\\*[\\s\\S]*?@flowWeaver\\s+workflow[\\s\\S]*?\\*/)\\s*\\n\\s*(?:export\\s+)?(?:async\\s+)?function\\s+${ast.functionName}\\b`
15307
+ );
15308
+ const match = result.match(workflowFnPattern);
15309
+ if (match && match.index !== void 0) {
15310
+ const insertionLines = [];
15311
+ const emittedHelpers = /* @__PURE__ */ new Set();
15312
+ for (const node of builtInNodes) {
15313
+ const helperText = production ? node.helperTextProduction ?? null : node.helperText ?? null;
15314
+ if (helperText && !emittedHelpers.has(helperText)) {
15315
+ emittedHelpers.add(helperText);
15316
+ insertionLines.push(helperText);
15317
+ insertionLines.push("");
15318
+ }
15319
+ }
15320
+ for (const node of builtInNodes) {
15321
+ const funcText = production && node.functionTextProduction != null ? node.functionTextProduction : node.functionText;
15322
+ if (funcText) {
15323
+ const portAnnotations = [];
15324
+ portAnnotations.push("/**");
15325
+ portAnnotations.push(` * @flowWeaver nodeType`);
15326
+ for (const [name, port] of Object.entries(node.inputs)) {
15327
+ if (name === "execute") continue;
15328
+ const optPrefix = port.optional ? "[" : "";
15329
+ const optSuffix = port.optional ? "]" : "";
15330
+ portAnnotations.push(` * @input ${optPrefix}${name}${optSuffix} - ${port.label || name}`);
15331
+ }
15332
+ for (const [name, port] of Object.entries(node.outputs)) {
15333
+ if (name === "onSuccess" || name === "onFailure") continue;
15334
+ portAnnotations.push(` * @output ${name} - ${port.label || name}`);
15335
+ }
15336
+ portAnnotations.push(" */");
15337
+ insertionLines.push(portAnnotations.join("\n"));
15338
+ insertionLines.push(funcText);
15339
+ insertionLines.push("");
15340
+ }
15341
+ }
15342
+ const insertionCode = insertionLines.join("\n");
15343
+ result = result.slice(0, match.index) + insertionCode + "\n" + result.slice(match.index);
15344
+ hasChanges = true;
15345
+ }
15346
+ }
15284
15347
  const cleanupResult = removeOrphanedNodeTypeFunctions(result, ast, allWorkflows);
15285
15348
  if (cleanupResult.changed) {
15286
15349
  result = cleanupResult.code;
@@ -24302,6 +24365,12 @@ var init_port_parser = __esm({
24302
24365
  function parseNodeLine(input, warnings) {
24303
24366
  const lexResult = JSDocLexer.tokenize(input);
24304
24367
  if (lexResult.errors.length > 0) {
24368
+ const truncatedInput = input.length > 60 ? input.substring(0, 60) + "..." : input;
24369
+ warnings.push(
24370
+ `Failed to tokenize node line: "${truncatedInput}"
24371
+ Error: ${lexResult.errors[0].message}
24372
+ Expected format: @node instanceId NodeType`
24373
+ );
24305
24374
  return null;
24306
24375
  }
24307
24376
  if (lexResult.tokens.length === 0) {
@@ -24389,14 +24458,13 @@ var init_node_parser = __esm({
24389
24458
  this.CONSUME(LabelPrefix);
24390
24459
  this.CONSUME(StringLiteral, { LABEL: "labelValue" });
24391
24460
  });
24392
- // expr: port="value", port2="value2"
24461
+ // expr: port="value", port2="value2" (comma optional between assignments)
24393
24462
  exprAttr = this.RULE("exprAttr", () => {
24394
24463
  this.CONSUME(ExprPrefix);
24395
- this.AT_LEAST_ONE_SEP({
24396
- SEP: Comma,
24397
- DEF: () => {
24398
- this.SUBRULE(this.exprAssignment);
24399
- }
24464
+ this.SUBRULE(this.exprAssignment);
24465
+ this.MANY(() => {
24466
+ this.OPTION(() => this.CONSUME(Comma));
24467
+ this.SUBRULE2(this.exprAssignment);
24400
24468
  });
24401
24469
  });
24402
24470
  // port="value"
@@ -27260,6 +27328,306 @@ var init_coercion_types = __esm({
27260
27328
  }
27261
27329
  });
27262
27330
 
27331
+ // src/built-in-nodes/generated-registry.ts
27332
+ var BUILT_IN_NODE_TYPES;
27333
+ var init_generated_registry = __esm({
27334
+ "src/built-in-nodes/generated-registry.ts"() {
27335
+ "use strict";
27336
+ BUILT_IN_NODE_TYPES = [
27337
+ {
27338
+ type: "NodeType",
27339
+ name: "delay",
27340
+ functionName: "delay",
27341
+ isAsync: true,
27342
+ hasSuccessPort: true,
27343
+ hasFailurePort: true,
27344
+ executeWhen: "CONJUNCTION",
27345
+ variant: "FUNCTION",
27346
+ inputs: {
27347
+ execute: { dataType: "STEP", label: "Execute" },
27348
+ duration: { dataType: "STRING", label: 'Duration to sleep (e.g. "30s", "5m", "1h", "2d")', tsType: "string" }
27349
+ },
27350
+ outputs: {
27351
+ onSuccess: { dataType: "STEP", label: "On Success", isControlFlow: true },
27352
+ onFailure: { dataType: "STEP", label: "On Failure", isControlFlow: true, failure: true },
27353
+ elapsed: { dataType: "BOOLEAN", label: "Always true after sleep completes", tsType: "boolean" }
27354
+ },
27355
+ helperText: `
27356
+ function __fw_getMockConfig() {
27357
+ return globalThis.__fw_mocks__;
27358
+ }
27359
+
27360
+ function __fw_lookupMock(section, key) {
27361
+ if (!section)
27362
+ return undefined;
27363
+ const nodeId = globalThis.__fw_current_node_id__;
27364
+ if (nodeId) {
27365
+ const qualified = section[\`\${nodeId}:\${key}\`];
27366
+ if (qualified !== undefined)
27367
+ return qualified;
27368
+ }
27369
+ return section[key];
27370
+ }
27371
+ `.trim(),
27372
+ helperTextProduction: void 0,
27373
+ functionText: `
27374
+ async function delay(execute, duration) {
27375
+ if (!execute)
27376
+ return { onSuccess: false, onFailure: false, elapsed: false };
27377
+ const mocks = __fw_getMockConfig();
27378
+ if (mocks?.fast) {
27379
+ await new Promise((resolve) => setTimeout(resolve, 1));
27380
+ }
27381
+ else {
27382
+ const ms = __fw_parseDuration(duration);
27383
+ await new Promise((resolve) => setTimeout(resolve, ms));
27384
+ }
27385
+ return { onSuccess: true, onFailure: false, elapsed: true };
27386
+ }
27387
+ function __fw_parseDuration(duration) {
27388
+ const match = duration.match(/^(\\d+)(ms|s|m|h|d)$/);
27389
+ if (!match)
27390
+ return 0;
27391
+ const [, value, unit] = match;
27392
+ const multipliers = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
27393
+ return parseInt(value) * (multipliers[unit] || 0);
27394
+ }
27395
+ `.trim(),
27396
+ functionTextProduction: `
27397
+ async function delay(execute, duration) {
27398
+ if (!execute)
27399
+ return { onSuccess: false, onFailure: false, elapsed: false };
27400
+ const ms = __fw_parseDuration(duration);
27401
+ await new Promise((resolve) => setTimeout(resolve, ms));
27402
+ return { onSuccess: true, onFailure: false, elapsed: true };
27403
+ }
27404
+ function __fw_parseDuration(duration) {
27405
+ const match = duration.match(/^(\\d+)(ms|s|m|h|d)$/);
27406
+ if (!match)
27407
+ return 0;
27408
+ const [, value, unit] = match;
27409
+ const multipliers = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
27410
+ return parseInt(value) * (multipliers[unit] || 0);
27411
+ }
27412
+ `.trim()
27413
+ },
27414
+ {
27415
+ type: "NodeType",
27416
+ name: "waitForEvent",
27417
+ functionName: "waitForEvent",
27418
+ isAsync: true,
27419
+ hasSuccessPort: true,
27420
+ hasFailurePort: true,
27421
+ executeWhen: "CONJUNCTION",
27422
+ variant: "FUNCTION",
27423
+ inputs: {
27424
+ execute: { dataType: "STEP", label: "Execute" },
27425
+ eventName: { dataType: "STRING", label: 'Event name to wait for (e.g. "app/approval.received")', tsType: "string" },
27426
+ match: { dataType: "STRING", label: 'Field to match between trigger and waited event (e.g. "data.requestId")', tsType: "string", optional: true },
27427
+ timeout: { dataType: "STRING", label: 'Max wait time (e.g. "24h", "7d"). Empty = no timeout', tsType: "string", optional: true }
27428
+ },
27429
+ outputs: {
27430
+ onSuccess: { dataType: "STEP", label: "On Success", isControlFlow: true },
27431
+ onFailure: { dataType: "STEP", label: "On Failure", isControlFlow: true, failure: true },
27432
+ eventData: { dataType: "OBJECT", label: "The received event's data payload", tsType: "object" }
27433
+ },
27434
+ helperText: `
27435
+ function __fw_getMockConfig() {
27436
+ return globalThis.__fw_mocks__;
27437
+ }
27438
+
27439
+ function __fw_lookupMock(section, key) {
27440
+ if (!section)
27441
+ return undefined;
27442
+ const nodeId = globalThis.__fw_current_node_id__;
27443
+ if (nodeId) {
27444
+ const qualified = section[\`\${nodeId}:\${key}\`];
27445
+ if (qualified !== undefined)
27446
+ return qualified;
27447
+ }
27448
+ return section[key];
27449
+ }
27450
+ `.trim(),
27451
+ helperTextProduction: void 0,
27452
+ functionText: `
27453
+ async function waitForEvent(execute, eventName, match, timeout) {
27454
+ if (!execute)
27455
+ return { onSuccess: false, onFailure: false, eventData: {} };
27456
+ const mocks = __fw_getMockConfig();
27457
+ if (mocks) {
27458
+ const mockData = __fw_lookupMock(mocks.events, eventName);
27459
+ if (mockData !== undefined) {
27460
+ return { onSuccess: true, onFailure: false, eventData: mockData };
27461
+ }
27462
+ return { onSuccess: false, onFailure: true, eventData: {} };
27463
+ }
27464
+ return { onSuccess: true, onFailure: false, eventData: {} };
27465
+ }
27466
+ `.trim(),
27467
+ functionTextProduction: `
27468
+ async function waitForEvent(execute, eventName, match, timeout) {
27469
+ if (!execute)
27470
+ return { onSuccess: false, onFailure: false, eventData: {} };
27471
+ return { onSuccess: true, onFailure: false, eventData: {} };
27472
+ }
27473
+ `.trim()
27474
+ },
27475
+ {
27476
+ type: "NodeType",
27477
+ name: "invokeWorkflow",
27478
+ functionName: "invokeWorkflow",
27479
+ isAsync: true,
27480
+ hasSuccessPort: true,
27481
+ hasFailurePort: true,
27482
+ executeWhen: "CONJUNCTION",
27483
+ variant: "FUNCTION",
27484
+ inputs: {
27485
+ execute: { dataType: "STEP", label: "Execute" },
27486
+ functionId: { dataType: "STRING", label: 'Function ID of the workflow to invoke (e.g. "my-service/sub-workflow")', tsType: "string" },
27487
+ payload: { dataType: "OBJECT", label: "Data to pass as event.data to the invoked function", tsType: "object" },
27488
+ timeout: { dataType: "STRING", label: 'Max wait time (e.g. "1h")', tsType: "string", optional: true }
27489
+ },
27490
+ outputs: {
27491
+ onSuccess: { dataType: "STEP", label: "On Success", isControlFlow: true },
27492
+ onFailure: { dataType: "STEP", label: "On Failure", isControlFlow: true, failure: true },
27493
+ result: { dataType: "OBJECT", label: "Return value from the invoked function", tsType: "object" }
27494
+ },
27495
+ helperText: `
27496
+ function __fw_getMockConfig() {
27497
+ return globalThis.__fw_mocks__;
27498
+ }
27499
+
27500
+ function __fw_lookupMock(section, key) {
27501
+ if (!section)
27502
+ return undefined;
27503
+ const nodeId = globalThis.__fw_current_node_id__;
27504
+ if (nodeId) {
27505
+ const qualified = section[\`\${nodeId}:\${key}\`];
27506
+ if (qualified !== undefined)
27507
+ return qualified;
27508
+ }
27509
+ return section[key];
27510
+ }
27511
+ `.trim(),
27512
+ helperTextProduction: void 0,
27513
+ functionText: `
27514
+ async function invokeWorkflow(execute, functionId, payload, timeout) {
27515
+ if (!execute)
27516
+ return { onSuccess: false, onFailure: false, result: {} };
27517
+ const mocks = __fw_getMockConfig();
27518
+ if (mocks) {
27519
+ const mockResult = __fw_lookupMock(mocks.invocations, functionId);
27520
+ if (mockResult !== undefined) {
27521
+ return { onSuccess: true, onFailure: false, result: mockResult };
27522
+ }
27523
+ return { onSuccess: false, onFailure: true, result: {} };
27524
+ }
27525
+ const registry = globalThis.__fw_workflow_registry__;
27526
+ if (registry?.[functionId]) {
27527
+ try {
27528
+ const result = await registry[functionId](true, payload);
27529
+ return { onSuccess: true, onFailure: false, result: result ?? {} };
27530
+ }
27531
+ catch {
27532
+ return { onSuccess: false, onFailure: true, result: {} };
27533
+ }
27534
+ }
27535
+ return { onSuccess: true, onFailure: false, result: {} };
27536
+ }
27537
+ `.trim(),
27538
+ functionTextProduction: `
27539
+ async function invokeWorkflow(execute, functionId, payload, timeout) {
27540
+ if (!execute)
27541
+ return { onSuccess: false, onFailure: false, result: {} };
27542
+ const registry = globalThis.__fw_workflow_registry__;
27543
+ if (registry?.[functionId]) {
27544
+ try {
27545
+ const result = await registry[functionId](true, payload);
27546
+ return { onSuccess: true, onFailure: false, result: result ?? {} };
27547
+ }
27548
+ catch {
27549
+ return { onSuccess: false, onFailure: true, result: {} };
27550
+ }
27551
+ }
27552
+ return { onSuccess: true, onFailure: false, result: {} };
27553
+ }
27554
+ `.trim()
27555
+ },
27556
+ {
27557
+ type: "NodeType",
27558
+ name: "waitForAgent",
27559
+ functionName: "waitForAgent",
27560
+ isAsync: true,
27561
+ hasSuccessPort: true,
27562
+ hasFailurePort: true,
27563
+ executeWhen: "CONJUNCTION",
27564
+ variant: "FUNCTION",
27565
+ inputs: {
27566
+ execute: { dataType: "STEP", label: "Execute" },
27567
+ agentId: { dataType: "STRING", label: "Agent/task identifier", tsType: "string" },
27568
+ context: { dataType: "OBJECT", label: "Context data to send to the agent", tsType: "object" },
27569
+ prompt: { dataType: "STRING", label: "Message to display when requesting input", tsType: "string", optional: true }
27570
+ },
27571
+ outputs: {
27572
+ onSuccess: { dataType: "STEP", label: "On Success", isControlFlow: true },
27573
+ onFailure: { dataType: "STEP", label: "On Failure", isControlFlow: true, failure: true },
27574
+ agentResult: { dataType: "OBJECT", label: "Result returned by the agent", tsType: "object" }
27575
+ },
27576
+ helperText: `
27577
+ function __fw_getMockConfig() {
27578
+ return globalThis.__fw_mocks__;
27579
+ }
27580
+
27581
+ function __fw_lookupMock(section, key) {
27582
+ if (!section)
27583
+ return undefined;
27584
+ const nodeId = globalThis.__fw_current_node_id__;
27585
+ if (nodeId) {
27586
+ const qualified = section[\`\${nodeId}:\${key}\`];
27587
+ if (qualified !== undefined)
27588
+ return qualified;
27589
+ }
27590
+ return section[key];
27591
+ }
27592
+ `.trim(),
27593
+ helperTextProduction: void 0,
27594
+ functionText: `
27595
+ async function waitForAgent(execute, agentId, context, prompt) {
27596
+ if (!execute)
27597
+ return { onSuccess: false, onFailure: false, agentResult: {} };
27598
+ const mocks = __fw_getMockConfig();
27599
+ const mockResult = __fw_lookupMock(mocks?.agents, agentId);
27600
+ if (mockResult !== undefined) {
27601
+ return { onSuccess: true, onFailure: false, agentResult: mockResult };
27602
+ }
27603
+ if (mocks?.agents) {
27604
+ return { onSuccess: false, onFailure: true, agentResult: {} };
27605
+ }
27606
+ const channel = globalThis.__fw_agent_channel__;
27607
+ if (channel) {
27608
+ const result = await channel.request({ agentId, context, prompt });
27609
+ return { onSuccess: true, onFailure: false, agentResult: result };
27610
+ }
27611
+ return { onSuccess: true, onFailure: false, agentResult: {} };
27612
+ }
27613
+ `.trim(),
27614
+ functionTextProduction: `
27615
+ async function waitForAgent(execute, agentId, context, prompt) {
27616
+ if (!execute)
27617
+ return { onSuccess: false, onFailure: false, agentResult: {} };
27618
+ const channel = globalThis.__fw_agent_channel__;
27619
+ if (channel) {
27620
+ const result = await channel.request({ agentId, context, prompt });
27621
+ return { onSuccess: true, onFailure: false, agentResult: result };
27622
+ }
27623
+ return { onSuccess: true, onFailure: false, agentResult: {} };
27624
+ }
27625
+ `.trim()
27626
+ }
27627
+ ];
27628
+ }
27629
+ });
27630
+
27263
27631
  // src/parser/tag-registry.ts
27264
27632
  var TagHandlerRegistry, tagHandlerRegistry;
27265
27633
  var init_tag_registry = __esm({
@@ -27588,6 +27956,7 @@ var init_parser2 = __esm({
27588
27956
  init_shared_project();
27589
27957
  init_lru_cache();
27590
27958
  init_coercion_types();
27959
+ init_generated_registry();
27591
27960
  init_tag_registry();
27592
27961
  AnnotationParser = class {
27593
27962
  project;
@@ -27731,7 +28100,7 @@ var init_parser2 = __esm({
27731
28100
  }
27732
28101
  }
27733
28102
  }
27734
- const inferredNodeTypes = this.inferNodeTypesFromUnannotated(sourceFile, nodeTypes);
28103
+ const inferredNodeTypes = this.inferNodeTypesFromUnannotated(sourceFile, nodeTypes, localNodeTypes, warnings);
27735
28104
  nodeTypes.push(...inferredNodeTypes);
27736
28105
  const workflows = this.extractWorkflows(sourceFile, nodeTypes, filePath, errors2, warnings);
27737
28106
  const patterns = this.extractPatterns(sourceFile, nodeTypes, filePath, errors2, warnings);
@@ -27771,7 +28140,7 @@ var init_parser2 = __esm({
27771
28140
  const workflowSignatures = this.extractWorkflowSignatures(sourceFile, virtualPath, warnings);
27772
28141
  const sameFileWorkflowNodeTypes = workflowSignatures.map((wf) => this.workflowToNodeType(wf));
27773
28142
  const nodeTypes = [...localNodeTypes, ...sameFileWorkflowNodeTypes];
27774
- const inferredNodeTypes = this.inferNodeTypesFromUnannotated(sourceFile, nodeTypes);
28143
+ const inferredNodeTypes = this.inferNodeTypesFromUnannotated(sourceFile, nodeTypes, localNodeTypes, warnings);
27775
28144
  nodeTypes.push(...inferredNodeTypes);
27776
28145
  const workflows = this.extractWorkflows(sourceFile, nodeTypes, virtualPath, errors2, warnings);
27777
28146
  const patterns = this.extractPatterns(sourceFile, nodeTypes, virtualPath, errors2, warnings);
@@ -28003,7 +28372,7 @@ var init_parser2 = __esm({
28003
28372
  if (imp.importSource.startsWith(".")) {
28004
28373
  return this.resolveLocalImportAnnotation(imp, currentDir, warnings);
28005
28374
  } else {
28006
- return this.resolveNpmImportAnnotation(imp, currentDir);
28375
+ return this.resolveNpmImportAnnotation(imp, currentDir, warnings);
28007
28376
  }
28008
28377
  }
28009
28378
  /**
@@ -28048,7 +28417,7 @@ var init_parser2 = __esm({
28048
28417
  /**
28049
28418
  * Resolve an npm package @fwImport to a node type by reading .d.ts declarations.
28050
28419
  */
28051
- resolveNpmImportAnnotation(imp, currentDir) {
28420
+ resolveNpmImportAnnotation(imp, currentDir, warnings) {
28052
28421
  const cacheKey = `npm:${imp.importSource}`;
28053
28422
  if (this.importCache.has(cacheKey)) {
28054
28423
  const cached2 = this.importCache.get(cacheKey);
@@ -28072,6 +28441,9 @@ var init_parser2 = __esm({
28072
28441
  }
28073
28442
  const dtsPath = resolvePackageTypesPath(imp.importSource, currentDir);
28074
28443
  if (!dtsPath) {
28444
+ warnings.push(
28445
+ `@fwImport: Package "${imp.importSource}" has no type declarations (.d.ts). Install @types/${imp.importSource} or add a local wrapper with @flowWeaver nodeType annotations.`
28446
+ );
28075
28447
  return this.createImportStub(imp);
28076
28448
  }
28077
28449
  try {
@@ -28101,7 +28473,13 @@ var init_parser2 = __esm({
28101
28473
  if (found) {
28102
28474
  return { ...found, name: imp.name, importSource: imp.importSource };
28103
28475
  }
28104
- } catch {
28476
+ warnings.push(
28477
+ `@fwImport: Function "${imp.functionName}" not found in type declarations for "${imp.importSource}". Available exports: ${allNodeTypes.map((nt2) => nt2.functionName).join(", ") || "(none)"}.`
28478
+ );
28479
+ } catch (err) {
28480
+ warnings.push(
28481
+ `@fwImport: Failed to parse type declarations for "${imp.importSource}": ${err.message}. Node "${imp.name}" will use a generic stub.`
28482
+ );
28105
28483
  }
28106
28484
  return this.createImportStub(imp);
28107
28485
  }
@@ -28400,6 +28778,18 @@ ${fn.getText()}` : fn.getText();
28400
28778
  const importedNpmNodeTypes = (config2.imports || []).map(
28401
28779
  (imp) => this.resolveImportAnnotation(imp, filePath, warnings)
28402
28780
  );
28781
+ for (const nt2 of importedNpmNodeTypes) {
28782
+ const dataInputs = Object.keys(nt2.inputs).filter((p) => p !== "execute");
28783
+ const nonControlOutputs = Object.keys(nt2.outputs).filter(
28784
+ (p) => p !== "onSuccess" && p !== "onFailure"
28785
+ );
28786
+ const isStubOnly = nonControlOutputs.length === 1 && nonControlOutputs[0] === "result" && nt2.outputs.result?.dataType === "ANY";
28787
+ if (dataInputs.length === 0 && isStubOnly) {
28788
+ warnings.push(
28789
+ `Could not infer ports for "${nt2.functionName}" from "${nt2.importSource}". Wrap it in a local function with @flowWeaver nodeType annotations instead.`
28790
+ );
28791
+ }
28792
+ }
28403
28793
  const allAvailableNodeTypes = [...availableNodeTypes, ...importedNpmNodeTypes];
28404
28794
  const instances = (config2.instances || []).map((inst) => {
28405
28795
  const nodeTypeExists = allAvailableNodeTypes.some(
@@ -28750,7 +29140,7 @@ ${fn.getText()}` : fn.getText();
28750
29140
  * nodeType annotation, we infer an expression node type from its TypeScript
28751
29141
  * signature. Phase 1: same-file functions only.
28752
29142
  */
28753
- inferNodeTypesFromUnannotated(sourceFile, existingNodeTypes) {
29143
+ inferNodeTypesFromUnannotated(sourceFile, existingNodeTypes, localNodeTypes, warnings) {
28754
29144
  const allFunctions = extractFunctionLikes(sourceFile);
28755
29145
  const referencedTypes = /* @__PURE__ */ new Set();
28756
29146
  for (const fn of allFunctions) {
@@ -28774,8 +29164,24 @@ ${fn.getText()}` : fn.getText();
28774
29164
  if (unresolvedTypes.size === 0) return [];
28775
29165
  const inferredNodeTypes = [];
28776
29166
  const alreadyInferred = /* @__PURE__ */ new Set();
29167
+ const builtInByName = new Map(BUILT_IN_NODE_TYPES.map((nt2) => [nt2.name, nt2]));
29168
+ const annotatedNames = new Set(localNodeTypes.map((nt2) => nt2.functionName));
28777
29169
  for (const unresolvedType of unresolvedTypes) {
28778
29170
  if (alreadyInferred.has(unresolvedType)) continue;
29171
+ const builtIn = builtInByName.get(unresolvedType);
29172
+ if (builtIn) {
29173
+ inferredNodeTypes.push(builtIn);
29174
+ alreadyInferred.add(unresolvedType);
29175
+ if (!annotatedNames.has(unresolvedType)) {
29176
+ const shadowFn = allFunctions.find((fn) => fn.getName() === unresolvedType && !this.hasFlowWeaverAnnotation(fn));
29177
+ if (shadowFn) {
29178
+ warnings.push(
29179
+ `Function '${unresolvedType}' exists in this file but is not annotated with @flowWeaver nodeType. The built-in '${unresolvedType}' will be used instead. Add @flowWeaver nodeType to use your version.`
29180
+ );
29181
+ }
29182
+ }
29183
+ continue;
29184
+ }
28779
29185
  const matchedFn = allFunctions.find((fn) => {
28780
29186
  if (fn.getName() !== unresolvedType) return false;
28781
29187
  return !this.hasFlowWeaverAnnotation(fn);
@@ -29674,6 +30080,7 @@ async function parseWorkflow(filePath, options) {
29674
30080
  }
29675
30081
  const parsed = parser.parse(filePath);
29676
30082
  warnings.push(...parsed.warnings);
30083
+ errors2.push(...parsed.errors);
29677
30084
  availableWorkflows = parsed.workflows.map((w) => w.functionName);
29678
30085
  let workflowName = options?.workflowName;
29679
30086
  if (!workflowName) {
@@ -29723,7 +30130,7 @@ async function parseWorkflow(filePath, options) {
29723
30130
  }
29724
30131
  return {
29725
30132
  ast: workflow,
29726
- errors: [],
30133
+ errors: errors2,
29727
30134
  warnings,
29728
30135
  availableWorkflows,
29729
30136
  allWorkflows: parsed.workflows
@@ -88526,7 +88933,7 @@ function parseIntStrict(value) {
88526
88933
  // src/cli/index.ts
88527
88934
  init_logger();
88528
88935
  init_error_utils();
88529
- var version2 = true ? "0.28.0" : "0.0.0-dev";
88936
+ var version2 = true ? "0.29.1" : "0.0.0-dev";
88530
88937
  var program2 = new Command();
88531
88938
  program2.name("fw").description("Flow Weaver Annotations - Compile and validate workflow files").option("-v, --version", "Output the current version").option("--no-color", "Disable colors").option("--color", "Force colors").on("option:version", () => {
88532
88939
  logger.banner(version2);
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.28.0";
1
+ export declare const VERSION = "0.29.1";
2
2
  //# sourceMappingURL=generated-version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by scripts/generate-version.ts — do not edit manually
2
- export const VERSION = '0.28.0';
2
+ export const VERSION = '0.29.1';
3
3
  //# sourceMappingURL=generated-version.js.map
@@ -196,8 +196,9 @@ export function generateScopeFunctionClosure(scopeName, parentNodeId, parentNode
196
196
  // When enabled, wraps the child execution so breakpoints can pause on scoped nodes.
197
197
  if (emitDebugHooks) {
198
198
  const awaitHook = isAsync ? 'await ' : '';
199
- // Hoist Idx declaration before the if block so it stays in scope after
200
- lines.push(` let ${safeChildId}Idx: number;`);
199
+ // Hoist Idx declaration before the if block so it stays in scope after.
200
+ // Initialize to -1 so getVariable can still read checkpointed values when beforeNode skips.
201
+ lines.push(` let ${safeChildId}Idx: number = -1;`);
201
202
  lines.push(` if (${awaitHook}__ctrl__.beforeNode('${child.id}', scopedCtx)) {`);
202
203
  childIndent = ' ';
203
204
  }
package/dist/parser.js CHANGED
@@ -14,6 +14,7 @@ import { getPackageExports } from './npm-packages.js';
14
14
  import { getSharedProject } from './shared-project.js';
15
15
  import { LRUCache } from './utils/lru-cache.js';
16
16
  import { COERCION_NODE_TYPES, COERCE_TYPE_MAP } from './built-in-nodes/coercion-types.js';
17
+ import { BUILT_IN_NODE_TYPES } from './built-in-nodes/generated-registry.js';
17
18
  import { tagHandlerRegistry } from './parser/tag-registry.js';
18
19
  /**
19
20
  * Convert a TExternalNodeType to a TNodeTypeAST with sensible defaults.
@@ -233,8 +234,9 @@ export class AnnotationParser {
233
234
  }
234
235
  }
235
236
  }
236
- // Auto-infer node types from unannotated functions referenced by @node
237
- const inferredNodeTypes = this.inferNodeTypesFromUnannotated(sourceFile, nodeTypes);
237
+ // Auto-infer node types from unannotated functions referenced by @node,
238
+ // and lazily inject built-in nodes (delay, waitForEvent, etc.) when referenced
239
+ const inferredNodeTypes = this.inferNodeTypesFromUnannotated(sourceFile, nodeTypes, localNodeTypes, warnings);
238
240
  nodeTypes.push(...inferredNodeTypes);
239
241
  const workflows = this.extractWorkflows(sourceFile, nodeTypes, filePath, errors, warnings);
240
242
  const patterns = this.extractPatterns(sourceFile, nodeTypes, filePath, errors, warnings);
@@ -282,8 +284,9 @@ export class AnnotationParser {
282
284
  const workflowSignatures = this.extractWorkflowSignatures(sourceFile, virtualPath, warnings);
283
285
  const sameFileWorkflowNodeTypes = workflowSignatures.map((wf) => this.workflowToNodeType(wf));
284
286
  const nodeTypes = [...localNodeTypes, ...sameFileWorkflowNodeTypes];
285
- // Auto-infer node types from unannotated functions referenced by @node
286
- const inferredNodeTypes = this.inferNodeTypesFromUnannotated(sourceFile, nodeTypes);
287
+ // Auto-infer node types from unannotated functions referenced by @node,
288
+ // and lazily inject built-in nodes (delay, waitForEvent, etc.) when referenced
289
+ const inferredNodeTypes = this.inferNodeTypesFromUnannotated(sourceFile, nodeTypes, localNodeTypes, warnings);
287
290
  nodeTypes.push(...inferredNodeTypes);
288
291
  // Note: imports not supported for virtual files - would need filesystem access
289
292
  const workflows = this.extractWorkflows(sourceFile, nodeTypes, virtualPath, errors, warnings);
@@ -555,7 +558,7 @@ export class AnnotationParser {
555
558
  }
556
559
  else {
557
560
  // npm package import - use .d.ts inference
558
- return this.resolveNpmImportAnnotation(imp, currentDir);
561
+ return this.resolveNpmImportAnnotation(imp, currentDir, warnings);
559
562
  }
560
563
  }
561
564
  /**
@@ -609,7 +612,7 @@ export class AnnotationParser {
609
612
  /**
610
613
  * Resolve an npm package @fwImport to a node type by reading .d.ts declarations.
611
614
  */
612
- resolveNpmImportAnnotation(imp, currentDir) {
615
+ resolveNpmImportAnnotation(imp, currentDir, warnings) {
613
616
  // Check cache (with mtime validation — same pattern as resolveNpmImports)
614
617
  const cacheKey = `npm:${imp.importSource}`;
615
618
  if (this.importCache.has(cacheKey)) {
@@ -637,6 +640,8 @@ export class AnnotationParser {
637
640
  // Resolve .d.ts path
638
641
  const dtsPath = resolvePackageTypesPath(imp.importSource, currentDir);
639
642
  if (!dtsPath) {
643
+ warnings.push(`@fwImport: Package "${imp.importSource}" has no type declarations (.d.ts). ` +
644
+ `Install @types/${imp.importSource} or add a local wrapper with @flowWeaver nodeType annotations.`);
640
645
  return this.createImportStub(imp);
641
646
  }
642
647
  try {
@@ -667,9 +672,12 @@ export class AnnotationParser {
667
672
  if (found) {
668
673
  return { ...found, name: imp.name, importSource: imp.importSource };
669
674
  }
675
+ // Function not found in .d.ts
676
+ warnings.push(`@fwImport: Function "${imp.functionName}" not found in type declarations for "${imp.importSource}". ` +
677
+ `Available exports: ${allNodeTypes.map((nt) => nt.functionName).join(', ') || '(none)'}.`);
670
678
  }
671
- catch {
672
- // Silently skip packages whose .d.ts can't be parsed
679
+ catch (err) {
680
+ warnings.push(`@fwImport: Failed to parse type declarations for "${imp.importSource}": ${err.message}. Node "${imp.name}" will use a generic stub.`);
673
681
  }
674
682
  return this.createImportStub(imp);
675
683
  }
@@ -982,6 +990,22 @@ export class AnnotationParser {
982
990
  // These are persisted in JSDoc so they survive file re-parsing
983
991
  // Uses the same inference logic as TS imports for consistency
984
992
  const importedNpmNodeTypes = (config.imports || []).map((imp) => this.resolveImportAnnotation(imp, filePath, warnings));
993
+ // Post-resolution check: warn when inferred node type has zero data ports
994
+ // (excluding control-flow ports). This usually means the .d.ts inference
995
+ // couldn't extract meaningful port info and a local wrapper is needed.
996
+ for (const nt of importedNpmNodeTypes) {
997
+ const dataInputs = Object.keys(nt.inputs).filter((p) => p !== 'execute');
998
+ const nonControlOutputs = Object.keys(nt.outputs).filter((p) => p !== 'onSuccess' && p !== 'onFailure');
999
+ // Stub fallback has only result: ANY — if that's all we have with zero
1000
+ // data inputs, inference likely failed
1001
+ const isStubOnly = nonControlOutputs.length === 1 &&
1002
+ nonControlOutputs[0] === 'result' &&
1003
+ nt.outputs.result?.dataType === 'ANY';
1004
+ if (dataInputs.length === 0 && isStubOnly) {
1005
+ warnings.push(`Could not infer ports for "${nt.functionName}" from "${nt.importSource}". ` +
1006
+ `Wrap it in a local function with @flowWeaver nodeType annotations instead.`);
1007
+ }
1008
+ }
985
1009
  // Combine available node types with imported npm types for validation
986
1010
  const allAvailableNodeTypes = [...availableNodeTypes, ...importedNpmNodeTypes];
987
1011
  // Convert instances to NodeInstanceAST
@@ -1350,7 +1374,7 @@ export class AnnotationParser {
1350
1374
  * nodeType annotation, we infer an expression node type from its TypeScript
1351
1375
  * signature. Phase 1: same-file functions only.
1352
1376
  */
1353
- inferNodeTypesFromUnannotated(sourceFile, existingNodeTypes) {
1377
+ inferNodeTypesFromUnannotated(sourceFile, existingNodeTypes, localNodeTypes, warnings) {
1354
1378
  const allFunctions = extractFunctionLikes(sourceFile);
1355
1379
  // 1. Pre-scan workflows for @node references to collect referenced type names
1356
1380
  const referencedTypes = new Set();
@@ -1376,17 +1400,33 @@ export class AnnotationParser {
1376
1400
  }
1377
1401
  if (unresolvedTypes.size === 0)
1378
1402
  return [];
1379
- // 3. Match unresolved types to unannotated functions in the same file
1403
+ // 3. Match unresolved types to unannotated functions OR built-in nodes
1380
1404
  const inferredNodeTypes = [];
1381
1405
  const alreadyInferred = new Set();
1406
+ const builtInByName = new Map(BUILT_IN_NODE_TYPES.map((nt) => [nt.name, nt]));
1407
+ const annotatedNames = new Set(localNodeTypes.map((nt) => nt.functionName));
1382
1408
  for (const unresolvedType of unresolvedTypes) {
1383
1409
  if (alreadyInferred.has(unresolvedType))
1384
1410
  continue;
1385
- // Find matching function
1411
+ // 3a. Check built-in nodes first
1412
+ const builtIn = builtInByName.get(unresolvedType);
1413
+ if (builtIn) {
1414
+ inferredNodeTypes.push(builtIn);
1415
+ alreadyInferred.add(unresolvedType);
1416
+ // Warn if a local unannotated function has the same name (it will be shadowed)
1417
+ if (!annotatedNames.has(unresolvedType)) {
1418
+ const shadowFn = allFunctions.find((fn) => fn.getName() === unresolvedType && !this.hasFlowWeaverAnnotation(fn));
1419
+ if (shadowFn) {
1420
+ warnings.push(`Function '${unresolvedType}' exists in this file but is not annotated with @flowWeaver nodeType. ` +
1421
+ `The built-in '${unresolvedType}' will be used instead. Add @flowWeaver nodeType to use your version.`);
1422
+ }
1423
+ }
1424
+ continue;
1425
+ }
1426
+ // 3b. Match unannotated local function
1386
1427
  const matchedFn = allFunctions.find((fn) => {
1387
1428
  if (fn.getName() !== unresolvedType)
1388
1429
  return false;
1389
- // Must NOT have a valid @flowWeaver annotation
1390
1430
  return !this.hasFlowWeaverAnnotation(fn);
1391
1431
  });
1392
1432
  if (!matchedFn)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synergenius/flow-weaver",
3
- "version": "0.28.0",
3
+ "version": "0.29.1",
4
4
  "description": "Flow Weaver: deterministic TypeScript workflow compiler. Define workflows with JSDoc annotations, compile to standalone functions with zero runtime dependencies.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -113,7 +113,9 @@
113
113
  "LICENSE"
114
114
  ],
115
115
  "scripts": {
116
- "prebuild": "tsx scripts/generate-version.ts",
116
+ "prebuild": "tsx scripts/generate-version.ts && tsx scripts/generate-built-in-registry.ts",
117
+ "generate:registry": "tsx scripts/generate-built-in-registry.ts",
118
+ "generate:registry:check": "tsx scripts/generate-built-in-registry.ts --check",
117
119
  "build": "rimraf dist .tsbuildinfo && tsc -p tsconfig.build.json && npm run build:cli",
118
120
  "postbuild": "npx tsx scripts/postbuild.ts && npm run generate:docs",
119
121
  "generate:docs": "tsx scripts/generate-docs.ts",