@synergenius/flow-weaver 0.27.5 → 0.29.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.
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.27.5";
1
+ export declare const VERSION = "0.29.0";
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.27.5';
2
+ export const VERSION = '0.29.0';
3
3
  //# sourceMappingURL=generated-version.js.map
@@ -187,18 +187,35 @@ export function generateScopeFunctionClosure(scopeName, parentNodeId, parentNode
187
187
  }
188
188
  const safeChildId = toValidIdentifier(child.id);
189
189
  const awaitPrefix = isAsync ? 'await ' : '';
190
+ const emitDebugHooks = !production;
191
+ // Indentation increases when debug hooks wrap the child block
192
+ let childIndent = ' ';
190
193
  lines.push(``);
191
194
  lines.push(` // Execute: ${child.id} (${child.nodeType})`);
192
- lines.push(` scopedCtx.checkAborted('${child.id}');`);
193
- lines.push(` const ${safeChildId}Idx = scopedCtx.addExecution('${child.id}');`);
194
- lines.push(` if (typeof globalThis !== 'undefined') (globalThis as unknown as { __fw_current_node_id__?: string }).__fw_current_node_id__ = '${child.id}';`);
195
- lines.push(` ${awaitPrefix}scopedCtx.sendStatusChangedEvent({`);
196
- lines.push(` nodeTypeName: '${child.nodeType}',`);
197
- lines.push(` id: '${child.id}',`);
198
- lines.push(` executionIndex: ${safeChildId}Idx,`);
199
- lines.push(` status: 'RUNNING',`);
200
- lines.push(` });`);
201
- lines.push(` try {`);
195
+ // Debug controller: beforeNode hook for scoped children
196
+ // When enabled, wraps the child execution so breakpoints can pause on scoped nodes.
197
+ if (emitDebugHooks) {
198
+ const awaitHook = isAsync ? 'await ' : '';
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;`);
202
+ lines.push(` if (${awaitHook}__ctrl__.beforeNode('${child.id}', scopedCtx)) {`);
203
+ childIndent = ' ';
204
+ }
205
+ lines.push(`${childIndent}scopedCtx.checkAborted('${child.id}');`);
206
+ // Use assignment when debug hooks hoist the declaration, const otherwise
207
+ const idxDecl = emitDebugHooks ? '' : 'const ';
208
+ lines.push(`${childIndent}${idxDecl}${safeChildId}Idx = scopedCtx.addExecution('${child.id}');`);
209
+ lines.push(`${childIndent}if (typeof globalThis !== 'undefined') (globalThis as unknown as { __fw_current_node_id__?: string }).__fw_current_node_id__ = '${child.id}';`);
210
+ lines.push(`${childIndent}${awaitPrefix}scopedCtx.sendStatusChangedEvent({`);
211
+ lines.push(`${childIndent} nodeTypeName: '${child.nodeType}',`);
212
+ lines.push(`${childIndent} id: '${child.id}',`);
213
+ lines.push(`${childIndent} executionIndex: ${safeChildId}Idx,`);
214
+ lines.push(`${childIndent} status: 'RUNNING',`);
215
+ lines.push(`${childIndent}});`);
216
+ lines.push(`${childIndent}try {`);
217
+ // Inner indentation: inside try block (childIndent + 2 spaces for try body)
218
+ const tryIndent = `${childIndent} `;
202
219
  // Pre-handle connections from parent scoped OUTPUT ports with correct index variables
203
220
  const argLines = [];
204
221
  const getCall = isAsync ? 'await scopedCtx.getVariable' : 'scopedCtx.getVariable';
@@ -218,9 +235,9 @@ export function generateScopeFunctionClosure(scopeName, parentNodeId, parentNode
218
235
  const portType = targetPortDef
219
236
  ? mapToTypeScript(targetPortDef.dataType, targetPortDef.tsType)
220
237
  : 'unknown';
221
- argLines.push(` const ${varName} = ${getCall}({ id: '${parentNodeId}', portName: '${conn.from.port}', executionIndex: ${scopeParamIdxVar} }) as ${portType};`);
238
+ argLines.push(`${tryIndent}const ${varName} = ${getCall}({ id: '${parentNodeId}', portName: '${conn.from.port}', executionIndex: ${scopeParamIdxVar} }) as ${portType};`);
222
239
  // Emit VARIABLE_SET for the child's INPUT port so breakpoints and inspection work
223
- argLines.push(` ${childSetCall}({ id: '${child.id}', portName: '${targetPort}', executionIndex: ${safeChildId}Idx, nodeTypeName: '${child.nodeType}' }, ${varName});`);
240
+ argLines.push(`${tryIndent}${childSetCall}({ id: '${child.id}', portName: '${targetPort}', executionIndex: ${safeChildId}Idx, nodeTypeName: '${child.nodeType}' }, ${varName});`);
224
241
  preHandledPorts.add(targetPort);
225
242
  }
226
243
  });
@@ -230,7 +247,7 @@ export function generateScopeFunctionClosure(scopeName, parentNodeId, parentNode
230
247
  workflow: scopeWorkflow,
231
248
  id: child.id,
232
249
  lines: argLines,
233
- indent: ` `,
250
+ indent: tryIndent,
234
251
  getCall,
235
252
  isAsync,
236
253
  instanceParent: child.parent ? `${child.parent.id}.${child.parent.scope}` : undefined,
@@ -245,11 +262,11 @@ export function generateScopeFunctionClosure(scopeName, parentNodeId, parentNode
245
262
  // Call the child node function
246
263
  if (childNodeType.expression) {
247
264
  // Expression nodes use original signature (positional args, no execute)
248
- lines.push(` const ${safeChildId}Result = ${awaitPrefix}${child.nodeType}(${args.join(', ')});`);
265
+ lines.push(`${tryIndent}const ${safeChildId}Result = ${awaitPrefix}${child.nodeType}(${args.join(', ')});`);
249
266
  }
250
267
  else {
251
268
  // Regular node call with positional arguments
252
- lines.push(` const ${safeChildId}Result = ${awaitPrefix}${child.nodeType}(${args.join(', ')});`);
269
+ lines.push(`${tryIndent}const ${safeChildId}Result = ${awaitPrefix}${child.nodeType}(${args.join(', ')});`);
253
270
  }
254
271
  // Store outputs (including onSuccess/onFailure for debugging)
255
272
  // Expression nodes don't return onSuccess/onFailure — hardcode them
@@ -258,48 +275,57 @@ export function generateScopeFunctionClosure(scopeName, parentNodeId, parentNode
258
275
  const portDef = childNodeType.outputs[outPort];
259
276
  if (portDef.failure || isFailurePort(outPort)) {
260
277
  // Failure ports always false on success (expression nodes always succeed)
261
- lines.push(` ${childSetCall}({ id: '${child.id}', portName: '${outPort}', executionIndex: ${safeChildId}Idx, nodeTypeName: '${child.nodeType}' }, false);`);
278
+ lines.push(`${tryIndent}${childSetCall}({ id: '${child.id}', portName: '${outPort}', executionIndex: ${safeChildId}Idx, nodeTypeName: '${child.nodeType}' }, false);`);
262
279
  }
263
280
  else if (portDef.isControlFlow || isSuccessPort(outPort)) {
264
281
  // Success control flow ports always true (expression nodes always succeed)
265
- lines.push(` ${childSetCall}({ id: '${child.id}', portName: '${outPort}', executionIndex: ${safeChildId}Idx, nodeTypeName: '${child.nodeType}' }, true);`);
282
+ lines.push(`${tryIndent}${childSetCall}({ id: '${child.id}', portName: '${outPort}', executionIndex: ${safeChildId}Idx, nodeTypeName: '${child.nodeType}' }, true);`);
266
283
  }
267
284
  else {
268
285
  // Data outputs read from result object
269
- lines.push(` ${childSetCall}({ id: '${child.id}', portName: '${outPort}', executionIndex: ${safeChildId}Idx, nodeTypeName: '${child.nodeType}' }, ${safeChildId}Result.${outPort});`);
286
+ lines.push(`${tryIndent}${childSetCall}({ id: '${child.id}', portName: '${outPort}', executionIndex: ${safeChildId}Idx, nodeTypeName: '${child.nodeType}' }, ${safeChildId}Result.${outPort});`);
270
287
  }
271
288
  });
272
289
  }
273
290
  else {
274
291
  Object.keys(childNodeType.outputs || {}).forEach((outPort) => {
275
- lines.push(` ${childSetCall}({ id: '${child.id}', portName: '${outPort}', executionIndex: ${safeChildId}Idx, nodeTypeName: '${child.nodeType}' }, ${safeChildId}Result.${outPort});`);
292
+ lines.push(`${tryIndent}${childSetCall}({ id: '${child.id}', portName: '${outPort}', executionIndex: ${safeChildId}Idx, nodeTypeName: '${child.nodeType}' }, ${safeChildId}Result.${outPort});`);
276
293
  });
277
294
  }
278
295
  // Add SUCCEEDED status event
279
- lines.push(` ${awaitPrefix}scopedCtx.sendStatusChangedEvent({`);
280
- lines.push(` nodeTypeName: '${child.nodeType}',`);
281
- lines.push(` id: '${child.id}',`);
282
- lines.push(` executionIndex: ${safeChildId}Idx,`);
283
- lines.push(` status: 'SUCCEEDED',`);
284
- lines.push(` });`);
285
- lines.push(` } catch (error: unknown) {`);
286
- lines.push(` const isCancellation = CancellationError.isCancellationError(error);`);
287
- lines.push(` ${awaitPrefix}scopedCtx.sendStatusChangedEvent({`);
288
- lines.push(` nodeTypeName: '${child.nodeType}',`);
289
- lines.push(` id: '${child.id}',`);
290
- lines.push(` executionIndex: ${safeChildId}Idx,`);
291
- lines.push(` status: isCancellation ? 'CANCELLED' : 'FAILED',`);
292
- lines.push(` });`);
293
- lines.push(` if (!isCancellation) {`);
294
- lines.push(` scopedCtx.sendLogErrorEvent({`);
295
- lines.push(` nodeTypeName: '${child.nodeType}',`);
296
- lines.push(` id: '${child.id}',`);
297
- lines.push(` executionIndex: ${safeChildId}Idx,`);
298
- lines.push(` error: error instanceof Error ? error.message : String(error),`);
299
- lines.push(` });`);
300
- lines.push(` }`);
301
- lines.push(` throw error;`);
302
- lines.push(` }`);
296
+ lines.push(`${tryIndent}${awaitPrefix}scopedCtx.sendStatusChangedEvent({`);
297
+ lines.push(`${tryIndent} nodeTypeName: '${child.nodeType}',`);
298
+ lines.push(`${tryIndent} id: '${child.id}',`);
299
+ lines.push(`${tryIndent} executionIndex: ${safeChildId}Idx,`);
300
+ lines.push(`${tryIndent} status: 'SUCCEEDED',`);
301
+ lines.push(`${tryIndent}});`);
302
+ // Debug controller: afterNode hook for scoped children
303
+ if (emitDebugHooks) {
304
+ const awaitHook = isAsync ? 'await ' : '';
305
+ lines.push(`${tryIndent}${awaitHook}__ctrl__.afterNode('${child.id}', scopedCtx);`);
306
+ }
307
+ lines.push(`${childIndent}} catch (error: unknown) {`);
308
+ lines.push(`${tryIndent}const isCancellation = CancellationError.isCancellationError(error);`);
309
+ lines.push(`${tryIndent}${awaitPrefix}scopedCtx.sendStatusChangedEvent({`);
310
+ lines.push(`${tryIndent} nodeTypeName: '${child.nodeType}',`);
311
+ lines.push(`${tryIndent} id: '${child.id}',`);
312
+ lines.push(`${tryIndent} executionIndex: ${safeChildId}Idx,`);
313
+ lines.push(`${tryIndent} status: isCancellation ? 'CANCELLED' : 'FAILED',`);
314
+ lines.push(`${tryIndent}});`);
315
+ lines.push(`${tryIndent}if (!isCancellation) {`);
316
+ lines.push(`${tryIndent} scopedCtx.sendLogErrorEvent({`);
317
+ lines.push(`${tryIndent} nodeTypeName: '${child.nodeType}',`);
318
+ lines.push(`${tryIndent} id: '${child.id}',`);
319
+ lines.push(`${tryIndent} executionIndex: ${safeChildId}Idx,`);
320
+ lines.push(`${tryIndent} error: error instanceof Error ? error.message : String(error),`);
321
+ lines.push(`${tryIndent} });`);
322
+ lines.push(`${tryIndent}}`);
323
+ lines.push(`${tryIndent}throw error;`);
324
+ lines.push(`${childIndent}}`);
325
+ // Close debug controller beforeNode if-block
326
+ if (emitDebugHooks) {
327
+ lines.push(` }`);
328
+ }
303
329
  });
304
330
  lines.push(``);
305
331
  }
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)
@@ -1653,6 +1693,10 @@ export class AnnotationParser {
1653
1693
  for (const [inputName] of Object.entries(nextInputs)) {
1654
1694
  if (isControlFlowPort(inputName))
1655
1695
  continue;
1696
+ // Skip auto-wiring if a manual @connect already targets this input port
1697
+ const alreadyConnected = connections.some(c => c.to.node === nextId && c.to.port === inputName);
1698
+ if (alreadyConnected)
1699
+ continue;
1656
1700
  // Walk backward through path steps to find nearest ancestor with same-name output
1657
1701
  for (let j = i; j >= 0; j--) {
1658
1702
  const ancestorId = steps[j].node;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synergenius/flow-weaver",
3
- "version": "0.27.5",
3
+ "version": "0.29.0",
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",