@bubblelab/bubble-runtime 0.1.13 → 0.1.15

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 (68) hide show
  1. package/dist/extraction/BubbleParser.d.ts +188 -8
  2. package/dist/extraction/BubbleParser.d.ts.map +1 -1
  3. package/dist/extraction/BubbleParser.js +2265 -99
  4. package/dist/extraction/BubbleParser.js.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/injection/BubbleInjector.d.ts +31 -5
  10. package/dist/injection/BubbleInjector.d.ts.map +1 -1
  11. package/dist/injection/BubbleInjector.js +349 -28
  12. package/dist/injection/BubbleInjector.js.map +1 -1
  13. package/dist/injection/LoggerInjector.d.ts +17 -0
  14. package/dist/injection/LoggerInjector.d.ts.map +1 -1
  15. package/dist/injection/LoggerInjector.js +319 -9
  16. package/dist/injection/LoggerInjector.js.map +1 -1
  17. package/dist/parse/BubbleScript.d.ts +54 -5
  18. package/dist/parse/BubbleScript.d.ts.map +1 -1
  19. package/dist/parse/BubbleScript.js +112 -24
  20. package/dist/parse/BubbleScript.js.map +1 -1
  21. package/dist/parse/index.d.ts +0 -1
  22. package/dist/parse/index.d.ts.map +1 -1
  23. package/dist/parse/index.js +0 -1
  24. package/dist/parse/index.js.map +1 -1
  25. package/dist/runtime/BubbleRunner.d.ts +9 -2
  26. package/dist/runtime/BubbleRunner.d.ts.map +1 -1
  27. package/dist/runtime/BubbleRunner.js +76 -49
  28. package/dist/runtime/BubbleRunner.js.map +1 -1
  29. package/dist/runtime/index.d.ts +1 -1
  30. package/dist/runtime/index.d.ts.map +1 -1
  31. package/dist/runtime/index.js.map +1 -1
  32. package/dist/utils/bubble-helper.d.ts +2 -2
  33. package/dist/utils/bubble-helper.d.ts.map +1 -1
  34. package/dist/utils/bubble-helper.js +5 -1
  35. package/dist/utils/bubble-helper.js.map +1 -1
  36. package/dist/utils/error-sanitizer.d.ts +16 -0
  37. package/dist/utils/error-sanitizer.d.ts.map +1 -0
  38. package/dist/utils/error-sanitizer.js +111 -0
  39. package/dist/utils/error-sanitizer.js.map +1 -0
  40. package/dist/utils/normalize-control-flow.d.ts +14 -0
  41. package/dist/utils/normalize-control-flow.d.ts.map +1 -0
  42. package/dist/utils/normalize-control-flow.js +179 -0
  43. package/dist/utils/normalize-control-flow.js.map +1 -0
  44. package/dist/utils/parameter-formatter.d.ts +14 -5
  45. package/dist/utils/parameter-formatter.d.ts.map +1 -1
  46. package/dist/utils/parameter-formatter.js +166 -51
  47. package/dist/utils/parameter-formatter.js.map +1 -1
  48. package/dist/utils/sanitize-script.d.ts +11 -0
  49. package/dist/utils/sanitize-script.d.ts.map +1 -0
  50. package/dist/utils/sanitize-script.js +43 -0
  51. package/dist/utils/sanitize-script.js.map +1 -0
  52. package/dist/validation/BubbleValidator.d.ts +15 -0
  53. package/dist/validation/BubbleValidator.d.ts.map +1 -1
  54. package/dist/validation/BubbleValidator.js +171 -1
  55. package/dist/validation/BubbleValidator.js.map +1 -1
  56. package/dist/validation/index.d.ts +7 -3
  57. package/dist/validation/index.d.ts.map +1 -1
  58. package/dist/validation/index.js +63 -12
  59. package/dist/validation/index.js.map +1 -1
  60. package/dist/validation/lint-rules.d.ts +91 -0
  61. package/dist/validation/lint-rules.d.ts.map +1 -0
  62. package/dist/validation/lint-rules.js +755 -0
  63. package/dist/validation/lint-rules.js.map +1 -0
  64. package/package.json +5 -5
  65. package/dist/parse/traceDependencies.d.ts +0 -18
  66. package/dist/parse/traceDependencies.d.ts.map +0 -1
  67. package/dist/parse/traceDependencies.js +0 -196
  68. package/dist/parse/traceDependencies.js.map +0 -1
@@ -1,8 +1,22 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
1
2
  import { buildClassNameLookup } from '../utils/bubble-helper';
2
- import { BubbleParameterType } from '@bubblelab/shared-schemas';
3
+ import { BubbleParameterType, hashToVariableId, buildCallSiteKey, } from '@bubblelab/shared-schemas';
3
4
  import { parseToolsParamValue } from '../utils/parameter-formatter';
4
5
  export class BubbleParser {
5
6
  bubbleScript;
7
+ cachedAST = null;
8
+ methodInvocationOrdinalMap = new Map();
9
+ invocationBubbleCloneCache = new Map();
10
+ /**
11
+ * Track which call expressions have been assigned an invocation index.
12
+ * Key: `methodName:startOffset` (using AST range start position)
13
+ * Value: the assigned invocation index
14
+ * This prevents double-counting when the same call site is processed multiple times
15
+ * (e.g., once in .map() callback processing, again in Promise.all resolution)
16
+ */
17
+ processedCallSiteIndexes = new Map();
18
+ /** Custom tool func ranges for marking bubbles inside custom tools */
19
+ customToolFuncs = [];
6
20
  constructor(bubbleScript) {
7
21
  this.bubbleScript = bubbleScript;
8
22
  }
@@ -17,13 +31,35 @@ export class BubbleParser {
17
31
  }
18
32
  const nodes = {};
19
33
  const errors = [];
20
- // Find handle method location
21
- const handleMethodLocation = this.findHandleMethodLocation(ast);
34
+ // Find main BubbleFlow class and all its instance methods
35
+ const mainClass = this.findMainBubbleFlowClass(ast);
36
+ const instanceMethodsLocation = {};
37
+ if (mainClass) {
38
+ const methods = this.findAllInstanceMethods(mainClass);
39
+ const methodNames = methods.map((m) => m.methodName);
40
+ const invocations = this.findMethodInvocations(ast, methodNames);
41
+ // Combine method locations with invocation lines
42
+ for (const method of methods) {
43
+ instanceMethodsLocation[method.methodName] = {
44
+ startLine: method.startLine,
45
+ endLine: method.endLine,
46
+ definitionStartLine: method.definitionStartLine,
47
+ bodyStartLine: method.bodyStartLine,
48
+ invocationLines: invocations[method.methodName] || [],
49
+ };
50
+ }
51
+ }
52
+ // Clear custom tool func tracking from previous runs
53
+ this.customToolFuncs = [];
22
54
  // Visit AST nodes to find bubble instantiations
23
55
  this.visitNode(ast, nodes, classNameToInfo, scopeManager);
24
56
  if (errors.length > 0) {
25
57
  throw new Error(`Failed to trace bubble dependencies: ${errors.join(', ')}`);
26
58
  }
59
+ // Find custom tools in ai-agent bubbles and populate customToolFuncs
60
+ this.findCustomToolsInAIAgentBubbles(ast, nodes, classNameToInfo);
61
+ // Mark bubbles that are inside custom tool funcs
62
+ this.markBubblesInsideCustomTools(nodes);
27
63
  // Build a set of used variable IDs to ensure uniqueness for any synthetic IDs we allocate
28
64
  const usedVariableIds = new Set();
29
65
  for (const [idStr, node] of Object.entries(nodes)) {
@@ -59,10 +95,69 @@ export class BubbleParser {
59
95
  ordinalCounters, usedVariableIds, bubble.variableId, // Root variable id mirrors the parsed bubble's variable id
60
96
  true, // suppress adding self segment for root
61
97
  bubble.variableName);
98
+ // Add functionCallChildren for ai-agent bubbles with custom tools
99
+ if (bubble.bubbleName === 'ai-agent' &&
100
+ bubble.dependencyGraph &&
101
+ this.customToolFuncs.length > 0) {
102
+ const toolsForThisBubble = this.customToolFuncs.filter((t) => t.parentBubbleVariableId === bubble.variableId);
103
+ if (toolsForThisBubble.length > 0) {
104
+ const functionCallChildren = [];
105
+ for (const toolFunc of toolsForThisBubble) {
106
+ // Find all bubbles that are inside this custom tool func
107
+ const childBubbleNodes = [];
108
+ for (const [, bubbleInfo] of Object.entries(nodes)) {
109
+ if (bubbleInfo.containingCustomToolId ===
110
+ `${toolFunc.parentBubbleVariableId}.${toolFunc.toolName}`) {
111
+ childBubbleNodes.push({
112
+ type: 'bubble',
113
+ variableId: bubbleInfo.variableId,
114
+ });
115
+ }
116
+ }
117
+ // Create FunctionCallWorkflowNode for this custom tool
118
+ const funcCallNode = {
119
+ type: 'function_call',
120
+ functionName: toolFunc.toolName,
121
+ description: toolFunc.description,
122
+ isMethodCall: false,
123
+ code: `customTool:${toolFunc.toolName}`,
124
+ variableId: hashToVariableId(`${bubble.variableId}.customTool.${toolFunc.toolName}`),
125
+ location: {
126
+ startLine: toolFunc.startLine,
127
+ startCol: toolFunc.startCol,
128
+ endLine: toolFunc.endLine,
129
+ endCol: toolFunc.endCol,
130
+ },
131
+ methodDefinition: {
132
+ location: {
133
+ startLine: toolFunc.startLine,
134
+ endLine: toolFunc.endLine,
135
+ },
136
+ isAsync: toolFunc.isAsync,
137
+ parameters: ['input'], // Custom tools always have 'input' parameter
138
+ },
139
+ children: childBubbleNodes,
140
+ };
141
+ functionCallChildren.push(funcCallNode);
142
+ }
143
+ bubble.dependencyGraph.functionCallChildren = functionCallChildren;
144
+ }
145
+ }
146
+ }
147
+ // Store AST for method definition lookup
148
+ this.cachedAST = ast;
149
+ this.methodInvocationOrdinalMap.clear();
150
+ this.invocationBubbleCloneCache.clear();
151
+ this.processedCallSiteIndexes.clear();
152
+ // Build hierarchical workflow structure
153
+ const workflow = this.buildWorkflowTree(ast, nodes, scopeManager);
154
+ for (const clone of this.invocationBubbleCloneCache.values()) {
155
+ nodes[clone.variableId] = clone;
62
156
  }
63
157
  return {
64
158
  bubbles: nodes,
65
- handleMethodLocation,
159
+ workflow,
160
+ instanceMethodsLocation,
66
161
  };
67
162
  }
