@bubblelab/bubble-runtime 0.1.14 → 0.1.16

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