68
163
  findDependenciesForBubble(currentDependencies, bubbleFactory, parameters, seen = new Set()) {
@@ -143,7 +238,7 @@ export class BubbleParser {
143
238
  : `${bubbleName}#${nextOrdinal}`;
144
239
  const variableId = typeof explicitVariableId === 'number'
145
240
  ? explicitVariableId
146
- : this.hashUniqueIdToVarId(uniqueId);
241
+ : hashToVariableId(uniqueId);
147
242
  const metadata = bubbleFactory.getMetadata(bubbleName);
148
243
  if (seen.has(bubbleName)) {
149
244
  return {
@@ -180,7 +275,7 @@ export class BubbleParser {
180
275
  const aiOrdinal = (ordinalCounters.get(aiCountKey) || 0) + 1;
181
276
  ordinalCounters.set(aiCountKey, aiOrdinal);
182
277
  const aiAgentUniqueId = `${uniqueId}.ai-agent#${aiOrdinal}`;
183
- const aiAgentVarId = this.hashUniqueIdToVarId(aiAgentUniqueId);
278
+ const aiAgentVarId = hashToVariableId(aiAgentUniqueId);
184
279
  const toolChildren = [];
185
280
  for (const toolName of toolsForChild) {
186
281
  toolChildren.push(this.buildDependencyGraph(toolName, bubbleFactory, nextSeen, undefined, aiAgentUniqueId, ordinalCounters, usedVariableIds, undefined, false, toolName));
@@ -225,17 +320,6 @@ export class BubbleParser {
225
320
  };
226
321
  return nodeObj;
227
322
  }
228
- // Deterministic non-negative integer ID from uniqueId string
229
- hashUniqueIdToVarId(input) {
230
- let hash = 2166136261; // FNV-1a 32-bit offset basis
231
- for (let i = 0; i < input.length; i++) {
232
- hash ^= input.charCodeAt(i);
233
- hash = (hash * 16777619) >>> 0; // unsigned 32-bit
234
- }
235
- // Map to 6-digit range to avoid colliding with small AST ids while readable
236
- const mapped = 100000 + (hash % 900000);
237
- return mapped;
238
- }
239
323
  /**
240
324
  * Build a JSON Schema object for the payload parameter of the top-level `handle` entrypoint.
241
325
  * Supports primitives, arrays, unions (anyOf), intersections (allOf), type literals, and
@@ -549,8 +633,17 @@ export class BubbleParser {
549
633
  if (!keyName)
550
634
  continue;
551
635
  const propSchema = m.typeAnnotation
552
- ? this.tsTypeToJsonSchema(m.typeAnnotation.typeAnnotation, ast)
636
+ ? this.tsTypeToJsonSchema(m.typeAnnotation.typeAnnotation, ast) || {}
553
637
  : {};
638
+ // Extract comment/description and JSDoc tags for this property
639
+ const jsDocInfo = this.extractJSDocForNode(m);
640
+ if (jsDocInfo.description) {
641
+ propSchema.description = jsDocInfo.description;
642
+ }
643
+ // Add canBeFile flag to schema if explicitly specified in JSDoc
644
+ if (jsDocInfo.canBeFile !== undefined) {
645
+ propSchema.canBeFile = jsDocInfo.canBeFile;
646
+ }
554
647
  properties[keyName] = propSchema;
555
648
  if (!m.optional)
556
649
  required.push(keyName);
@@ -561,6 +654,7 @@ export class BubbleParser {
561
654
  return schema;
562
655
  }
563
656
  // Minimal mapping for known trigger event keys to JSON Schema shapes
657
+ // Used for the input schema in the BubbleFlow editor if defined as BubbleTriggerEventRegistry[eventType]
564
658
  eventKeyToSchema(eventKey) {
565
659
  if (eventKey === 'slack/bot_mentioned') {
566
660
  return {
@@ -585,22 +679,12 @@ export class BubbleParser {
585
679
  },
586
680
  };
587
681
  }
588
- if (eventKey === 'gmail/email_received') {
682
+ if (eventKey === 'schedule/cron') {
589
683
  return {
590
684
  type: 'object',
591
685
  properties: {
592
- email: { type: 'string' },
593
- },
594
- required: ['email'],
595
- };
596
- }
597
- if (eventKey === 'schedule/cron/daily') {
598
- return {
599
- type: 'object',
600
- properties: {
601
- cron: { type: 'string' },
686
+ body: { type: 'object' },
602
687
  },
603
- required: ['cron'],
604
688
  };
605
689
  }
606
690
  if (eventKey === 'slack/message_received') {
@@ -641,94 +725,411 @@ export class BubbleParser {
641
725
  return null;
642
726
  }
643
727
  /**
644
- * Find the handle method location in the AST
728
+ * Find the main class that extends BubbleFlow
645
729
  */
646
- findHandleMethodLocation(ast) {
730
+ findMainBubbleFlowClass(ast) {
647
731
  for (const statement of ast.body) {
648
- // Look for function declarations named 'handle'
649
- if (statement.type === 'FunctionDeclaration' &&
650
- statement.id?.name === 'handle') {
651
- return {
652
- startLine: statement.loc?.start.line || -1,
653
- endLine: statement.loc?.end.line || -1,
654
- };
655
- }
656
- // Look for exported function declarations: export function handle() {}
732
+ let classDecl = null;
733
+ // Check exported class declarations
657
734
  if (statement.type === 'ExportNamedDeclaration' &&
658
- statement.declaration?.type === 'FunctionDeclaration' &&
659
- statement.declaration.id?.name === 'handle') {
660
- return {
661
- startLine: statement.declaration.loc?.start.line || -1,
662
- endLine: statement.declaration.loc?.end.line || -1,
663
- };
735
+ statement.declaration?.type === 'ClassDeclaration') {
736
+ classDecl = statement.declaration;
664
737
  }
665
- // Look for variable declarations with function expressions: const handle = () => {}
666
- if (statement.type === 'VariableDeclaration') {
667
- for (const declarator of statement.declarations) {
668
- if (declarator.type === 'VariableDeclarator' &&
669
- declarator.id.type === 'Identifier' &&
670
- declarator.id.name === 'handle' &&
671
- (declarator.init?.type === 'FunctionExpression' ||
672
- declarator.init?.type === 'ArrowFunctionExpression')) {
673
- return {
674
- startLine: declarator.init.loc?.start.line || -1,
675
- endLine: declarator.init.loc?.end.line || -1,
676
- };
677
- }
678
- }
738
+ // Check non-exported class declarations
739
+ else if (statement.type === 'ClassDeclaration') {
740
+ classDecl = statement;
679
741
  }
680
- // Look for exported variable declarations: export const handle = () => {}
681
- if (statement.type === 'ExportNamedDeclaration' &&
682
- statement.declaration?.type === 'VariableDeclaration') {
683
- for (const declarator of statement.declaration.declarations) {
684
- if (declarator.type === 'VariableDeclarator' &&
685
- declarator.id.type === 'Identifier' &&
686
- declarator.id.name === 'handle' &&
687
- (declarator.init?.type === 'FunctionExpression' ||
688
- declarator.init?.type === 'ArrowFunctionExpression')) {
689
- return {
690
- startLine: declarator.init.loc?.start.line || -1,
691
- endLine: declarator.init.loc?.end.line || -1,
692
- };
742
+ if (classDecl) {
743
+ // Check if this class extends BubbleFlow
744
+ if (classDecl.superClass) {
745
+ const superClass = classDecl.superClass;
746
+ // Handle simple identifier like extends BubbleFlow
747
+ if (superClass.type === 'Identifier' &&
748
+ superClass.name === 'BubbleFlow') {
749
+ return classDecl;
750
+ }
751
+ // Handle generic type like BubbleFlow<'webhook/http'>
752
+ // Check if it's a TSTypeReference with type parameters
753
+ // Use type assertion since TSESTree types may not fully expose this
754
+ if (superClass.type === 'TSTypeReference') {
755
+ const typeName = superClass.typeName;
756
+ if (typeName &&
757
+ typeName.type === 'Identifier' &&
758
+ typeName.name === 'BubbleFlow') {
759
+ return classDecl;
760
+ }
693
761
  }
694
- }
695
- }
696
- // Look for exported class declarations with handle method
697
- if (statement.type === 'ExportNamedDeclaration' &&
698
- statement.declaration?.type === 'ClassDeclaration') {
699
- const handleMethod = this.findHandleMethodInClass(statement.declaration);
700
- if (handleMethod) {
701
- return handleMethod;
702
- }
703
- }
704
- // Look for class declarations with handle method
705
- if (statement.type === 'ClassDeclaration') {
706
- const handleMethod = this.findHandleMethodInClass(statement);
707
- if (handleMethod) {
708
- return handleMethod;
709
762
  }
710
763
  }
711
764
  }
712
- return null; // Handle method not found
765
+ return null;
713
766
  }
714
767
  /**
715
- * Find handle method within a class declaration
768
+ * Extract all instance methods from a class
716
769
  */
717
- findHandleMethodInClass(classDeclaration) {
770
+ findAllInstanceMethods(classDeclaration) {
771
+ const methods = [];
718
772
  if (!classDeclaration.body)
719
- return null;
773
+ return methods;
720
774
  for (const member of classDeclaration.body.body) {
775
+ // Only process instance methods (not static, not getters/setters)
721
776
  if (member.type === 'MethodDefinition' &&
777
+ !member.static &&
778
+ member.kind === 'method' &&
722
779
  member.key.type === 'Identifier' &&
723
- member.key.name === 'handle' &&
724
780
  member.value.type === 'FunctionExpression') {
725
- return {
726
- startLine: member.value.loc?.start.line || -1,
727
- endLine: member.value.loc?.end.line || -1,
728
- };
781
+ const methodName = member.key.name;
782
+ const definitionStart = member.loc?.start.line || -1;
783
+ const bodyStart = member.value.body?.loc?.start.line || definitionStart;
784
+ const definitionEnd = member.loc?.end.line || -1;
785
+ methods.push({
786
+ methodName,
787
+ startLine: definitionStart,
788
+ endLine: definitionEnd,
789
+ definitionStartLine: definitionStart,
790
+ bodyStartLine: bodyStart,
791
+ });
729
792
  }
730
793
  }
731
- return null;
794
+ return methods;
795
+ }
796
+ /**
797
+ * Find all method invocations in the AST with full details
798
+ */
799
+ findMethodInvocations(ast, methodNames) {
800
+ const invocations = {};
801
+ const methodNameSet = new Set(methodNames);
802
+ const visitedNodes = new WeakSet();
803
+ const parentMap = new WeakMap();
804
+ const invocationCounters = new Map();
805
+ // Initialize invocations map
806
+ for (const methodName of methodNames) {
807
+ invocations[methodName] = [];
808
+ }
809
+ // First pass: Build parent map
810
+ const buildParentMap = (node, parent) => {
811
+ if (parent) {
812
+ parentMap.set(node, parent);
813
+ }
814
+ // Visit children
815
+ const visitValue = (value) => {
816
+ if (value && typeof value === 'object') {
817
+ if (Array.isArray(value)) {
818
+ value.forEach(visitValue);
819
+ }
820
+ else if ('type' in value && typeof value.type === 'string') {
821
+ buildParentMap(value, node);
822
+ }
823
+ else {
824
+ Object.values(value).forEach(visitValue);
825
+ }
826
+ }
827
+ };
828
+ const nodeObj = node;
829
+ for (const [key, value] of Object.entries(nodeObj)) {
830
+ if (key === 'parent' || key === 'loc' || key === 'range') {
831
+ continue;
832
+ }
833
+ visitValue(value);
834
+ }
835
+ };
836
+ buildParentMap(ast);
837
+ const isPromiseAllArrayElement = (arrayNode, elementNode) => {
838
+ if (!elementNode || !arrayNode.elements.includes(elementNode)) {
839
+ return false;
840
+ }
841
+ const parentCall = parentMap.get(arrayNode);
842
+ if (!parentCall || parentCall.type !== 'CallExpression') {
843
+ return false;
844
+ }
845
+ const callee = parentCall.callee;
846
+ if (callee.type !== 'MemberExpression' ||
847
+ callee.object.type !== 'Identifier' ||
848
+ callee.object.name !== 'Promise' ||
849
+ callee.property.type !== 'Identifier' ||
850
+ callee.property.name !== 'all') {
851
+ return false;
852
+ }
853
+ return (parentCall.arguments.length > 0 && parentCall.arguments[0] === arrayNode);
854
+ };
855
+ const visitNode = (node, parent) => {
856
+ // Skip if already visited
857
+ if (visitedNodes.has(node)) {
858
+ return;
859
+ }
860
+ visitedNodes.add(node);
861
+ // Look for CallExpression nodes
862
+ if (node.type === 'CallExpression') {
863
+ const callee = node.callee;
864
+ // Check if it's a method call: this.methodName()
865
+ if (callee.type === 'MemberExpression') {
866
+ const object = callee.object;
867
+ const property = callee.property;
868
+ if (object.type === 'ThisExpression' &&
869
+ property.type === 'Identifier' &&
870
+ methodNameSet.has(property.name)) {
871
+ const methodName = property.name;
872
+ const lineNumber = node.loc?.start.line;
873
+ const endLineNumber = node.loc?.end.line;
874
+ const columnNumber = node.loc?.start.column ?? -1;
875
+ if (!lineNumber || !endLineNumber)
876
+ return;
877
+ // Extract arguments
878
+ const args = node.arguments
879
+ .map((arg) => this.bubbleScript.substring(arg.range[0], arg.range[1]))
880
+ .join(', ');
881
+ // Determine statement type by looking at parent context
882
+ let statementType = 'simple';
883
+ let variableName;
884
+ let variableType;
885
+ let destructuringPattern;
886
+ let hasAwait = false;
887
+ // For condition_expression: track the containing statement info
888
+ let containingStatementLine;
889
+ let callRange;
890
+ let callText;
891
+ // Check if parent is AwaitExpression
892
+ if (parent?.type === 'AwaitExpression') {
893
+ hasAwait = true;
894
+ }
895
+ // Find the statement containing this call using parent map
896
+ // We need to check parent chain to identify:
897
+ // 1. VariableDeclarator -> VariableDeclaration (for const/let/var x = ...)
898
+ // 2. AssignmentExpression (for x = ...)
899
+ // 3. ReturnStatement (for return ...)
900
+ // Also check for complex expressions that should not be instrumented
901
+ let currentParent = parentMap.get(node);
902
+ let isInComplexExpression = false;
903
+ let invocationContext = 'default';
904
+ let currentChild = node;
905
+ while (currentParent) {
906
+ const parentIsPromiseAllElement = currentParent.type === 'ArrayExpression' &&
907
+ isPromiseAllArrayElement(currentParent, currentChild);
908
+ // Check if we're inside a complex expression before reaching the statement level
909
+ // These expressions cannot be instrumented inline without breaking syntax
910
+ if (currentParent.type === 'ConditionalExpression' || // Ternary: a ? b : c
911
+ currentParent.type === 'ObjectExpression' || // Object literal: { key: value }
912
+ (currentParent.type === 'ArrayExpression' &&
913
+ !parentIsPromiseAllElement) || // Array literal outside Promise.all
914
+ currentParent.type === 'Property' || // Object property
915
+ currentParent.type === 'SpreadElement' // Spread: ...expr
916
+ ) {
917
+ isInComplexExpression = true;
918
+ break;
919
+ }
920
+ // Check if we're inside an arrow function expression body (no braces)
921
+ // e.g., arr.map((x) => this.method(x)) - the body is just an expression
922
+ // These need to be wrapped in an async IIFE, similar to promise_all_element
923
+ if (currentParent.type === 'ArrowFunctionExpression' &&
924
+ currentChild === currentParent.body &&
925
+ currentParent.body.type !== 'BlockStatement') {
926
+ invocationContext = 'promise_all_element';
927
+ // Capture call text for proper replacement
928
+ const callNode = hasAwait ? parent : node;
929
+ if (callNode?.range) {
930
+ callRange = {
931
+ start: callNode.range[0],
932
+ end: callNode.range[1],
933
+ };
934
+ callText = this.bubbleScript.substring(callRange.start, callRange.end);
935
+ }
936
+ // Don't break - continue to find the outer statement for line info
937
+ }
938
+ // Check if we're inside the condition/test part of a control flow statement
939
+ // These need special handling - extract call before the statement and replace in-place
940
+ // IMPORTANT: Only treat as condition_expression if the call is in the test/condition,
941
+ // not in the body (consequent/alternate/etc)
942
+ if (currentParent.type === 'IfStatement' ||
943
+ currentParent.type === 'WhileStatement' ||
944
+ currentParent.type === 'DoWhileStatement' ||
945
+ currentParent.type === 'ForStatement' ||
946
+ currentParent.type === 'SwitchStatement') {
947
+ // Check if currentChild is actually in the test/condition part
948
+ const isInCondition = this.isNodeInConditionPart(currentParent, currentChild);
949
+ if (isInCondition) {
950
+ statementType = 'condition_expression';
951
+ containingStatementLine = currentParent.loc?.start.line;
952
+ // Capture the call range - include await if present
953
+ const callNode = hasAwait ? parent : node;
954
+ if (callNode?.range) {
955
+ callRange = {
956
+ start: callNode.range[0],
957
+ end: callNode.range[1],
958
+ };
959
+ callText = this.bubbleScript.substring(callRange.start, callRange.end);
960
+ }
961
+ break;
962
+ }
963
+ // If not in condition part, continue walking up - the call is in the body
964
+ // and should be treated as a normal statement
965
+ }
966
+ if (parentIsPromiseAllElement) {
967
+ invocationContext = 'promise_all_element';
968
+ }
969
+ // Check if we're nested inside another CallExpression (e.g., arr.push(this.method()))
970
+ // This needs special handling similar to condition_expression - extract call before
971
+ // the statement and replace the call text inline
972
+ if (currentParent.type === 'CallExpression' &&
973
+ currentParent !== node) {
974
+ // This is a nested call - the tracked method is an argument to another call
975
+ // Find the containing ExpressionStatement line
976
+ let stmtParent = currentParent;
977
+ while (stmtParent &&
978
+ stmtParent.type !== 'ExpressionStatement') {
979
+ stmtParent = parentMap.get(stmtParent);
980
+ }
981
+ if (stmtParent?.type === 'ExpressionStatement') {
982
+ statementType = 'nested_call_expression';
983
+ containingStatementLine = stmtParent.loc?.start.line;
984
+ // Capture the call range - include await if present
985
+ const callNode = hasAwait ? parent : node;
986
+ if (callNode?.range) {
987
+ callRange = {
988
+ start: callNode.range[0],
989
+ end: callNode.range[1],
990
+ };
991
+ callText = this.bubbleScript.substring(callRange.start, callRange.end);
992
+ }
993
+ break;
994
+ }
995
+ }
996
+ if (currentParent.type === 'VariableDeclarator') {
997
+ statementType = 'variable_declaration';
998
+ if (currentParent.id.type === 'Identifier') {
999
+ variableName = currentParent.id.name;
1000
+ }
1001
+ else if (currentParent.id.type === 'ObjectPattern' ||
1002
+ currentParent.id.type === 'ArrayPattern') {
1003
+ // Extract the destructuring pattern from the source
1004
+ const declaratorNode = currentParent;
1005
+ const patternRange = declaratorNode.id.range;
1006
+ if (patternRange) {
1007
+ destructuringPattern = this.bubbleScript.substring(patternRange[0], patternRange[1]);
1008
+ }
1009
+ }
1010
+ // Continue to find the VariableDeclaration parent to get const/let/var
1011
+ }
1012
+ else if (currentParent.type === 'VariableDeclaration') {
1013
+ // This should only be reached if we found VariableDeclarator first
1014
+ if (statementType === 'variable_declaration') {
1015
+ variableType = currentParent.kind;
1016
+ break;
1017
+ }
1018
+ }
1019
+ else if (currentParent.type === 'AssignmentExpression') {
1020
+ statementType = 'assignment';
1021
+ if (currentParent.left.type === 'Identifier') {
1022
+ variableName = currentParent.left.name;
1023
+ }
1024
+ break;
1025
+ }
1026
+ else if (currentParent.type === 'ReturnStatement') {
1027
+ statementType = 'return';
1028
+ break;
1029
+ }
1030
+ // Move up the tree using parent map
1031
+ currentChild = currentParent;
1032
+ currentParent = parentMap.get(currentParent);
1033
+ }
1034
+ // Skip this invocation if it's inside a complex expression
1035
+ // Instrumenting these would break the syntax
1036
+ if (isInComplexExpression) {
1037
+ return;
1038
+ }
1039
+ const invocationIndex = (invocationCounters.get(methodName) ?? 0) + 1;
1040
+ invocationCounters.set(methodName, invocationIndex);
1041
+ invocations[methodName].push({
1042
+ lineNumber,
1043
+ endLineNumber,
1044
+ columnNumber,
1045
+ invocationIndex,
1046
+ hasAwait,
1047
+ arguments: args,
1048
+ statementType,
1049
+ variableName,
1050
+ variableType,
1051
+ destructuringPattern,
1052
+ context: invocationContext,
1053
+ containingStatementLine,
1054
+ callRange,
1055
+ callText,
1056
+ });
1057
+ }
1058
+ }
1059
+ }
1060
+ // Check for await expressions - pass current node as parent
1061
+ if (node.type === 'AwaitExpression' && node.argument) {
1062
+ visitNode(node.argument, node);
1063
+ }
1064
+ // Recursively visit child nodes with parent context
1065
+ this.visitChildNodesForInvocations(node, (child) => visitNode(child, node));
1066
+ };
1067
+ visitNode(ast);
1068
+ return invocations;
1069
+ }
1070
+ /**
1071
+ * Check if a child node is in the condition/test part of a control flow statement
1072
+ * Returns true if the child is the test/discriminant expression, false if it's in the body
1073
+ */
1074
+ isNodeInConditionPart(controlFlowNode, childNode) {
1075
+ if (!childNode)
1076
+ return false;
1077
+ switch (controlFlowNode.type) {
1078
+ case 'IfStatement': {
1079
+ const ifStmt = controlFlowNode;
1080
+ return childNode === ifStmt.test;
1081
+ }
1082
+ case 'WhileStatement': {
1083
+ const whileStmt = controlFlowNode;
1084
+ return childNode === whileStmt.test;
1085
+ }
1086
+ case 'DoWhileStatement': {
1087
+ const doWhileStmt = controlFlowNode;
1088
+ return childNode === doWhileStmt.test;
1089
+ }
1090
+ case 'ForStatement': {
1091
+ const forStmt = controlFlowNode;
1092
+ // ForStatement has init, test, and update - all are condition-like
1093
+ return (childNode === forStmt.init ||
1094
+ childNode === forStmt.test ||
1095
+ childNode === forStmt.update);
1096
+ }
1097
+ case 'SwitchStatement': {
1098
+ const switchStmt = controlFlowNode;
1099
+ return childNode === switchStmt.discriminant;
1100
+ }
1101
+ default:
1102
+ return false;
1103
+ }
1104
+ }
1105
+ /**
1106
+ * Helper to recursively visit child nodes for finding invocations
1107
+ */
1108
+ visitChildNodesForInvocations(node, visitor) {
1109
+ const visitValue = (value) => {
1110
+ if (value && typeof value === 'object') {
1111
+ if (Array.isArray(value)) {
1112
+ value.forEach(visitValue);
1113
+ }
1114
+ else if ('type' in value && typeof value.type === 'string') {
1115
+ // This is likely an AST node
1116
+ visitor(value);
1117
+ }
1118
+ else {
1119
+ // This is a regular object, recurse into its properties
1120
+ Object.values(value).forEach(visitValue);
1121
+ }
1122
+ }
1123
+ };
1124
+ // Get all property values of the node, excluding metadata properties
1125
+ const nodeObj = node;
1126
+ for (const [key, value] of Object.entries(nodeObj)) {
1127
+ // Skip metadata properties that aren't part of the AST structure
1128
+ if (key === 'parent' || key === 'loc' || key === 'range') {
1129
+ continue;
1130
+ }
1131
+ visitValue(value);
1132
+ }
732
1133
  }
733
1134
  /**
734
1135
  * Recursively visit AST nodes to find bubble instantiations
@@ -744,6 +1145,11 @@ export class BubbleParser {
744
1145
  const bubbleNode = this.extractBubbleFromExpression(declarator.init, classNameLookup);
745
1146
  if (bubbleNode) {
746
1147
  bubbleNode.variableName = nameText;
1148
+ // Extract comment for this bubble node
1149
+ const description = this.extractCommentForNode(node);
1150
+ if (description) {
1151
+ bubbleNode.description = description;
1152
+ }
747
1153
  // Find the Variable object for this bubble declaration
748
1154
  const variable = this.findVariableForBubble(nameText, node, scopeManager);
749
1155
  if (variable) {
@@ -766,6 +1172,11 @@ export class BubbleParser {
766
1172
  if (bubbleNode) {
767
1173
  const synthetic = `_anonymous_${bubbleNode.className}_${Object.keys(nodes).length}`;
768
1174
  bubbleNode.variableName = synthetic;
1175
+ // Extract comment for this bubble node
1176
+ const description = this.extractCommentForNode(node);
1177
+ if (description) {
1178
+ bubbleNode.description = description;
1179
+ }
769
1180
  // For anonymous bubbles, use negative synthetic ID (no Variable object exists)
770
1181
  const syntheticId = -1 * (Object.keys(nodes).length + 1);
771
1182
  bubbleNode.variableId = syntheticId;
@@ -794,6 +1205,11 @@ export class BubbleParser {
794
1205
  if (bubbleNode) {
795
1206
  const synthetic = `_anonymous_${bubbleNode.className}_${Object.keys(nodes).length}`;
796
1207
  bubbleNode.variableName = synthetic;
1208
+ // Extract comment for this bubble node
1209
+ const description = this.extractCommentForNode(node);
1210
+ if (description) {
1211
+ bubbleNode.description = description;
1212
+ }
797
1213
  const syntheticId = -1 * (Object.keys(nodes).length + 1);
798
1214
  bubbleNode.variableId = syntheticId;
799
1215
  bubbleNode.parameters = this.addVariableReferencesToParameters(bubbleNode.parameters, node, scopeManager);
@@ -992,6 +1408,7 @@ export class BubbleParser {
992
1408
  name,
993
1409
  ...value,
994
1410
  location,
1411
+ source: 'object-property', // Parameter came from an object literal property
995
1412
  });
996
1413
  }
997
1414
  }
@@ -1013,6 +1430,7 @@ export class BubbleParser {
1013
1430
  name: spreadName,
1014
1431
  ...value,
1015
1432
  location,
1433
+ source: 'spread', // Changed from 'object-property' to 'spread'
1016
1434
  });
1017
1435
  }
1018
1436
  }
@@ -1034,6 +1452,7 @@ export class BubbleParser {
1034
1452
  name: argName,
1035
1453
  ...value,
1036
1454
  location,
1455
+ source: 'first-arg', // Parameter represents the entire first argument
1037
1456
  });
1038
1457
  }
1039
1458
  }
@@ -1120,5 +1539,1752 @@ export class BubbleParser {
1120
1539
  // Fallback
1121
1540
  return { value: valueText, type: BubbleParameterType.UNKNOWN };
1122
1541
  }
1542
+ /**
1543
+ * Find custom tools in ai-agent bubbles and populate customToolFuncs.
1544
+ * This scans the AST for ai-agent instantiations and extracts custom tool func locations.
1545
+ */
1546
+ findCustomToolsInAIAgentBubbles(ast, nodes, classNameLookup) {
1547
+ // Get all ai-agent bubbles from nodes
1548
+ const aiAgentBubbles = Object.values(nodes).filter((n) => n.bubbleName === 'ai-agent');
1549
+ if (aiAgentBubbles.length === 0)
1550
+ return;
1551
+ // Helper to find NewExpression nodes in the AST
1552
+ const findNewExpressions = (node, results) => {
1553
+ if (node.type === 'NewExpression') {
1554
+ results.push(node);
1555
+ }
1556
+ for (const key of Object.keys(node)) {
1557
+ if (key === 'parent' || key === 'range' || key === 'loc')
1558
+ continue;
1559
+ const child = node[key];
1560
+ if (Array.isArray(child)) {
1561
+ for (const item of child) {
1562
+ if (item && typeof item === 'object' && 'type' in item) {
1563
+ findNewExpressions(item, results);
1564
+ }
1565
+ }
1566
+ }
1567
+ else if (child && typeof child === 'object' && 'type' in child) {
1568
+ findNewExpressions(child, results);
1569
+ }
1570
+ }
1571
+ };
1572
+ const allNewExprs = [];
1573
+ findNewExpressions(ast, allNewExprs);
1574
+ // Find NewExpressions that are AIAgentBubble instantiations
1575
+ for (const newExpr of allNewExprs) {
1576
+ if (newExpr.callee.type !== 'Identifier' ||
1577
+ !classNameLookup.has(newExpr.callee.name)) {
1578
+ continue;
1579
+ }
1580
+ const info = classNameLookup.get(newExpr.callee.name);
1581
+ if (!info || info.bubbleName !== 'ai-agent')
1582
+ continue;
1583
+ // Find the corresponding parsed bubble by location
1584
+ const matchingBubble = aiAgentBubbles.find((b) => b.location.startLine === (newExpr.loc?.start.line ?? 0) &&
1585
+ b.location.startCol === (newExpr.loc?.start.column ?? 0));
1586
+ if (!matchingBubble)
1587
+ continue;
1588
+ // Find the customTools property in the first argument
1589
+ if (!newExpr.arguments ||
1590
+ newExpr.arguments.length === 0 ||
1591
+ newExpr.arguments[0].type !== 'ObjectExpression') {
1592
+ continue;
1593
+ }
1594
+ const firstArg = newExpr.arguments[0];
1595
+ const customToolsProp = firstArg.properties.find((p) => p.type === 'Property' &&
1596
+ p.key.type === 'Identifier' &&
1597
+ p.key.name === 'customTools');
1598
+ if (!customToolsProp ||
1599
+ customToolsProp.value.type !== 'ArrayExpression') {
1600
+ continue;
1601
+ }
1602
+ const customToolsArray = customToolsProp.value;
1603
+ // Process each custom tool object
1604
+ for (const element of customToolsArray.elements) {
1605
+ if (!element || element.type !== 'ObjectExpression')
1606
+ continue;
1607
+ const toolObj = element;
1608
+ // Find name property
1609
+ const nameProp = toolObj.properties.find((p) => p.type === 'Property' &&
1610
+ p.key.type === 'Identifier' &&
1611
+ p.key.name === 'name');
1612
+ const toolName = nameProp?.value.type === 'Literal' &&
1613
+ typeof nameProp.value.value === 'string'
1614
+ ? nameProp.value.value
1615
+ : undefined;
1616
+ if (!toolName)
1617
+ continue;
1618
+ // Find description property
1619
+ const descProp = toolObj.properties.find((p) => p.type === 'Property' &&
1620
+ p.key.type === 'Identifier' &&
1621
+ p.key.name === 'description');
1622
+ const description = descProp?.value.type === 'Literal' &&
1623
+ typeof descProp.value.value === 'string'
1624
+ ? descProp.value.value
1625
+ : undefined;
1626
+ // Find func property
1627
+ const funcProp = toolObj.properties.find((p) => p.type === 'Property' &&
1628
+ p.key.type === 'Identifier' &&
1629
+ p.key.name === 'func');
1630
+ if (!funcProp)
1631
+ continue;
1632
+ const funcValue = funcProp.value;
1633
+ if (funcValue.type !== 'ArrowFunctionExpression' &&
1634
+ funcValue.type !== 'FunctionExpression') {
1635
+ continue;
1636
+ }
1637
+ const funcExpr = funcValue;
1638
+ // Store the custom tool func info
1639
+ this.customToolFuncs.push({
1640
+ toolName,
1641
+ description,
1642
+ isAsync: funcExpr.async,
1643
+ startLine: funcExpr.loc?.start.line ?? 0,
1644
+ endLine: funcExpr.loc?.end.line ?? 0,
1645
+ startCol: funcExpr.loc?.start.column ?? 0,
1646
+ endCol: funcExpr.loc?.end.column ?? 0,
1647
+ parentBubbleVariableId: matchingBubble.variableId,
1648
+ });
1649
+ }
1650
+ }
1651
+ }
1652
+ /**
1653
+ * Mark bubbles that are inside custom tool funcs with isInsideCustomTool flag.
1654
+ */
1655
+ markBubblesInsideCustomTools(nodes) {
1656
+ for (const bubble of Object.values(nodes)) {
1657
+ // Skip ai-agent bubbles themselves
1658
+ if (bubble.bubbleName === 'ai-agent')
1659
+ continue;
1660
+ // Check if this bubble's location falls inside any custom tool func
1661
+ for (const toolFunc of this.customToolFuncs) {
1662
+ if (bubble.location.startLine >= toolFunc.startLine &&
1663
+ bubble.location.endLine <= toolFunc.endLine) {
1664
+ bubble.isInsideCustomTool = true;
1665
+ bubble.containingCustomToolId = `${toolFunc.parentBubbleVariableId}.${toolFunc.toolName}`;
1666
+ break;
1667
+ }
1668
+ }
1669
+ }
1670
+ }
1671
+ /**
1672
+ * Extract comment/description for a node by looking at preceding comments
1673
+ **/
1674
+ extractCommentForNode(node) {
1675
+ // Get the line number where this node starts
1676
+ const nodeLine = node.loc?.start.line;
1677
+ if (!nodeLine)
1678
+ return undefined;
1679
+ // Split the script into lines to find comments
1680
+ const lines = this.bubbleScript.split('\n');
1681
+ // Look backwards from the node line to find comments
1682
+ const commentLines = [];
1683
+ let currentLine = nodeLine - 1; // Start from the line before the node (0-indexed, but node.loc is 1-indexed)
1684
+ let isBlockComment = false;
1685
+ // Scan backwards to collect comment lines
1686
+ while (currentLine > 0) {
1687
+ const line = lines[currentLine - 1]?.trim(); // Convert to 0-indexed
1688
+ if (!line) {
1689
+ // Empty line - if we already have comments, stop here
1690
+ if (commentLines.length > 0)
1691
+ break;
1692
+ currentLine--;
1693
+ continue;
1694
+ }
1695
+ // Check for single-line comment (//)
1696
+ if (line.startsWith('//')) {
1697
+ commentLines.unshift(line);
1698
+ currentLine--;
1699
+ continue;
1700
+ }
1701
+ // Check if this line is part of a block comment
1702
+ if (line.startsWith('*') ||
1703
+ line.startsWith('/**') ||
1704
+ line.startsWith('/*')) {
1705
+ commentLines.unshift(line);
1706
+ isBlockComment = true;
1707
+ currentLine--;
1708
+ continue;
1709
+ }
1710
+ // Check if this line ends a block comment
1711
+ if (line.endsWith('*/')) {
1712
+ commentLines.unshift(line);
1713
+ isBlockComment = true;
1714
+ currentLine--;
1715
+ // Continue collecting the rest of the comment block
1716
+ continue;
1717
+ }
1718
+ // If we've already collected some comment lines and hit a non-comment, stop
1719
+ if (commentLines.length > 0) {
1720
+ break;
1721
+ }
1722
+ // Otherwise, this might be code - stop looking
1723
+ break;
1724
+ }
1725
+ if (commentLines.length === 0)
1726
+ return undefined;
1727
+ // Join comment lines and extract the actual text
1728
+ const fullComment = commentLines.join('\n');
1729
+ let cleaned;
1730
+ if (isBlockComment) {
1731
+ // Extract text from JSDoc-style or block comments
1732
+ // Remove /** ... */ or /* ... */ wrappers and clean up
1733
+ cleaned = fullComment
1734
+ .replace(/^\/\*\*?\s*/, '') // Remove opening /** or /*
1735
+ .replace(/\s*\*\/\s*$/, '') // Remove closing */
1736
+ .split('\n')
1737
+ .map((line) => {
1738
+ // Remove leading * and whitespace from each line
1739
+ return line.replace(/^\s*\*\s?/, '').trim();
1740
+ })
1741
+ .filter((line) => line.length > 0) // Remove empty lines
1742
+ .join(' ') // Join into single line
1743
+ .trim();
1744
+ }
1745
+ else {
1746
+ // Handle single-line comments (//)
1747
+ cleaned = fullComment
1748
+ .split('\n')
1749
+ .map((line) => {
1750
+ // Remove leading // and whitespace from each line
1751
+ return line.replace(/^\/\/\s?/, '').trim();
1752
+ })
1753
+ .filter((line) => line.length > 0) // Remove empty lines
1754
+ .join(' ') // Join into single line
1755
+ .trim();
1756
+ }
1757
+ return cleaned || undefined;
1758
+ }
1759
+ /**
1760
+ * Extract JSDoc info including description and @canBeFile tag from a node's preceding comments.
1761
+ * The @canBeFile tag controls whether file upload is enabled for string fields in the UI.
1762
+ */
1763
+ extractJSDocForNode(node) {
1764
+ // Get the line number where this node starts
1765
+ const nodeLine = node.loc?.start.line;
1766
+ if (!nodeLine)
1767
+ return {};
1768
+ // Split the script into lines to find comments
1769
+ const lines = this.bubbleScript.split('\n');
1770
+ // Look backwards from the node line to find comments
1771
+ const commentLines = [];
1772
+ let currentLine = nodeLine - 1;
1773
+ let isBlockComment = false;
1774
+ // Scan backwards to collect comment lines
1775
+ while (currentLine > 0) {
1776
+ const line = lines[currentLine - 1]?.trim();
1777
+ if (!line) {
1778
+ if (commentLines.length > 0)
1779
+ break;
1780
+ currentLine--;
1781
+ continue;
1782
+ }
1783
+ if (line.startsWith('//')) {
1784
+ commentLines.unshift(line);
1785
+ currentLine--;
1786
+ continue;
1787
+ }
1788
+ if (line.startsWith('*') ||
1789
+ line.startsWith('/**') ||
1790
+ line.startsWith('/*')) {
1791
+ commentLines.unshift(line);
1792
+ isBlockComment = true;
1793
+ currentLine--;
1794
+ continue;
1795
+ }
1796
+ if (line.endsWith('*/')) {
1797
+ commentLines.unshift(line);
1798
+ isBlockComment = true;
1799
+ currentLine--;
1800
+ continue;
1801
+ }
1802
+ if (commentLines.length > 0) {
1803
+ break;
1804
+ }
1805
+ break;
1806
+ }
1807
+ if (commentLines.length === 0)
1808
+ return {};
1809
+ const fullComment = commentLines.join('\n');
1810
+ let canBeFile;
1811
+ // Parse @canBeFile tag from the raw comment
1812
+ const canBeFileMatch = fullComment.match(/@canBeFile\s+(true|false)/i);
1813
+ if (canBeFileMatch) {
1814
+ canBeFile = canBeFileMatch[1].toLowerCase() === 'true';
1815
+ }
1816
+ let description;
1817
+ if (isBlockComment) {
1818
+ description = fullComment
1819
+ .replace(/^\/\*\*?\s*/, '')
1820
+ .replace(/\s*\*\/\s*$/, '')
1821
+ .split('\n')
1822
+ .map((line) => line.replace(/^\s*\*\s?/, '').trim())
1823
+ .filter((line) => line.length > 0 && !line.startsWith('@canBeFile'))
1824
+ .join(' ')
1825
+ .trim();
1826
+ }
1827
+ else {
1828
+ description = fullComment
1829
+ .split('\n')
1830
+ .map((line) => line.replace(/^\/\/\s?/, '').trim())
1831
+ .filter((line) => line.length > 0 && !line.startsWith('@canBeFile'))
1832
+ .join(' ')
1833
+ .trim();
1834
+ }
1835
+ return {
1836
+ description: description || undefined,
1837
+ canBeFile,
1838
+ };
1839
+ }
1840
+ /**
1841
+ * Check if a list of workflow nodes contains a terminating statement (return/throw)
1842
+ * A branch terminates if its last statement is a return or throw
1843
+ */
1844
+ branchTerminates(nodes) {
1845
+ if (nodes.length === 0)
1846
+ return false;
1847
+ const lastNode = nodes[nodes.length - 1];
1848
+ // Check if last node is a return statement
1849
+ if (lastNode.type === 'return') {
1850
+ return true;
1851
+ }
1852
+ // Check if last node is a code block containing return/throw
1853
+ if (lastNode.type === 'code_block') {
1854
+ const code = lastNode.code.trim();
1855
+ return (code.startsWith('return ') ||
1856
+ code.startsWith('return;') ||
1857
+ code === 'return' ||
1858
+ code.startsWith('throw '));
1859
+ }
1860
+ // Check nested control flow - all branches must terminate
1861
+ if (lastNode.type === 'if') {
1862
+ const thenTerminates = this.branchTerminates(lastNode.children);
1863
+ const elseTerminates = lastNode.elseBranch
1864
+ ? this.branchTerminates(lastNode.elseBranch)
1865
+ : false;
1866
+ // Both branches must terminate for the if to terminate
1867
+ return thenTerminates && elseTerminates;
1868
+ }
1869
+ if (lastNode.type === 'try_catch') {
1870
+ const tryTerminates = this.branchTerminates(lastNode.children);
1871
+ const catchTerminates = lastNode.catchBlock
1872
+ ? this.branchTerminates(lastNode.catchBlock)
1873
+ : false;
1874
+ return tryTerminates && catchTerminates;
1875
+ }
1876
+ return false;
1877
+ }
1878
+ /**
1879
+ * Build hierarchical workflow structure from AST
1880
+ */
1881
+ buildWorkflowTree(ast, bubbles, scopeManager) {
1882
+ const handleNode = this.findHandleFunctionNode(ast);
1883
+ if (!handleNode || handleNode.body.type !== 'BlockStatement') {
1884
+ // If no handle method or empty body, return empty workflow
1885
+ return {
1886
+ root: [],
1887
+ bubbles,
1888
+ };
1889
+ }
1890
+ const workflowNodes = [];
1891
+ const bubbleMap = new Map();
1892
+ for (const [id, bubble] of Object.entries(bubbles)) {
1893
+ bubbleMap.set(Number(id), bubble);
1894
+ }
1895
+ // Process statements in handle method body
1896
+ const statements = handleNode.body.body;
1897
+ for (let i = 0; i < statements.length; i++) {
1898
+ const stmt = statements[i];
1899
+ const node = this.buildWorkflowNodeFromStatement(stmt, bubbleMap, scopeManager);
1900
+ if (node) {
1901
+ // Check if this is an if with terminating then branch but no else
1902
+ // In this case, move subsequent statements into implicit else
1903
+ if (node.type === 'if' &&
1904
+ node.thenTerminates &&
1905
+ !node.elseBranch &&
1906
+ i < statements.length - 1) {
1907
+ // Collect remaining statements as implicit else branch
1908
+ const implicitElse = [];
1909
+ for (let j = i + 1; j < statements.length; j++) {
1910
+ const remainingNode = this.buildWorkflowNodeFromStatement(statements[j], bubbleMap, scopeManager);
1911
+ if (remainingNode) {
1912
+ implicitElse.push(remainingNode);
1913
+ }
1914
+ }
1915
+ if (implicitElse.length > 0) {
1916
+ node.elseBranch = implicitElse;
1917
+ }
1918
+ workflowNodes.push(node);
1919
+ break; // All remaining statements have been moved to else branch
1920
+ }
1921
+ workflowNodes.push(node);
1922
+ }
1923
+ }
1924
+ // Group consecutive nodes of the same type
1925
+ const groupedNodes = this.groupConsecutiveNodes(workflowNodes);
1926
+ return {
1927
+ root: groupedNodes,
1928
+ bubbles,
1929
+ };
1930
+ }
1931
+ /**
1932
+ * Group consecutive nodes of the same type
1933
+ * - Consecutive variable_declaration nodes → merge into one
1934
+ * - Consecutive code_block nodes → merge into one
1935
+ * - return nodes are NOT grouped (each is a distinct exit point)
1936
+ */
1937
+ groupConsecutiveNodes(nodes) {
1938
+ if (nodes.length === 0)
1939
+ return [];
1940
+ const result = [];
1941
+ let currentGroup = [];
1942
+ let currentType = null;
1943
+ for (const node of nodes) {
1944
+ // Control flow nodes break grouping
1945
+ const isControlFlow = node.type === 'if' ||
1946
+ node.type === 'for' ||
1947
+ node.type === 'while' ||
1948
+ node.type === 'try_catch' ||
1949
+ node.type === 'bubble' ||
1950
+ node.type === 'function_call';
1951
+ // Return nodes are never grouped
1952
+ const isReturn = node.type === 'return';
1953
+ // If we hit a control flow node or return, flush current group
1954
+ if (isControlFlow || isReturn) {
1955
+ if (currentGroup.length > 0) {
1956
+ result.push(...this.mergeGroup(currentGroup, currentType));
1957
+ currentGroup = [];
1958
+ currentType = null;
1959
+ }
1960
+ result.push(node);
1961
+ continue;
1962
+ }
1963
+ // Check if this node can be grouped
1964
+ const groupableType = node.type === 'variable_declaration' || node.type === 'code_block';
1965
+ if (groupableType) {
1966
+ // Don't group if node has children (e.g., function calls)
1967
+ const hasChildren = node.children && node.children.length > 0;
1968
+ if (hasChildren) {
1969
+ // Flush current group and add this node as-is
1970
+ if (currentGroup.length > 0) {
1971
+ result.push(...this.mergeGroup(currentGroup, currentType));
1972
+ currentGroup = [];
1973
+ currentType = null;
1974
+ }
1975
+ result.push(node);
1976
+ }
1977
+ else if (currentType === node.type) {
1978
+ // Same type, no children, add to group
1979
+ currentGroup.push(node);
1980
+ }
1981
+ else {
1982
+ // Different type, flush current group and start new one
1983
+ if (currentGroup.length > 0) {
1984
+ result.push(...this.mergeGroup(currentGroup, currentType));
1985
+ }
1986
+ currentGroup = [node];
1987
+ currentType = node.type;
1988
+ }
1989
+ }
1990
+ else {
1991
+ // Not groupable, flush and add as-is
1992
+ if (currentGroup.length > 0) {
1993
+ result.push(...this.mergeGroup(currentGroup, currentType));
1994
+ currentGroup = [];
1995
+ currentType = null;
1996
+ }
1997
+ result.push(node);
1998
+ }
1999
+ }
2000
+ // Flush any remaining group
2001
+ if (currentGroup.length > 0) {
2002
+ result.push(...this.mergeGroup(currentGroup, currentType));
2003
+ }
2004
+ return result;
2005
+ }
2006
+ /**
2007
+ * Merge a group of nodes of the same type into a single node
2008
+ */
2009
+ mergeGroup(group, type) {
2010
+ if (group.length === 0)
2011
+ return [];
2012
+ if (group.length === 1)
2013
+ return group;
2014
+ if (type === 'variable_declaration') {
2015
+ const first = group[0];
2016
+ const allVariables = first.variables.slice();
2017
+ let startLine = first.location.startLine;
2018
+ let startCol = first.location.startCol;
2019
+ let endLine = first.location.endLine;
2020
+ let endCol = first.location.endCol;
2021
+ let code = first.code;
2022
+ for (let i = 1; i < group.length; i++) {
2023
+ const node = group[i];
2024
+ allVariables.push(...node.variables);
2025
+ if (node.location.startLine < startLine) {
2026
+ startLine = node.location.startLine;
2027
+ startCol = node.location.startCol;
2028
+ }
2029
+ if (node.location.endLine > endLine) {
2030
+ endLine = node.location.endLine;
2031
+ endCol = node.location.endCol;
2032
+ }
2033
+ code += '\n' + node.code;
2034
+ }
2035
+ return [
2036
+ {
2037
+ type: 'variable_declaration',
2038
+ location: {
2039
+ startLine,
2040
+ startCol,
2041
+ endLine,
2042
+ endCol,
2043
+ },
2044
+ code,
2045
+ variables: allVariables,
2046
+ children: [],
2047
+ },
2048
+ ];
2049
+ }
2050
+ if (type === 'code_block') {
2051
+ const first = group[0];
2052
+ let startLine = first.location.startLine;
2053
+ let startCol = first.location.startCol;
2054
+ let endLine = first.location.endLine;
2055
+ let endCol = first.location.endCol;
2056
+ let code = first.code;
2057
+ for (let i = 1; i < group.length; i++) {
2058
+ const node = group[i];
2059
+ if (node.location.startLine < startLine) {
2060
+ startLine = node.location.startLine;
2061
+ startCol = node.location.startCol;
2062
+ }
2063
+ if (node.location.endLine > endLine) {
2064
+ endLine = node.location.endLine;
2065
+ endCol = node.location.endCol;
2066
+ }
2067
+ code += '\n' + node.code;
2068
+ }
2069
+ return [
2070
+ {
2071
+ type: 'code_block',
2072
+ location: {
2073
+ startLine,
2074
+ startCol,
2075
+ endLine,
2076
+ endCol,
2077
+ },
2078
+ code,
2079
+ children: [],
2080
+ },
2081
+ ];
2082
+ }
2083
+ // Fallback: return as-is
2084
+ return group;
2085
+ }
2086
+ /**
2087
+ * Build a workflow node from an AST statement
2088
+ */
2089
+ buildWorkflowNodeFromStatement(stmt, bubbleMap, scopeManager) {
2090
+ // Handle IfStatement
2091
+ if (stmt.type === 'IfStatement') {
2092
+ return this.buildIfNode(stmt, bubbleMap, scopeManager);
2093
+ }
2094
+ // Handle ForStatement
2095
+ if (stmt.type === 'ForStatement' ||
2096
+ stmt.type === 'ForInStatement' ||
2097
+ stmt.type === 'ForOfStatement') {
2098
+ return this.buildForNode(stmt, bubbleMap, scopeManager);
2099
+ }
2100
+ // Handle WhileStatement
2101
+ if (stmt.type === 'WhileStatement') {
2102
+ return this.buildWhileNode(stmt, bubbleMap, scopeManager);
2103
+ }
2104
+ // Handle TryStatement
2105
+ if (stmt.type === 'TryStatement') {
2106
+ return this.buildTryCatchNode(stmt, bubbleMap, scopeManager);
2107
+ }
2108
+ // Handle VariableDeclaration - check if it's a bubble, Promise.all, or function call
2109
+ if (stmt.type === 'VariableDeclaration') {
2110
+ for (const decl of stmt.declarations) {
2111
+ if (decl.init) {
2112
+ // Check if it's Promise.all (supports array destructuring)
2113
+ const promiseAll = this.detectPromiseAll(decl.init);
2114
+ if (promiseAll) {
2115
+ return this.buildParallelExecutionNode(promiseAll, stmt, bubbleMap, scopeManager);
2116
+ }
2117
+ // Handle Identifier declarations (const foo = ...)
2118
+ if (decl.id.type === 'Identifier') {
2119
+ // Try to find bubble by variable name and location
2120
+ // This handles same-named variables in different scopes by matching location
2121
+ const variableName = decl.id.name;
2122
+ const stmtStartLine = stmt.loc?.start.line ?? 0;
2123
+ const stmtEndLine = stmt.loc?.end.line ?? 0;
2124
+ const bubble = Array.from(bubbleMap.values()).find((b) => b.variableName === variableName &&
2125
+ b.location.startLine >= stmtStartLine &&
2126
+ b.location.endLine <= stmtEndLine);
2127
+ if (bubble) {
2128
+ return {
2129
+ type: 'bubble',
2130
+ variableId: bubble.variableId,
2131
+ };
2132
+ }
2133
+ // Check if initializer is a function call
2134
+ const functionCall = this.detectFunctionCall(decl.init);
2135
+ if (functionCall) {
2136
+ // If variable declaration contains a function call, represent it as function_call or transformation_function
2137
+ // The function call node will contain the full statement code
2138
+ // Variable declaration is already handled inside buildFunctionCallNode
2139
+ return this.buildFunctionCallNode(functionCall, stmt, bubbleMap, scopeManager);
2140
+ }
2141
+ // Fallback to expression matching for bubbles
2142
+ const bubbleFromExpr = this.findBubbleInExpression(decl.init, bubbleMap);
2143
+ if (bubbleFromExpr) {
2144
+ return {
2145
+ type: 'bubble',
2146
+ variableId: bubbleFromExpr.variableId,
2147
+ };
2148
+ }
2149
+ }
2150
+ else if (decl.id.type === 'ObjectPattern' ||
2151
+ decl.id.type === 'ArrayPattern') {
2152
+ // Handle destructuring declarations (const { a, b } = ... or const [a, b] = ...)
2153
+ // Check if initializer is a function call - transformation functions take precedence
2154
+ const functionCall = this.detectFunctionCall(decl.init);
2155
+ if (functionCall) {
2156
+ // If variable declaration contains a function call, represent it as function_call or transformation_function
2157
+ // The function call node will contain the full statement code
2158
+ // Variable declaration is already handled inside buildFunctionCallNode
2159
+ return this.buildFunctionCallNode(functionCall, stmt, bubbleMap, scopeManager);
2160
+ }
2161
+ // Check for bubbles in the expression
2162
+ const bubbleFromExpr = this.findBubbleInExpression(decl.init, bubbleMap);
2163
+ if (bubbleFromExpr) {
2164
+ return {
2165
+ type: 'bubble',
2166
+ variableId: bubbleFromExpr.variableId,
2167
+ };
2168
+ }
2169
+ }
2170
+ }
2171
+ }
2172
+ // If not a bubble or function call, create variable declaration node
2173
+ return this.buildVariableDeclarationNode(stmt, bubbleMap, scopeManager);
2174
+ }
2175
+ // Handle ExpressionStatement - check if it's a bubble or function call
2176
+ if (stmt.type === 'ExpressionStatement') {
2177
+ // Handle AssignmentExpression (e.g., agentResponse = await this.method())
2178
+ if (stmt.expression.type === 'AssignmentExpression') {
2179
+ const assignExpr = stmt.expression;
2180
+ // Check if right-hand side is a bubble
2181
+ const bubble = this.findBubbleInExpression(assignExpr.right, bubbleMap);
2182
+ if (bubble) {
2183
+ return {
2184
+ type: 'bubble',
2185
+ variableId: bubble.variableId,
2186
+ };
2187
+ }
2188
+ // Check if right-hand side is a function call
2189
+ const functionCall = this.detectFunctionCall(assignExpr.right);
2190
+ if (functionCall) {
2191
+ return this.buildFunctionCallNode(functionCall, stmt, bubbleMap, scopeManager);
2192
+ }
2193
+ }
2194
+ else {
2195
+ // Regular expression (not assignment)
2196
+ const bubble = this.findBubbleInExpression(stmt.expression, bubbleMap);
2197
+ if (bubble) {
2198
+ return {
2199
+ type: 'bubble',
2200
+ variableId: bubble.variableId,
2201
+ };
2202
+ }
2203
+ // Check for function calls
2204
+ const functionCall = this.detectFunctionCall(stmt.expression);
2205
+ if (functionCall) {
2206
+ return this.buildFunctionCallNode(functionCall, stmt, bubbleMap, scopeManager);
2207
+ }
2208
+ }
2209
+ // If not a bubble or function call, treat as code block
2210
+ return this.buildCodeBlockNode(stmt, bubbleMap, scopeManager);
2211
+ }
2212
+ // Handle ReturnStatement
2213
+ if (stmt.type === 'ReturnStatement') {
2214
+ if (stmt.argument) {
2215
+ const bubble = this.findBubbleInExpression(stmt.argument, bubbleMap);
2216
+ if (bubble) {
2217
+ return {
2218
+ type: 'bubble',
2219
+ variableId: bubble.variableId,
2220
+ };
2221
+ }
2222
+ // Check if return value is a function call
2223
+ const functionCall = this.detectFunctionCall(stmt.argument);
2224
+ if (functionCall) {
2225
+ // Create return node with function call as child
2226
+ const returnNode = this.buildReturnNode(stmt, bubbleMap, scopeManager);
2227
+ if (returnNode) {
2228
+ const funcCallNode = this.buildFunctionCallNode(functionCall, stmt, bubbleMap, scopeManager);
2229
+ if (funcCallNode) {
2230
+ returnNode.children = [funcCallNode];
2231
+ }
2232
+ return returnNode;
2233
+ }
2234
+ }
2235
+ }
2236
+ return this.buildReturnNode(stmt, bubbleMap, scopeManager);
2237
+ }
2238
+ // Default: treat as code block
2239
+ return this.buildCodeBlockNode(stmt, bubbleMap, scopeManager);
2240
+ }
2241
+ /**
2242
+ * Build an if node from IfStatement
2243
+ */
2244
+ buildIfNode(stmt, bubbleMap, scopeManager) {
2245
+ const condition = this.extractConditionString(stmt.test);
2246
+ const location = this.extractLocation(stmt);
2247
+ const children = [];
2248
+ if (stmt.consequent.type === 'BlockStatement') {
2249
+ for (const childStmt of stmt.consequent.body) {
2250
+ const node = this.buildWorkflowNodeFromStatement(childStmt, bubbleMap, scopeManager);
2251
+ if (node) {
2252
+ children.push(node);
2253
+ }
2254
+ }
2255
+ }
2256
+ else {
2257
+ // Single statement (no braces)
2258
+ const node = this.buildWorkflowNodeFromStatement(stmt.consequent, bubbleMap, scopeManager);
2259
+ if (node) {
2260
+ children.push(node);
2261
+ }
2262
+ }
2263
+ const elseBranch = stmt.alternate
2264
+ ? (() => {
2265
+ if (stmt.alternate.type === 'BlockStatement') {
2266
+ const nodes = [];
2267
+ for (const childStmt of stmt.alternate.body) {
2268
+ const node = this.buildWorkflowNodeFromStatement(childStmt, bubbleMap, scopeManager);
2269
+ if (node) {
2270
+ nodes.push(node);
2271
+ }
2272
+ }
2273
+ return nodes;
2274
+ }
2275
+ else if (stmt.alternate.type === 'IfStatement') {
2276
+ // else if - treat as nested if
2277
+ const node = this.buildIfNode(stmt.alternate, bubbleMap, scopeManager);
2278
+ return [node];
2279
+ }
2280
+ else {
2281
+ // Single statement else
2282
+ const node = this.buildWorkflowNodeFromStatement(stmt.alternate, bubbleMap, scopeManager);
2283
+ return node ? [node] : [];
2284
+ }
2285
+ })()
2286
+ : undefined;
2287
+ // Check if branches terminate (contain return/throw)
2288
+ const thenTerminates = this.branchTerminates(children);
2289
+ const elseTerminates = elseBranch
2290
+ ? this.branchTerminates(elseBranch)
2291
+ : false;
2292
+ return {
2293
+ type: 'if',
2294
+ location,
2295
+ condition,
2296
+ children,
2297
+ elseBranch,
2298
+ thenTerminates: thenTerminates || undefined,
2299
+ elseTerminates: elseTerminates || undefined,
2300
+ };
2301
+ }
2302
+ /**
2303
+ * Build a for node from ForStatement/ForInStatement/ForOfStatement
2304
+ */
2305
+ buildForNode(stmt, bubbleMap, scopeManager) {
2306
+ const location = this.extractLocation(stmt);
2307
+ let condition;
2308
+ if (stmt.type === 'ForStatement') {
2309
+ const init = stmt.init
2310
+ ? this.bubbleScript.substring(stmt.init.range[0], stmt.init.range[1])
2311
+ : '';
2312
+ const test = stmt.test
2313
+ ? this.bubbleScript.substring(stmt.test.range[0], stmt.test.range[1])
2314
+ : '';
2315
+ const update = stmt.update
2316
+ ? this.bubbleScript.substring(stmt.update.range[0], stmt.update.range[1])
2317
+ : '';
2318
+ condition = `${init}; ${test}; ${update}`.trim();
2319
+ }
2320
+ else if (stmt.type === 'ForInStatement') {
2321
+ const left = this.bubbleScript.substring(stmt.left.range[0], stmt.left.range[1]);
2322
+ const right = this.bubbleScript.substring(stmt.right.range[0], stmt.right.range[1]);
2323
+ condition = `${left} in ${right}`;
2324
+ }
2325
+ else if (stmt.type === 'ForOfStatement') {
2326
+ const left = this.bubbleScript.substring(stmt.left.range[0], stmt.left.range[1]);
2327
+ const right = this.bubbleScript.substring(stmt.right.range[0], stmt.right.range[1]);
2328
+ condition = `${left} of ${right}`;
2329
+ }
2330
+ const children = [];
2331
+ if (stmt.body.type === 'BlockStatement') {
2332
+ for (const childStmt of stmt.body.body) {
2333
+ const node = this.buildWorkflowNodeFromStatement(childStmt, bubbleMap, scopeManager);
2334
+ if (node) {
2335
+ children.push(node);
2336
+ }
2337
+ }
2338
+ }
2339
+ else {
2340
+ // Single statement (no braces)
2341
+ const node = this.buildWorkflowNodeFromStatement(stmt.body, bubbleMap, scopeManager);
2342
+ if (node) {
2343
+ children.push(node);
2344
+ }
2345
+ }
2346
+ return {
2347
+ type: stmt.type === 'ForOfStatement' ? 'for' : 'for',
2348
+ location,
2349
+ condition,
2350
+ children,
2351
+ };
2352
+ }
2353
+ /**
2354
+ * Build a while node from WhileStatement
2355
+ */
2356
+ buildWhileNode(stmt, bubbleMap, scopeManager) {
2357
+ const location = this.extractLocation(stmt);
2358
+ const condition = this.extractConditionString(stmt.test);
2359
+ const children = [];
2360
+ if (stmt.body.type === 'BlockStatement') {
2361
+ for (const childStmt of stmt.body.body) {
2362
+ const node = this.buildWorkflowNodeFromStatement(childStmt, bubbleMap, scopeManager);
2363
+ if (node) {
2364
+ children.push(node);
2365
+ }
2366
+ }
2367
+ }
2368
+ else {
2369
+ // Single statement (no braces)
2370
+ const node = this.buildWorkflowNodeFromStatement(stmt.body, bubbleMap, scopeManager);
2371
+ if (node) {
2372
+ children.push(node);
2373
+ }
2374
+ }
2375
+ return {
2376
+ type: 'while',
2377
+ location,
2378
+ condition,
2379
+ children,
2380
+ };
2381
+ }
2382
+ /**
2383
+ * Build a try-catch node from TryStatement
2384
+ */
2385
+ buildTryCatchNode(stmt, bubbleMap, scopeManager) {
2386
+ const location = this.extractLocation(stmt);
2387
+ const children = [];
2388
+ if (stmt.block.type === 'BlockStatement') {
2389
+ for (const childStmt of stmt.block.body) {
2390
+ const node = this.buildWorkflowNodeFromStatement(childStmt, bubbleMap, scopeManager);
2391
+ if (node) {
2392
+ children.push(node);
2393
+ }
2394
+ }
2395
+ }
2396
+ const catchBlock = stmt.handler
2397
+ ? (() => {
2398
+ if (stmt.handler.body.type === 'BlockStatement') {
2399
+ const nodes = [];
2400
+ for (const childStmt of stmt.handler.body.body) {
2401
+ const node = this.buildWorkflowNodeFromStatement(childStmt, bubbleMap, scopeManager);
2402
+ if (node) {
2403
+ nodes.push(node);
2404
+ }
2405
+ }
2406
+ return nodes;
2407
+ }
2408
+ return [];
2409
+ })()
2410
+ : undefined;
2411
+ return {
2412
+ type: 'try_catch',
2413
+ location,
2414
+ children,
2415
+ catchBlock,
2416
+ };
2417
+ }
2418
+ /**
2419
+ * Build a code block node from a statement
2420
+ */
2421
+ buildCodeBlockNode(stmt, bubbleMap, scopeManager) {
2422
+ const location = this.extractLocation(stmt);
2423
+ if (!location)
2424
+ return null;
2425
+ const code = this.bubbleScript.substring(stmt.range[0], stmt.range[1]);
2426
+ // Check for nested structures
2427
+ const children = [];
2428
+ if (stmt.type === 'BlockStatement') {
2429
+ for (const childStmt of stmt.body) {
2430
+ const node = this.buildWorkflowNodeFromStatement(childStmt, bubbleMap, scopeManager);
2431
+ if (node) {
2432
+ children.push(node);
2433
+ }
2434
+ }
2435
+ }
2436
+ return {
2437
+ type: 'code_block',
2438
+ location,
2439
+ code,
2440
+ children,
2441
+ };
2442
+ }
2443
+ /**
2444
+ * Find a bubble in an expression by checking if it matches any parsed bubble
2445
+ */
2446
+ findBubbleInExpression(expr, bubbleMap) {
2447
+ if (!expr.loc)
2448
+ return null;
2449
+ // Extract the NewExpression from the expression (handles await, .action(), etc.)
2450
+ const newExpr = this.extractNewExpression(expr);
2451
+ if (!newExpr || !newExpr.loc)
2452
+ return null;
2453
+ // Match by NewExpression location (this is what bubbles are stored with)
2454
+ for (const bubble of bubbleMap.values()) {
2455
+ // Check if the NewExpression location overlaps with bubble location
2456
+ // Use a tolerance for column matching since the exact column might differ slightly
2457
+ if (bubble.location.startLine === newExpr.loc.start.line &&
2458
+ bubble.location.endLine === newExpr.loc.end.line &&
2459
+ Math.abs(bubble.location.startCol - newExpr.loc.start.column) <= 5) {
2460
+ return bubble;
2461
+ }
2462
+ }
2463
+ return null;
2464
+ }
2465
+ /**
2466
+ * Extract the NewExpression from an expression, handling await, .action(), etc.
2467
+ */
2468
+ extractNewExpression(expr) {
2469
+ // Handle await new X()
2470
+ if (expr.type === 'AwaitExpression' && expr.argument) {
2471
+ return this.extractNewExpression(expr.argument);
2472
+ }
2473
+ // Handle new X().action()
2474
+ if (expr.type === 'CallExpression' &&
2475
+ expr.callee.type === 'MemberExpression') {
2476
+ if (expr.callee.object) {
2477
+ return this.extractNewExpression(expr.callee.object);
2478
+ }
2479
+ }
2480
+ // Direct NewExpression
2481
+ if (expr.type === 'NewExpression') {
2482
+ return expr;
2483
+ }
2484
+ return null;
2485
+ }
2486
+ /**
2487
+ * Build a variable declaration node from a VariableDeclaration statement
2488
+ */
2489
+ buildVariableDeclarationNode(stmt, _bubbleMap, _scopeManager) {
2490
+ const location = this.extractLocation(stmt);
2491
+ if (!location)
2492
+ return null;
2493
+ const code = this.bubbleScript.substring(stmt.range[0], stmt.range[1]);
2494
+ const variables = [];
2495
+ for (const decl of stmt.declarations) {
2496
+ if (decl.id.type === 'Identifier') {
2497
+ variables.push({
2498
+ name: decl.id.name,
2499
+ type: stmt.kind,
2500
+ hasInitializer: decl.init !== null && decl.init !== undefined,
2501
+ });
2502
+ }
2503
+ }
2504
+ return {
2505
+ type: 'variable_declaration',
2506
+ location,
2507
+ code,
2508
+ variables,
2509
+ children: [],
2510
+ };
2511
+ }
2512
+ /**
2513
+ * Build a return node from a ReturnStatement
2514
+ */
2515
+ buildReturnNode(stmt, _bubbleMap, _scopeManager) {
2516
+ const location = this.extractLocation(stmt);
2517
+ if (!location)
2518
+ return null;
2519
+ const code = this.bubbleScript.substring(stmt.range[0], stmt.range[1]);
2520
+ const value = stmt.argument
2521
+ ? this.bubbleScript.substring(stmt.argument.range[0], stmt.argument.range[1])
2522
+ : undefined;
2523
+ return {
2524
+ type: 'return',
2525
+ location,
2526
+ code,
2527
+ value,
2528
+ children: [],
2529
+ };
2530
+ }
2531
+ /**
2532
+ * Detect if an expression is Promise.all([...]) or Promise.all(variable)
2533
+ */
2534
+ detectPromiseAll(expr) {
2535
+ // Handle await Promise.all([...])
2536
+ let callExpr = null;
2537
+ if (expr.type === 'AwaitExpression' && expr.argument) {
2538
+ if (expr.argument.type === 'CallExpression') {
2539
+ callExpr = expr.argument;
2540
+ }
2541
+ }
2542
+ else if (expr.type === 'CallExpression') {
2543
+ callExpr = expr;
2544
+ }
2545
+ if (!callExpr)
2546
+ return null;
2547
+ // Check if it's Promise.all
2548
+ const callee = callExpr.callee;
2549
+ if (callee.type === 'MemberExpression' &&
2550
+ callee.object.type === 'Identifier' &&
2551
+ callee.object.name === 'Promise' &&
2552
+ callee.property.type === 'Identifier' &&
2553
+ callee.property.name === 'all') {
2554
+ // Check if the first argument is an array or variable
2555
+ if (callExpr.arguments.length > 0) {
2556
+ const arg = callExpr.arguments[0];
2557
+ if (arg.type === 'ArrayExpression') {
2558
+ return {
2559
+ callExpr,
2560
+ arrayExpr: arg,
2561
+ };
2562
+ }
2563
+ if (arg.type === 'Identifier') {
2564
+ return {
2565
+ callExpr,
2566
+ arrayExpr: arg,
2567
+ };
2568
+ }
2569
+ }
2570
+ }
2571
+ return null;
2572
+ }
2573
+ /**
2574
+ * Detect if an expression is a function call
2575
+ */
2576
+ detectFunctionCall(expr) {
2577
+ // Handle await functionCall()
2578
+ let callExpr = null;
2579
+ if (expr.type === 'AwaitExpression' && expr.argument) {
2580
+ if (expr.argument.type === 'CallExpression') {
2581
+ callExpr = expr.argument;
2582
+ }
2583
+ }
2584
+ else if (expr.type === 'CallExpression') {
2585
+ callExpr = expr;
2586
+ }
2587
+ if (!callExpr)
2588
+ return null;
2589
+ const callee = callExpr.callee;
2590
+ let functionName = null;
2591
+ let isMethodCall = false;
2592
+ if (callee.type === 'Identifier') {
2593
+ // Direct function call: functionName()
2594
+ functionName = callee.name;
2595
+ isMethodCall = false;
2596
+ }
2597
+ else if (callee.type === 'MemberExpression') {
2598
+ // Method call: this.methodName() or obj.methodName()
2599
+ if (callee.object.type === 'ThisExpression' &&
2600
+ callee.property.type === 'Identifier') {
2601
+ functionName = callee.property.name;
2602
+ isMethodCall = true;
2603
+ }
2604
+ else if (callee.property.type === 'Identifier') {
2605
+ functionName = callee.property.name;
2606
+ isMethodCall = false; // External method call, not this.method()
2607
+ }
2608
+ }
2609
+ if (!functionName)
2610
+ return null;
2611
+ const args = callExpr.arguments
2612
+ .map((arg) => this.bubbleScript.substring(arg.range[0], arg.range[1]))
2613
+ .join(', ');
2614
+ return {
2615
+ functionName,
2616
+ isMethodCall,
2617
+ arguments: args,
2618
+ callExpr,
2619
+ };
2620
+ }
2621
+ /**
2622
+ * Find a method definition in the class by name
2623
+ */
2624
+ findMethodDefinition(methodName, ast) {
2625
+ const mainClass = this.findMainBubbleFlowClass(ast);
2626
+ if (!mainClass || !mainClass.body)
2627
+ return null;
2628
+ for (const member of mainClass.body.body) {
2629
+ if (member.type === 'MethodDefinition' &&
2630
+ member.key.type === 'Identifier' &&
2631
+ member.key.name === methodName &&
2632
+ member.value.type === 'FunctionExpression') {
2633
+ const func = member.value;
2634
+ const body = func.body.type === 'BlockStatement' ? func.body : null;
2635
+ const isAsync = func.async || false;
2636
+ const parameters = func.params
2637
+ .map((param) => {
2638
+ if (param.type === 'Identifier')
2639
+ return param.name;
2640
+ if (param.type === 'AssignmentPattern' &&
2641
+ param.left.type === 'Identifier')
2642
+ return param.left.name;
2643
+ return '';
2644
+ })
2645
+ .filter((name) => name !== '');
2646
+ return {
2647
+ method: member,
2648
+ body,
2649
+ isAsync,
2650
+ parameters,
2651
+ };
2652
+ }
2653
+ }
2654
+ return null;
2655
+ }
2656
+ /**
2657
+ * Check if a workflow node tree contains any bubbles (recursively)
2658
+ */
2659
+ containsBubbles(nodes) {
2660
+ for (const node of nodes) {
2661
+ if (node.type === 'bubble') {
2662
+ return true;
2663
+ }
2664
+ // Check children recursively
2665
+ if ('children' in node && Array.isArray(node.children)) {
2666
+ if (this.containsBubbles(node.children)) {
2667
+ return true;
2668
+ }
2669
+ }
2670
+ // Check elseBranch for if statements
2671
+ if (node.type === 'if' && node.elseBranch) {
2672
+ if (this.containsBubbles(node.elseBranch)) {
2673
+ return true;
2674
+ }
2675
+ }
2676
+ // Check catchBlock for try_catch
2677
+ if (node.type === 'try_catch' && node.catchBlock) {
2678
+ if (this.containsBubbles(node.catchBlock)) {
2679
+ return true;
2680
+ }
2681
+ }
2682
+ }
2683
+ return false;
2684
+ }
2685
+ /**
2686
+ * Build a function call node from a function call expression
2687
+ */
2688
+ buildFunctionCallNode(callInfo, stmt, bubbleMap, scopeManager) {
2689
+ const location = this.extractLocation(stmt);
2690
+ if (!location)
2691
+ return null;
2692
+ const code = this.bubbleScript.substring(stmt.range[0], stmt.range[1]);
2693
+ // Try to find method definition if it's a method call
2694
+ let methodDefinition = undefined;
2695
+ const methodChildren = [];
2696
+ const children = [];
2697
+ let description = undefined;
2698
+ const methodBubbleMap = new Map();
2699
+ if (callInfo.isMethodCall && this.cachedAST) {
2700
+ const methodDef = this.findMethodDefinition(callInfo.functionName, this.cachedAST);
2701
+ if (methodDef && methodDef.body) {
2702
+ methodDefinition = {
2703
+ location: {
2704
+ startLine: methodDef.method.loc?.start.line || 0,
2705
+ endLine: methodDef.method.loc?.end.line || 0,
2706
+ },
2707
+ isAsync: methodDef.isAsync,
2708
+ parameters: methodDef.parameters,
2709
+ };
2710
+ // Extract description from method comments
2711
+ description = this.extractCommentForNode(methodDef.method);
2712
+ // Filter bubbleMap to only include bubbles within this method's scope
2713
+ const methodStartLine = methodDef.method.loc?.start.line || 0;
2714
+ const methodEndLine = methodDef.method.loc?.end.line || 0;
2715
+ for (const [id, bubble] of bubbleMap.entries()) {
2716
+ // Include bubble if it's within the method's line range
2717
+ if (bubble.location.startLine >= methodStartLine &&
2718
+ bubble.location.endLine <= methodEndLine) {
2719
+ methodBubbleMap.set(id, bubble);
2720
+ }
2721
+ }
2722
+ // Recursively build workflow nodes from method body
2723
+ for (const childStmt of methodDef.body.body) {
2724
+ const node = this.buildWorkflowNodeFromStatement(childStmt, methodBubbleMap, scopeManager);
2725
+ if (node) {
2726
+ methodChildren.push(node);
2727
+ }
2728
+ }
2729
+ }
2730
+ }
2731
+ const shouldTrackInvocation = callInfo.isMethodCall && !!methodDefinition;
2732
+ // Pass the call expression's start offset to deduplicate when the same call
2733
+ // is processed multiple times (e.g., .map() callback processing vs Promise.all resolution)
2734
+ const callExprStartOffset = callInfo.callExpr.range?.[0];
2735
+ const invocationIndex = shouldTrackInvocation
2736
+ ? this.getNextInvocationIndex(callInfo.functionName, callExprStartOffset)
2737
+ : 0;
2738
+ const callSiteKey = shouldTrackInvocation && invocationIndex > 0
2739
+ ? buildCallSiteKey(callInfo.functionName, invocationIndex)
2740
+ : null;
2741
+ const invocationCloneMap = callSiteKey !== null ? new Map() : null;
2742
+ const fallbackCallSiteKey = `${callInfo.functionName}:${location.startLine}:${location.startCol}`;
2743
+ if (methodChildren.length > 0) {
2744
+ if (callSiteKey && methodBubbleMap.size > 0 && invocationCloneMap) {
2745
+ const clonedMethodChildren = this.cloneWorkflowNodesForInvocation(methodChildren, callSiteKey, methodBubbleMap, invocationCloneMap);
2746
+ children.push(...clonedMethodChildren);
2747
+ }
2748
+ else {
2749
+ children.push(...methodChildren);
2750
+ }
2751
+ }
2752
+ // After method definition processing, check for callback arguments
2753
+ if (callInfo.callExpr && callInfo.callExpr.arguments.length > 0) {
2754
+ for (const arg of callInfo.callExpr.arguments) {
2755
+ if (arg.type === 'ArrowFunctionExpression' ||
2756
+ arg.type === 'FunctionExpression') {
2757
+ const callbackBody = this.extractCallbackBody(arg);
2758
+ if (callbackBody && callbackBody.length > 0) {
2759
+ const callbackStartLine = arg.loc?.start.line || 0;
2760
+ const callbackEndLine = arg.loc?.end.line || 0;
2761
+ const callbackBubbleMap = new Map();
2762
+ for (const [id, bubble] of bubbleMap.entries()) {
2763
+ if (bubble.location.startLine >= callbackStartLine &&
2764
+ bubble.location.endLine <= callbackEndLine) {
2765
+ callbackBubbleMap.set(id, bubble);
2766
+ }
2767
+ }
2768
+ const callbackNodes = [];
2769
+ for (const callbackStmt of callbackBody) {
2770
+ const node = this.buildWorkflowNodeFromStatement(callbackStmt, callbackBubbleMap, scopeManager);
2771
+ if (node) {
2772
+ callbackNodes.push(node);
2773
+ }
2774
+ }
2775
+ if (callSiteKey && callbackNodes.length > 0 && invocationCloneMap) {
2776
+ const clonedCallbacks = this.cloneWorkflowNodesForInvocation(callbackNodes, callSiteKey, callbackBubbleMap, invocationCloneMap);
2777
+ children.push(...clonedCallbacks);
2778
+ }
2779
+ else {
2780
+ children.push(...callbackNodes);
2781
+ }
2782
+ }
2783
+ }
2784
+ }
2785
+ }
2786
+ // Check if this function call contains any bubbles
2787
+ // Only create transformation_function if:
2788
+ // 1. It's a method call (this.methodName())
2789
+ // 2. A method definition was found (method exists in class)
2790
+ // 3. It has no bubbles in its children
2791
+ if (callInfo.isMethodCall &&
2792
+ methodDefinition &&
2793
+ !this.containsBubbles(children)) {
2794
+ // Get the entire method body code
2795
+ let fullCode = code;
2796
+ if (this.cachedAST) {
2797
+ const methodDef = this.findMethodDefinition(callInfo.functionName, this.cachedAST);
2798
+ if (methodDef && methodDef.body) {
2799
+ // Extract the entire method body code
2800
+ fullCode = this.bubbleScript.substring(methodDef.body.range[0], methodDef.body.range[1]);
2801
+ }
2802
+ }
2803
+ const idKey = callSiteKey ?? fallbackCallSiteKey;
2804
+ const variableId = hashToVariableId(idKey);
2805
+ const transformationNode = {
2806
+ type: 'transformation_function',
2807
+ location,
2808
+ code: fullCode,
2809
+ functionName: callInfo.functionName,
2810
+ isMethodCall: callInfo.isMethodCall,
2811
+ description,
2812
+ arguments: callInfo.arguments,
2813
+ variableId,
2814
+ methodDefinition,
2815
+ };
2816
+ // Add variable declaration if present
2817
+ if (stmt.type === 'VariableDeclaration' && stmt.declarations.length > 0) {
2818
+ const decl = stmt.declarations[0];
2819
+ if (decl.id.type === 'Identifier') {
2820
+ transformationNode.variableDeclaration = {
2821
+ variableName: decl.id.name,
2822
+ variableType: stmt.kind,
2823
+ };
2824
+ }
2825
+ }
2826
+ else if (stmt.type === 'ExpressionStatement' &&
2827
+ stmt.expression.type === 'AssignmentExpression' &&
2828
+ stmt.expression.left.type === 'Identifier') {
2829
+ transformationNode.variableDeclaration = {
2830
+ variableName: stmt.expression.left.name,
2831
+ variableType: 'let', // Assignment implies let/var, default to let
2832
+ };
2833
+ }
2834
+ return transformationNode;
2835
+ }
2836
+ const variableId = hashToVariableId(callSiteKey ?? fallbackCallSiteKey);
2837
+ const functionCallNode = {
2838
+ type: 'function_call',
2839
+ location,
2840
+ functionName: callInfo.functionName,
2841
+ isMethodCall: callInfo.isMethodCall,
2842
+ description,
2843
+ arguments: callInfo.arguments,
2844
+ code,
2845
+ variableId,
2846
+ methodDefinition,
2847
+ children,
2848
+ };
2849
+ // Add variable declaration if present
2850
+ if (stmt.type === 'VariableDeclaration' && stmt.declarations.length > 0) {
2851
+ const decl = stmt.declarations[0];
2852
+ if (decl.id.type === 'Identifier') {
2853
+ functionCallNode.variableDeclaration = {
2854
+ variableName: decl.id.name,
2855
+ variableType: stmt.kind,
2856
+ };
2857
+ }
2858
+ }
2859
+ else if (stmt.type === 'ExpressionStatement' &&
2860
+ stmt.expression.type === 'AssignmentExpression' &&
2861
+ stmt.expression.left.type === 'Identifier') {
2862
+ functionCallNode.variableDeclaration = {
2863
+ variableName: stmt.expression.left.name,
2864
+ variableType: 'let', // Assignment implies let/var, default to let
2865
+ };
2866
+ }
2867
+ return functionCallNode;
2868
+ }
2869
+ /**
2870
+ * Extract the body of a callback function (arrow or regular function expression)
2871
+ * Handles both block statements and concise arrow functions
2872
+ */
2873
+ extractCallbackBody(func) {
2874
+ // Handle block statement body: (x) => { statements }
2875
+ if (func.body.type === 'BlockStatement') {
2876
+ return func.body.body;
2877
+ }
2878
+ // Handle concise arrow function: (x) => expression
2879
+ // Convert expression to a synthetic return statement
2880
+ const syntheticReturn = {
2881
+ type: AST_NODE_TYPES.ReturnStatement,
2882
+ argument: func.body,
2883
+ range: func.body.range,
2884
+ loc: func.body.loc,
2885
+ parent: func,
2886
+ };
2887
+ return [syntheticReturn];
2888
+ }
2889
+ /**
2890
+ * Find array elements from .push() calls or .map() callbacks
2891
+ * Handles both patterns:
2892
+ * - .push(): array.push(item1, item2, ...)
2893
+ * - .map(): const promises = items.map(item => this.processItem(item))
2894
+ */
2895
+ findArrayElements(arrayVarName, ast, contextLine, scopeManager) {
2896
+ const elements = [];
2897
+ const varId = this.findVariableIdByName(arrayVarName, contextLine, scopeManager);
2898
+ if (varId === undefined)
2899
+ return elements;
2900
+ const walk = (node) => {
2901
+ // Handle .push() calls: array.push(item1, item2, ...)
2902
+ if (node.type === 'CallExpression' &&
2903
+ node.callee.type === 'MemberExpression' &&
2904
+ node.callee.property.type === 'Identifier' &&
2905
+ node.callee.property.name === 'push' &&
2906
+ node.callee.object.type === 'Identifier' &&
2907
+ node.callee.object.name === arrayVarName) {
2908
+ const callLine = node.loc?.start.line || 0;
2909
+ if (this.findVariableIdByName(arrayVarName, callLine, scopeManager) ===
2910
+ varId) {
2911
+ node.arguments.forEach((arg) => {
2912
+ elements.push(arg);
2913
+ });
2914
+ }
2915
+ }
2916
+ // Handle .map() calls: const promises = items.map(item => ...)
2917
+ if (node.type === 'VariableDeclaration') {
2918
+ for (const decl of node.declarations) {
2919
+ if (decl.id.type === 'Identifier' &&
2920
+ decl.id.name === arrayVarName &&
2921
+ decl.init &&
2922
+ decl.init.type === 'CallExpression' &&
2923
+ decl.init.callee.type === 'MemberExpression' &&
2924
+ decl.init.callee.property.type === 'Identifier' &&
2925
+ decl.init.callee.property.name === 'map') {
2926
+ const declLine = node.loc?.start.line || 0;
2927
+ if (this.findVariableIdByName(arrayVarName, declLine, scopeManager) === varId) {
2928
+ if (decl.init.arguments.length > 0) {
2929
+ const callback = decl.init.arguments[0];
2930
+ const callbackExpr = this.extractCallbackExpression(callback);
2931
+ if (callbackExpr) {
2932
+ const sourceArray = decl.init.callee.object;
2933
+ const sourceElements = this.getSourceArrayElements(sourceArray, ast, declLine, scopeManager);
2934
+ // Create one expression per source element, or single fallback
2935
+ const count = sourceElements?.length || 1;
2936
+ for (let i = 0; i < count; i++) {
2937
+ elements.push(callbackExpr);
2938
+ }
2939
+ }
2940
+ }
2941
+ }
2942
+ }
2943
+ }
2944
+ }
2945
+ for (const key in node) {
2946
+ const child = node[key];
2947
+ if (Array.isArray(child))
2948
+ child.forEach(walk);
2949
+ else if (child?.type)
2950
+ walk(child);
2951
+ }
2952
+ };
2953
+ walk(ast);
2954
+ return elements;
2955
+ }
2956
+ /**
2957
+ * Extract expression from callback function
2958
+ */
2959
+ extractCallbackExpression(callback) {
2960
+ if (callback.type === 'ArrowFunctionExpression' &&
2961
+ callback.body.type !== 'BlockStatement') {
2962
+ return callback.body;
2963
+ }
2964
+ if ((callback.type === 'ArrowFunctionExpression' ||
2965
+ callback.type === 'FunctionExpression') &&
2966
+ callback.body.type === 'BlockStatement') {
2967
+ const returns = this.findReturnStatements(callback.body);
2968
+ return returns[0]?.argument;
2969
+ }
2970
+ return null;
2971
+ }
2972
+ /**
2973
+ * Get elements from source array (literal or variable)
2974
+ */
2975
+ getSourceArrayElements(sourceArray, ast, contextLine, scopeManager) {
2976
+ if (sourceArray.type === 'ArrayExpression') {
2977
+ return sourceArray.elements.filter((el) => el !== null && el.type !== 'SpreadElement');
2978
+ }
2979
+ if (sourceArray.type === 'Identifier') {
2980
+ const varId = this.findVariableIdByName(sourceArray.name, contextLine, scopeManager);
2981
+ if (varId === undefined)
2982
+ return null;
2983
+ const walk = (node) => {
2984
+ if (node.type === 'VariableDeclaration') {
2985
+ for (const decl of node.declarations) {
2986
+ if (decl.id.type === 'Identifier' &&
2987
+ decl.id.name === sourceArray.name &&
2988
+ decl.init?.type === 'ArrayExpression' &&
2989
+ this.findVariableIdByName(sourceArray.name, node.loc?.start.line || 0, scopeManager) === varId) {
2990
+ return decl.init.elements.filter((el) => el !== null && el.type !== 'SpreadElement');
2991
+ }
2992
+ }
2993
+ }
2994
+ for (const key in node) {
2995
+ const child = node[key];
2996
+ if (Array.isArray(child)) {
2997
+ for (const c of child) {
2998
+ const result = walk(c);
2999
+ if (result)
3000
+ return result;
3001
+ }
3002
+ }
3003
+ else if (child?.type) {
3004
+ const result = walk(child);
3005
+ if (result)
3006
+ return result;
3007
+ }
3008
+ }
3009
+ return null;
3010
+ };
3011
+ return walk(ast);
3012
+ }
3013
+ return null;
3014
+ }
3015
+ /**
3016
+ * Find all return statements in a block statement
3017
+ */
3018
+ findReturnStatements(block) {
3019
+ const returns = [];
3020
+ const walk = (node) => {
3021
+ if (node.type === 'ReturnStatement') {
3022
+ returns.push(node);
3023
+ }
3024
+ for (const key in node) {
3025
+ const child = node[key];
3026
+ if (Array.isArray(child))
3027
+ child.forEach(walk);
3028
+ else if (child?.type)
3029
+ walk(child);
3030
+ }
3031
+ };
3032
+ walk(block);
3033
+ return returns;
3034
+ }
3035
+ /**
3036
+ * Build a parallel execution node from Promise.all()
3037
+ */
3038
+ buildParallelExecutionNode(promiseAllInfo, stmt, bubbleMap, scopeManager) {
3039
+ const location = this.extractLocation(stmt);
3040
+ if (!location)
3041
+ return null;
3042
+ const code = this.bubbleScript.substring(stmt.range[0], stmt.range[1]);
3043
+ const children = [];
3044
+ // Handle variable reference (e.g., Promise.all(exampleScrapers))
3045
+ if (promiseAllInfo.arrayExpr.type === 'Identifier' && this.cachedAST) {
3046
+ const arrayVarName = promiseAllInfo.arrayExpr.name;
3047
+ const contextLine = promiseAllInfo.arrayExpr.loc?.start.line || 0;
3048
+ const pushedArgs = this.findArrayElements(arrayVarName, this.cachedAST, contextLine, scopeManager);
3049
+ for (const arg of pushedArgs) {
3050
+ const methodCall = this.detectFunctionCall(arg);
3051
+ if (methodCall) {
3052
+ const syntheticStmt = {
3053
+ type: AST_NODE_TYPES.ExpressionStatement,
3054
+ expression: methodCall.callExpr,
3055
+ range: arg.range,
3056
+ loc: arg.loc,
3057
+ parent: stmt,
3058
+ };
3059
+ const funcCallNode = this.buildFunctionCallNode(methodCall, syntheticStmt, bubbleMap, scopeManager);
3060
+ if (funcCallNode)
3061
+ children.push(funcCallNode);
3062
+ }
3063
+ else {
3064
+ const bubble = this.findBubbleInExpression(arg, bubbleMap);
3065
+ if (bubble) {
3066
+ children.push({ type: 'bubble', variableId: bubble.variableId });
3067
+ }
3068
+ }
3069
+ }
3070
+ }
3071
+ else if (promiseAllInfo.arrayExpr.type === 'ArrayExpression') {
3072
+ // Handle direct array literal (existing logic)
3073
+ for (const element of promiseAllInfo.arrayExpr.elements) {
3074
+ if (!element || element.type === 'SpreadElement')
3075
+ continue;
3076
+ const bubble = this.findBubbleInExpression(element, bubbleMap);
3077
+ if (bubble) {
3078
+ children.push({ type: 'bubble', variableId: bubble.variableId });
3079
+ continue;
3080
+ }
3081
+ const functionCall = this.detectFunctionCall(element);
3082
+ if (functionCall) {
3083
+ const syntheticStmt = {
3084
+ type: AST_NODE_TYPES.ExpressionStatement,
3085
+ expression: functionCall.callExpr,
3086
+ range: element.range,
3087
+ loc: element.loc,
3088
+ parent: stmt,
3089
+ };
3090
+ const funcCallNode = this.buildFunctionCallNode(functionCall, syntheticStmt, bubbleMap, scopeManager);
3091
+ if (funcCallNode)
3092
+ children.push(funcCallNode);
3093
+ }
3094
+ }
3095
+ }
3096
+ // Extract variable declaration info if this is part of a variable declaration
3097
+ let variableDeclaration;
3098
+ if (stmt.type === 'VariableDeclaration' &&
3099
+ stmt.declarations.length > 0 &&
3100
+ stmt.declarations[0].id.type === 'ArrayPattern') {
3101
+ const arrayPattern = stmt.declarations[0].id;
3102
+ const variableNames = [];
3103
+ for (const element of arrayPattern.elements) {
3104
+ if (element && element.type === 'Identifier') {
3105
+ variableNames.push(element.name);
3106
+ }
3107
+ }
3108
+ variableDeclaration = {
3109
+ variableNames,
3110
+ variableType: stmt.kind,
3111
+ };
3112
+ }
3113
+ return {
3114
+ type: 'parallel_execution',
3115
+ location,
3116
+ code,
3117
+ variableDeclaration,
3118
+ children,
3119
+ };
3120
+ }
3121
+ /**
3122
+ * Get the invocation index for a method call.
3123
+ * If the same call expression (identified by its AST range) has been processed before,
3124
+ * return the same index to avoid double-counting.
3125
+ *
3126
+ * @param methodName - The name of the method being called
3127
+ * @param callExprStartOffset - Optional start offset of the CallExpression in the source.
3128
+ * Used to deduplicate when the same call is processed multiple times
3129
+ * (e.g., .map() callback processing vs Promise.all resolution)
3130
+ */
3131
+ getNextInvocationIndex(methodName, callExprStartOffset) {
3132
+ // Check if this specific call site has already been indexed
3133
+ if (callExprStartOffset !== undefined) {
3134
+ const callSiteId = `${methodName}:${callExprStartOffset}`;
3135
+ const existingIndex = this.processedCallSiteIndexes.get(callSiteId);
3136
+ if (existingIndex !== undefined) {
3137
+ return existingIndex;
3138
+ }
3139
+ // New call site - assign next index and cache it
3140
+ const next = (this.methodInvocationOrdinalMap.get(methodName) ?? 0) + 1;
3141
+ this.methodInvocationOrdinalMap.set(methodName, next);
3142
+ this.processedCallSiteIndexes.set(callSiteId, next);
3143
+ return next;
3144
+ }
3145
+ // Fallback: no offset provided, just increment (legacy behavior)
3146
+ const next = (this.methodInvocationOrdinalMap.get(methodName) ?? 0) + 1;
3147
+ this.methodInvocationOrdinalMap.set(methodName, next);
3148
+ return next;
3149
+ }
3150
+ cloneWorkflowNodesForInvocation(nodes, callSiteKey, bubbleSourceMap, localCloneMap) {
3151
+ return nodes.map((node) => this.cloneWorkflowNodeForInvocation(node, callSiteKey, bubbleSourceMap, localCloneMap));
3152
+ }
3153
+ cloneWorkflowNodeForInvocation(node, callSiteKey, bubbleSourceMap, localCloneMap) {
3154
+ if (node.type === 'bubble') {
3155
+ const originalId = Number(node.variableId);
3156
+ const clonedId = this.ensureClonedBubbleForInvocation(originalId, callSiteKey, bubbleSourceMap, localCloneMap);
3157
+ return { ...node, variableId: clonedId };
3158
+ }
3159
+ const clonedNode = { ...node };
3160
+ if ('children' in node && Array.isArray(node.children)) {
3161
+ clonedNode.children = node.children.map((child) => this.cloneWorkflowNodeForInvocation(child, callSiteKey, bubbleSourceMap, localCloneMap));
3162
+ }
3163
+ if ('elseBranch' in node && Array.isArray(node.elseBranch)) {
3164
+ clonedNode.elseBranch = node.elseBranch.map((child) => this.cloneWorkflowNodeForInvocation(child, callSiteKey, bubbleSourceMap, localCloneMap));
3165
+ }
3166
+ if ('catchBlock' in node && Array.isArray(node.catchBlock)) {
3167
+ clonedNode.catchBlock = node.catchBlock.map((child) => this.cloneWorkflowNodeForInvocation(child, callSiteKey, bubbleSourceMap, localCloneMap));
3168
+ }
3169
+ return clonedNode;
3170
+ }
3171
+ ensureClonedBubbleForInvocation(originalId, callSiteKey, bubbleSourceMap, localCloneMap) {
3172
+ const existing = localCloneMap.get(originalId);
3173
+ if (existing) {
3174
+ return existing;
3175
+ }
3176
+ const sourceBubble = bubbleSourceMap.get(originalId);
3177
+ if (!sourceBubble) {
3178
+ return originalId;
3179
+ }
3180
+ const clonedId = this.cloneBubbleForInvocation(sourceBubble, callSiteKey, bubbleSourceMap);
3181
+ localCloneMap.set(originalId, clonedId);
3182
+ return clonedId;
3183
+ }
3184
+ cloneBubbleForInvocation(bubble, callSiteKey, bubbleSourceMap) {
3185
+ const cacheKey = `${bubble.variableId}:${callSiteKey}`;
3186
+ const existing = this.invocationBubbleCloneCache.get(cacheKey);
3187
+ if (existing) {
3188
+ return existing.variableId;
3189
+ }
3190
+ const clonedBubble = {
3191
+ ...bubble,
3192
+ variableId: hashToVariableId(cacheKey),
3193
+ invocationCallSiteKey: callSiteKey,
3194
+ clonedFromVariableId: bubble.variableId,
3195
+ parameters: JSON.parse(JSON.stringify(bubble.parameters)),
3196
+ dependencyGraph: bubble.dependencyGraph
3197
+ ? this.cloneDependencyGraphNodeForInvocation(bubble.dependencyGraph, callSiteKey)
3198
+ : undefined,
3199
+ };
3200
+ /**
3201
+ * Also clone any bubbles that are referenced inside this bubble's
3202
+ * dependencyGraph.functionCallChildren (e.g. bubbles instantiated
3203
+ * inside AI agent customTools).
3204
+ *
3205
+ * This ensures that nested bubbles inside custom tools participate
3206
+ * in per-invocation cloning just like top-level workflow bubbles:
3207
+ * - They get their own cloned ParsedBubbleWithInfo entry
3208
+ * - clonedFromVariableId points back to the original id
3209
+ * - invocationCallSiteKey is set
3210
+ * - Their dependencyGraph is cloned with per-invocation uniqueId/variableId
3211
+ *
3212
+ * We then rewrite the functionCallChildren children variableIds in the
3213
+ * cloned dependencyGraph to point at the cloned bubble ids so that
3214
+ * __bubbleInvocationDependencyGraphs[callSiteKey][originalId] contains
3215
+ * a fully self-consistent graph for this invocation.
3216
+ */
3217
+ if (bubble.dependencyGraph &&
3218
+ bubble.dependencyGraph.functionCallChildren &&
3219
+ Array.isArray(bubble.dependencyGraph.functionCallChildren)) {
3220
+ const clonedDepGraph = clonedBubble.dependencyGraph;
3221
+ if (clonedDepGraph &&
3222
+ Array.isArray(clonedDepGraph.functionCallChildren)) {
3223
+ clonedDepGraph.functionCallChildren =
3224
+ clonedDepGraph.functionCallChildren.map((funcCallNode) => {
3225
+ if (!Array.isArray(funcCallNode.children)) {
3226
+ return funcCallNode;
3227
+ }
3228
+ const clonedChildren = funcCallNode.children.map((child) => {
3229
+ if (!child ||
3230
+ child.type !== 'bubble' ||
3231
+ typeof child.variableId !== 'number') {
3232
+ return child;
3233
+ }
3234
+ const originalChildId = child.variableId;
3235
+ const sourceChild = bubbleSourceMap.get(originalChildId);
3236
+ if (!sourceChild) {
3237
+ return child;
3238
+ }
3239
+ const clonedChildId = this.cloneBubbleForInvocation(sourceChild, callSiteKey, bubbleSourceMap);
3240
+ return {
3241
+ ...child,
3242
+ variableId: clonedChildId,
3243
+ };
3244
+ });
3245
+ return {
3246
+ ...funcCallNode,
3247
+ children: clonedChildren,
3248
+ };
3249
+ });
3250
+ }
3251
+ }
3252
+ this.invocationBubbleCloneCache.set(cacheKey, clonedBubble);
3253
+ return clonedBubble.variableId;
3254
+ }
3255
+ cloneDependencyGraphNodeForInvocation(node, callSiteKey) {
3256
+ const uniqueId = node.uniqueId
3257
+ ? `${node.uniqueId}@${callSiteKey}`
3258
+ : undefined;
3259
+ const variableId = typeof node.variableId === 'number'
3260
+ ? hashToVariableId(`${node.variableId}:${callSiteKey}`)
3261
+ : undefined;
3262
+ return {
3263
+ ...node,
3264
+ uniqueId,
3265
+ variableId,
3266
+ dependencies: node.dependencies.map((child) => this.cloneDependencyGraphNodeForInvocation(child, callSiteKey)),
3267
+ };
3268
+ }
3269
+ /**
3270
+ * Extract condition string from a test expression
3271
+ */
3272
+ extractConditionString(test) {
3273
+ return this.bubbleScript.substring(test.range[0], test.range[1]);
3274
+ }
3275
+ /**
3276
+ * Extract location from a node
3277
+ */
3278
+ extractLocation(node) {
3279
+ if (!node.loc) {
3280
+ return { startLine: 0, startCol: 0, endLine: 0, endCol: 0 };
3281
+ }
3282
+ return {
3283
+ startLine: node.loc.start.line,
3284
+ startCol: node.loc.start.column,
3285
+ endLine: node.loc.end.line,
3286
+ endCol: node.loc.end.column,
3287
+ };
3288
+ }
1123
3289
  }
1124
3290
  //# sourceMappingURL=BubbleParser.js.map