@pikku/inspector 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/add/add-forge-credential.d.ts +8 -0
  3. package/dist/add/add-forge-credential.js +77 -0
  4. package/dist/add/add-forge-node.d.ts +7 -0
  5. package/dist/add/add-forge-node.js +77 -0
  6. package/dist/add/add-functions.js +102 -9
  7. package/dist/add/add-http-route.js +24 -1
  8. package/dist/add/add-rpc-invocations.d.ts +3 -0
  9. package/dist/add/add-rpc-invocations.js +51 -25
  10. package/dist/add/add-workflow-graph.d.ts +6 -0
  11. package/dist/add/add-workflow-graph.js +659 -0
  12. package/dist/add/add-workflow.js +118 -22
  13. package/dist/error-codes.d.ts +3 -1
  14. package/dist/error-codes.js +3 -1
  15. package/dist/index.d.ts +3 -0
  16. package/dist/index.js +2 -0
  17. package/dist/inspector.js +19 -3
  18. package/dist/types.d.ts +26 -0
  19. package/dist/utils/extract-function-name.js +7 -7
  20. package/dist/utils/get-property-value.d.ts +2 -1
  21. package/dist/utils/get-property-value.js +6 -2
  22. package/dist/utils/serialize-inspector-state.d.ts +24 -1
  23. package/dist/utils/serialize-inspector-state.js +24 -0
  24. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
  25. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +898 -0
  26. package/dist/{workflow/extract-simple-workflow.d.ts → utils/workflow/dsl/extract-dsl-workflow.d.ts} +4 -2
  27. package/dist/{workflow/extract-simple-workflow.js → utils/workflow/dsl/extract-dsl-workflow.js} +549 -68
  28. package/dist/utils/workflow/dsl/index.d.ts +7 -0
  29. package/dist/utils/workflow/dsl/index.js +7 -0
  30. package/dist/{workflow → utils/workflow/dsl}/patterns.d.ts +21 -0
  31. package/dist/{workflow → utils/workflow/dsl}/patterns.js +90 -10
  32. package/dist/{workflow → utils/workflow/dsl}/validation.d.ts +2 -0
  33. package/dist/{workflow → utils/workflow/dsl}/validation.js +25 -7
  34. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
  35. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +316 -0
  36. package/dist/utils/workflow/graph/index.d.ts +6 -0
  37. package/dist/utils/workflow/graph/index.js +6 -0
  38. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +43 -0
  39. package/dist/utils/workflow/graph/serialize-workflow-graph.js +152 -0
  40. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +229 -0
  41. package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
  42. package/dist/visit.js +6 -0
  43. package/package.json +14 -2
  44. package/src/add/add-forge-credential.ts +119 -0
  45. package/src/add/add-forge-node.ts +132 -0
  46. package/src/add/add-functions.ts +129 -15
  47. package/src/add/add-http-route.ts +25 -1
  48. package/src/add/add-rpc-invocations.ts +61 -31
  49. package/src/add/add-workflow-graph.ts +864 -0
  50. package/src/add/add-workflow.ts +112 -26
  51. package/src/error-codes.ts +3 -1
  52. package/src/index.ts +10 -0
  53. package/src/inspector.ts +20 -4
  54. package/src/types.ts +25 -1
  55. package/src/utils/extract-function-name.ts +7 -7
  56. package/src/utils/get-property-value.ts +9 -2
  57. package/src/utils/serialize-inspector-state.ts +39 -1
  58. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1180 -0
  59. package/src/{workflow/extract-simple-workflow.ts → utils/workflow/dsl/extract-dsl-workflow.ts} +654 -81
  60. package/src/utils/workflow/dsl/index.ts +11 -0
  61. package/src/{workflow → utils/workflow/dsl}/patterns.ts +108 -11
  62. package/src/{workflow → utils/workflow/dsl}/validation.ts +34 -7
  63. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +415 -0
  64. package/src/utils/workflow/graph/index.ts +6 -0
  65. package/src/utils/workflow/graph/serialize-workflow-graph.ts +223 -0
  66. package/src/utils/workflow/graph/workflow-graph.types.ts +280 -0
  67. package/src/visit.ts +6 -0
  68. package/tsconfig.tsbuildinfo +1 -1
@@ -1,11 +1,26 @@
1
1
  import * as ts from 'typescript';
2
- import { extractStringLiteral, extractNumberLiteral, } from '../utils/extract-node-value.js';
3
- import { isWorkflowDoCall, isWorkflowSleepCall, isParallelFanout, isParallelGroup, isSequentialFanout, extractForOfVariable, isArrayType, getSourceText, } from './patterns.js';
2
+ import { isWorkflowDoCall, isWorkflowSleepCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, } from './patterns.js';
4
3
  import { validateNoDisallowedPatterns, validateAwaitedCalls, formatValidationErrors, } from './validation.js';
4
+ import { extractStringLiteral, extractNumberLiteral, } from '../../extract-node-value.js';
5
+ /**
6
+ * Extract full source path from an expression (e.g., data.memberEmails)
7
+ */
8
+ function extractSourcePath(expr) {
9
+ if (ts.isIdentifier(expr)) {
10
+ return expr.text;
11
+ }
12
+ if (ts.isPropertyAccessExpression(expr)) {
13
+ const base = extractSourcePath(expr.expression);
14
+ if (base) {
15
+ return `${base}.${expr.name.text}`;
16
+ }
17
+ }
18
+ return null;
19
+ }
5
20
  /**
6
21
  * Extract simple workflow metadata from a function declaration
7
22
  */
8
- export function extractSimpleWorkflow(funcNode, checker) {
23
+ export function extractDSLWorkflow(funcNode, checker) {
9
24
  try {
10
25
  // Find the async arrow function
11
26
  const arrowFunc = findWorkflowFunction(funcNode);
@@ -33,6 +48,9 @@ export function extractSimpleWorkflow(funcNode, checker) {
33
48
  conditionalVars: new Set(),
34
49
  inputParamName,
35
50
  errors: [],
51
+ loopVars: new Set(),
52
+ contextVars: new Map(),
53
+ depth: 0,
36
54
  };
37
55
  // Validate no disallowed patterns
38
56
  const patternErrors = validateNoDisallowedPatterns(arrowFunc.body);
@@ -62,9 +80,18 @@ export function extractSimpleWorkflow(funcNode, checker) {
62
80
  simple: false,
63
81
  };
64
82
  }
83
+ // Build workflow context from extracted context variables
84
+ const workflowContext = {};
85
+ for (const [name, info] of context.contextVars) {
86
+ workflowContext[name] = {
87
+ type: info.type,
88
+ default: info.default,
89
+ };
90
+ }
65
91
  return {
66
92
  status: 'ok',
67
93
  steps,
94
+ context: Object.keys(workflowContext).length > 0 ? workflowContext : undefined,
68
95
  simple: true,
69
96
  };
70
97
  }
@@ -80,7 +107,7 @@ export function extractSimpleWorkflow(funcNode, checker) {
80
107
  * Find the workflow function (async arrow function)
81
108
  */
82
109
  function findWorkflowFunction(node) {
83
- // Handle pikkuSimpleWorkflowFunc(async () => {}) or pikkuWorkflowFunc(async () => {})
110
+ // Handle pikkuWorkflowFunc(async () => {}) or pikkuWorkflowComplexFunc(async () => {})
84
111
  if (ts.isCallExpression(node)) {
85
112
  const arg = node.arguments[0];
86
113
  if (arg && ts.isArrowFunction(arg)) {
@@ -99,7 +126,7 @@ function findWorkflowFunction(node) {
99
126
  }
100
127
  }
101
128
  }
102
- // Handle pikkuSimpleWorkflowFunc({ func: async () => {} })
129
+ // Handle pikkuWorkflowFunc({ func: async () => {} })
103
130
  if (ts.isObjectLiteralExpression(node)) {
104
131
  for (const prop of node.properties) {
105
132
  if (ts.isPropertyAssignment(prop) &&
@@ -129,17 +156,25 @@ function extractInputParamName(arrowFunc) {
129
156
  /**
130
157
  * Extract steps from the function body
131
158
  */
132
- function extractSteps(body, context) {
159
+ function extractSteps(body, context, incrementDepth = false) {
133
160
  const steps = [];
134
161
  if (!ts.isBlock(body)) {
135
162
  return steps;
136
163
  }
164
+ // Increment depth when entering a nested block
165
+ if (incrementDepth) {
166
+ context.depth++;
167
+ }
137
168
  for (const statement of body.statements) {
138
169
  const extracted = extractStep(statement, context);
139
170
  if (extracted) {
140
171
  steps.push(extracted);
141
172
  }
142
173
  }
174
+ // Restore depth
175
+ if (incrementDepth) {
176
+ context.depth--;
177
+ }
143
178
  return steps;
144
179
  }
145
180
  /**
@@ -158,6 +193,10 @@ function extractStep(statement, context) {
158
193
  if (ts.isIfStatement(statement)) {
159
194
  return extractBranch(statement, context);
160
195
  }
196
+ // Switch statement
197
+ if (ts.isSwitchStatement(statement)) {
198
+ return extractSwitch(statement, context);
199
+ }
161
200
  // For-of statement (sequential fanout)
162
201
  if (ts.isForOfStatement(statement)) {
163
202
  return extractSequentialFanout(statement, context);
@@ -166,6 +205,10 @@ function extractStep(statement, context) {
166
205
  if (ts.isReturnStatement(statement)) {
167
206
  return extractReturn(statement, context);
168
207
  }
208
+ // Throw statement (for WorkflowCancelledException)
209
+ if (ts.isThrowStatement(statement)) {
210
+ return extractThrowCancel(statement, context);
211
+ }
169
212
  return null;
170
213
  }
171
214
  /**
@@ -182,9 +225,25 @@ function extractVariableDeclaration(statement, context) {
182
225
  }
183
226
  const varName = decl.name.text;
184
227
  const init = decl.initializer;
228
+ // Check for block-scoped variable declarations (not allowed)
229
+ if (context.depth > 0) {
230
+ context.errors.push({
231
+ message: `Variable declaration '${varName}' inside block is not supported in DSL workflows. Move all let/const declarations to the top level.`,
232
+ node: statement,
233
+ });
234
+ return null;
235
+ }
185
236
  if (!init) {
186
237
  return null;
187
238
  }
239
+ // Check for simple literal/expression context variable (let x = 'value')
240
+ const literalValue = extractLiteralValue(init);
241
+ if (literalValue !== undefined) {
242
+ const tsType = context.checker.getTypeAtLocation(decl);
243
+ const typeStr = inferSimpleType(tsType, context.checker);
244
+ context.contextVars.set(varName, { type: typeStr, default: literalValue });
245
+ return null; // No step emitted, just register the context var
246
+ }
188
247
  // Check for await workflow.do(...)
189
248
  if (ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)) {
190
249
  const call = init.expression;
@@ -215,6 +274,28 @@ function extractVariableDeclaration(statement, context) {
215
274
  }
216
275
  }
217
276
  }
277
+ // Check for array.filter(...)
278
+ if (ts.isCallExpression(init)) {
279
+ if (isArrayFilter(init)) {
280
+ const filterStep = extractArrayFilter(init, context, varName);
281
+ if (filterStep) {
282
+ const type = context.checker.getTypeAtLocation(decl);
283
+ context.outputVars.set(varName, { type, node: decl });
284
+ if (isArrayType(type, context.checker)) {
285
+ context.arrayVars.add(varName);
286
+ }
287
+ return filterStep;
288
+ }
289
+ }
290
+ if (isArraySome(init) || isArrayEvery(init)) {
291
+ const predicateStep = extractArrayPredicate(init, context, varName);
292
+ if (predicateStep) {
293
+ const type = context.checker.getTypeAtLocation(decl);
294
+ context.outputVars.set(varName, { type, node: decl });
295
+ return predicateStep;
296
+ }
297
+ }
298
+ }
218
299
  return null;
219
300
  }
220
301
  /**
@@ -222,13 +303,30 @@ function extractVariableDeclaration(statement, context) {
222
303
  */
223
304
  function extractExpressionStatement(statement, context) {
224
305
  let expr = statement.expression;
225
- // Handle assignment: owner = await workflow.do(...)
306
+ // Handle assignment: x = value or x = await workflow.do(...)
226
307
  let outputVar;
227
308
  if (ts.isBinaryExpression(expr) &&
228
309
  expr.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
229
310
  // Extract variable name from left side
230
311
  if (ts.isIdentifier(expr.left)) {
231
312
  outputVar = expr.left.text;
313
+ // Check if this is an assignment to a context variable (set step)
314
+ if (context.contextVars.has(outputVar)) {
315
+ const literalValue = extractLiteralValue(expr.right);
316
+ if (literalValue !== undefined) {
317
+ return {
318
+ type: 'set',
319
+ variable: outputVar,
320
+ value: literalValue,
321
+ };
322
+ }
323
+ // Non-literal assignment to context var - use expression as string
324
+ return {
325
+ type: 'set',
326
+ variable: outputVar,
327
+ value: getSourceText(expr.right),
328
+ };
329
+ }
232
330
  }
233
331
  // Use right side as the expression to extract from
234
332
  expr = expr.right;
@@ -371,35 +469,242 @@ function extractSleepStep(call, context) {
371
469
  }
372
470
  }
373
471
  /**
374
- * Extract branch step from if statement
472
+ * Extract cancel step from throw WorkflowCancelledException statement
473
+ */
474
+ function extractThrowCancel(statement, context) {
475
+ if (!isThrowCancelException(statement)) {
476
+ return null;
477
+ }
478
+ const reason = extractCancelReason(statement, context.checker);
479
+ return {
480
+ type: 'cancel',
481
+ reason,
482
+ };
483
+ }
484
+ /**
485
+ * Parse a condition expression into a Condition structure
486
+ */
487
+ function parseCondition(expr) {
488
+ // Handle binary expressions (&&, ||)
489
+ if (ts.isBinaryExpression(expr)) {
490
+ const operator = expr.operatorToken.kind;
491
+ // AND operator (&&)
492
+ if (operator === ts.SyntaxKind.AmpersandAmpersandToken) {
493
+ return {
494
+ type: 'and',
495
+ conditions: [parseCondition(expr.left), parseCondition(expr.right)],
496
+ };
497
+ }
498
+ // OR operator (||)
499
+ if (operator === ts.SyntaxKind.BarBarToken) {
500
+ return {
501
+ type: 'or',
502
+ conditions: [parseCondition(expr.left), parseCondition(expr.right)],
503
+ };
504
+ }
505
+ }
506
+ // Handle parenthesized expressions - unwrap and parse inner
507
+ if (ts.isParenthesizedExpression(expr)) {
508
+ return parseCondition(expr.expression);
509
+ }
510
+ // Simple condition (comparison, function call, variable, etc.)
511
+ return {
512
+ type: 'simple',
513
+ expression: getSourceText(expr),
514
+ };
515
+ }
516
+ /**
517
+ * Extract branch step from if statement (supports if/else-if/else chains)
375
518
  */
376
519
  function extractBranch(statement, context) {
377
- const condition = getSourceText(statement.expression);
378
- // Handle both block statements and single statements
379
- const thenSteps = ts.isBlock(statement.thenStatement)
380
- ? extractSteps(statement.thenStatement, context)
381
- : extractStepsFromStatement(statement.thenStatement, context);
382
- const elseSteps = statement.elseStatement
383
- ? ts.isBlock(statement.elseStatement)
384
- ? extractSteps(statement.elseStatement, context)
385
- : extractStepsFromStatement(statement.elseStatement, context)
386
- : undefined;
520
+ const branches = [];
521
+ let elseSteps;
522
+ // Walk the if/else-if chain
523
+ let current = statement;
524
+ while (current) {
525
+ const condition = parseCondition(current.expression);
526
+ const steps = ts.isBlock(current.thenStatement)
527
+ ? extractSteps(current.thenStatement, context, true)
528
+ : extractStepsFromStatement(current.thenStatement, context);
529
+ branches.push({ condition, steps });
530
+ // Check for else-if or else
531
+ if (current.elseStatement) {
532
+ if (ts.isIfStatement(current.elseStatement)) {
533
+ // else-if: continue the chain
534
+ current = current.elseStatement;
535
+ }
536
+ else {
537
+ // else: extract the final else block and stop
538
+ elseSteps = ts.isBlock(current.elseStatement)
539
+ ? extractSteps(current.elseStatement, context, true)
540
+ : extractStepsFromStatement(current.elseStatement, context);
541
+ current = undefined;
542
+ }
543
+ }
544
+ else {
545
+ // No else clause
546
+ current = undefined;
547
+ }
548
+ }
387
549
  return {
388
550
  type: 'branch',
389
- condition,
390
- branches: {
391
- then: thenSteps,
392
- else: elseSteps,
393
- },
551
+ branches,
552
+ elseSteps,
394
553
  };
395
554
  }
396
555
  /**
397
556
  * Extract steps from a single statement (non-block)
398
557
  */
399
558
  function extractStepsFromStatement(statement, context) {
559
+ // Increment depth for single-statement blocks (if without braces)
560
+ context.depth++;
400
561
  const step = extractStep(statement, context);
562
+ context.depth--;
401
563
  return step ? [step] : [];
402
564
  }
565
+ /**
566
+ * Extract switch statement
567
+ */
568
+ function extractSwitch(statement, context) {
569
+ const expression = getSourceText(statement.expression);
570
+ const cases = [];
571
+ let defaultSteps;
572
+ for (const clause of statement.caseBlock.clauses) {
573
+ if (ts.isCaseClause(clause)) {
574
+ const caseValue = extractCaseValue(clause.expression);
575
+ const steps = extractCaseSteps(clause.statements, context);
576
+ cases.push({
577
+ value: caseValue.value,
578
+ expression: caseValue.expression,
579
+ steps,
580
+ });
581
+ }
582
+ else if (ts.isDefaultClause(clause)) {
583
+ defaultSteps = extractCaseSteps(clause.statements, context);
584
+ }
585
+ }
586
+ return {
587
+ type: 'switch',
588
+ expression,
589
+ cases,
590
+ defaultSteps,
591
+ };
592
+ }
593
+ /**
594
+ * Extract case value from expression
595
+ */
596
+ function extractCaseValue(expr) {
597
+ if (ts.isStringLiteral(expr)) {
598
+ return { value: expr.text };
599
+ }
600
+ if (ts.isNumericLiteral(expr)) {
601
+ return { value: Number(expr.text) };
602
+ }
603
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) {
604
+ return { value: true };
605
+ }
606
+ if (expr.kind === ts.SyntaxKind.FalseKeyword) {
607
+ return { value: false };
608
+ }
609
+ if (expr.kind === ts.SyntaxKind.NullKeyword) {
610
+ return { value: null };
611
+ }
612
+ return { expression: getSourceText(expr) };
613
+ }
614
+ /**
615
+ * Extract steps from case statements, stopping at break
616
+ */
617
+ function extractCaseSteps(statements, context) {
618
+ const steps = [];
619
+ // Increment depth for case blocks
620
+ context.depth++;
621
+ for (const statement of statements) {
622
+ if (ts.isBreakStatement(statement)) {
623
+ break;
624
+ }
625
+ const step = extractStep(statement, context);
626
+ if (step) {
627
+ steps.push(step);
628
+ }
629
+ }
630
+ // Restore depth
631
+ context.depth--;
632
+ return steps;
633
+ }
634
+ /**
635
+ * Extract array filter operation
636
+ */
637
+ function extractArrayFilter(call, context, outputVar) {
638
+ if (!ts.isPropertyAccessExpression(call.expression)) {
639
+ return null;
640
+ }
641
+ const sourceExpr = call.expression.expression;
642
+ const sourceVar = extractSourcePath(sourceExpr);
643
+ if (!sourceVar) {
644
+ return null;
645
+ }
646
+ const filterFn = call.arguments[0];
647
+ if (!filterFn || !ts.isArrowFunction(filterFn)) {
648
+ return null;
649
+ }
650
+ const itemParam = filterFn.parameters[0];
651
+ if (!itemParam || !ts.isIdentifier(itemParam.name)) {
652
+ return null;
653
+ }
654
+ const itemVar = itemParam.name.text;
655
+ let condition;
656
+ if (ts.isBlock(filterFn.body)) {
657
+ return null;
658
+ }
659
+ else {
660
+ condition = parseCondition(filterFn.body);
661
+ }
662
+ return {
663
+ type: 'filter',
664
+ sourceVar,
665
+ itemVar,
666
+ condition,
667
+ outputVar,
668
+ };
669
+ }
670
+ /**
671
+ * Extract array predicate operation (some/every)
672
+ */
673
+ function extractArrayPredicate(call, context, outputVar) {
674
+ if (!ts.isPropertyAccessExpression(call.expression)) {
675
+ return null;
676
+ }
677
+ const mode = call.expression.name.text;
678
+ const sourceExpr = call.expression.expression;
679
+ const sourceVar = extractSourcePath(sourceExpr);
680
+ if (!sourceVar) {
681
+ return null;
682
+ }
683
+ const predicateFn = call.arguments[0];
684
+ if (!predicateFn || !ts.isArrowFunction(predicateFn)) {
685
+ return null;
686
+ }
687
+ const itemParam = predicateFn.parameters[0];
688
+ if (!itemParam || !ts.isIdentifier(itemParam.name)) {
689
+ return null;
690
+ }
691
+ const itemVar = itemParam.name.text;
692
+ let condition;
693
+ if (ts.isBlock(predicateFn.body)) {
694
+ return null;
695
+ }
696
+ else {
697
+ condition = parseCondition(predicateFn.body);
698
+ }
699
+ return {
700
+ type: 'arrayPredicate',
701
+ mode,
702
+ sourceVar,
703
+ itemVar,
704
+ condition,
705
+ outputVar,
706
+ };
707
+ }
403
708
  /**
404
709
  * Extract parallel fanout from Promise.all(array.map(...))
405
710
  */
@@ -413,14 +718,7 @@ function extractParallelFanout(call, context) {
413
718
  }
414
719
  // Extract source array
415
720
  const sourceExpr = mapCall.expression.expression;
416
- let sourceVar = null;
417
- if (ts.isIdentifier(sourceExpr)) {
418
- sourceVar = sourceExpr.text;
419
- }
420
- else if (ts.isPropertyAccessExpression(sourceExpr) &&
421
- ts.isIdentifier(sourceExpr.expression)) {
422
- sourceVar = sourceExpr.expression.text;
423
- }
721
+ const sourceVar = extractSourcePath(sourceExpr);
424
722
  if (!sourceVar) {
425
723
  return null;
426
724
  }
@@ -449,7 +747,15 @@ function extractParallelFanout(call, context) {
449
747
  else if (ts.isBlock(mapFn.body)) {
450
748
  // Look for workflow.do in block
451
749
  for (const stmt of mapFn.body.statements) {
452
- if (ts.isReturnStatement(stmt) && stmt.expression) {
750
+ if (ts.isExpressionStatement(stmt)) {
751
+ // Handle: await workflow.do(...)
752
+ if (ts.isAwaitExpression(stmt.expression) &&
753
+ ts.isCallExpression(stmt.expression.expression)) {
754
+ doCall = stmt.expression.expression;
755
+ break;
756
+ }
757
+ }
758
+ else if (ts.isReturnStatement(stmt) && stmt.expression) {
453
759
  if (ts.isCallExpression(stmt.expression)) {
454
760
  doCall = stmt.expression;
455
761
  break;
@@ -467,10 +773,11 @@ function extractParallelFanout(call, context) {
467
773
  if (!doCall || !isWorkflowDoCall(doCall, context.checker)) {
468
774
  return null;
469
775
  }
470
- // Create a temporary context for the child step
776
+ // Create a temporary context for the child step with the loop variable
471
777
  const childContext = {
472
778
  ...context,
473
779
  outputVars: new Map(context.outputVars),
780
+ loopVars: new Set([...context.loopVars, itemVar]),
474
781
  };
475
782
  const childStep = extractRpcStep(doCall, childContext);
476
783
  if (!childStep) {
@@ -528,14 +835,45 @@ function extractSequentialFanout(statement, context) {
528
835
  }
529
836
  let childStep = null;
530
837
  let timeBetween = undefined;
838
+ // Create a child context with the loop variable added
839
+ const childContext = {
840
+ ...context,
841
+ outputVars: new Map(context.outputVars),
842
+ loopVars: new Set([...context.loopVars, itemVar]),
843
+ };
844
+ let workflowDoCount = 0;
531
845
  for (const stmt of statement.statement.statements) {
532
- // Look for workflow.do
846
+ // Look for workflow.do in VariableStatement (const x = await workflow.do(...))
847
+ if (ts.isVariableStatement(stmt)) {
848
+ const declList = stmt.declarationList;
849
+ if (declList.declarations.length === 1) {
850
+ const decl = declList.declarations[0];
851
+ const init = decl.initializer;
852
+ if (init &&
853
+ ts.isAwaitExpression(init) &&
854
+ ts.isCallExpression(init.expression)) {
855
+ const call = init.expression;
856
+ if (isWorkflowDoCall(call, context.checker)) {
857
+ workflowDoCount++;
858
+ const varName = ts.isIdentifier(decl.name)
859
+ ? decl.name.text
860
+ : undefined;
861
+ const step = extractRpcStep(call, childContext, varName);
862
+ if (step) {
863
+ childStep = step;
864
+ }
865
+ }
866
+ }
867
+ }
868
+ }
869
+ // Look for workflow.do in ExpressionStatement (await workflow.do(...))
533
870
  if (ts.isExpressionStatement(stmt)) {
534
871
  const expr = stmt.expression;
535
872
  if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
536
873
  const call = expr.expression;
537
874
  if (isWorkflowDoCall(call, context.checker)) {
538
- const step = extractRpcStep(call, context);
875
+ workflowDoCount++;
876
+ const step = extractRpcStep(call, childContext);
539
877
  if (step) {
540
878
  childStep = step;
541
879
  }
@@ -595,6 +933,14 @@ function extractSequentialFanout(statement, context) {
595
933
  if (!childStep) {
596
934
  return null;
597
935
  }
936
+ // If there are multiple workflow.do calls, the loop is too complex for DSL
937
+ if (workflowDoCount > 1) {
938
+ context.errors.push({
939
+ message: `For-of loop has ${workflowDoCount} workflow.do calls but DSL only supports 1. Use pikkuWorkflowComplexFunc for complex loops.`,
940
+ node: statement,
941
+ });
942
+ return null;
943
+ }
598
944
  return {
599
945
  type: 'fanout',
600
946
  stepName: childStep.stepName,
@@ -605,6 +951,58 @@ function extractSequentialFanout(statement, context) {
605
951
  timeBetween,
606
952
  };
607
953
  }
954
+ /**
955
+ * Extract a single output binding from an expression
956
+ */
957
+ function extractOutputBinding(expr, context) {
958
+ // Check for property access (e.g., org.id, payment.status)
959
+ if (ts.isPropertyAccessExpression(expr)) {
960
+ const objName = ts.isIdentifier(expr.expression)
961
+ ? expr.expression.text
962
+ : null;
963
+ const propPath = expr.name.text;
964
+ if (objName && context.outputVars.has(objName)) {
965
+ return { from: 'outputVar', name: objName, path: propPath };
966
+ }
967
+ if (objName && context.contextVars.has(objName)) {
968
+ return { from: 'stateVar', name: objName, path: propPath };
969
+ }
970
+ if (objName === context.inputParamName) {
971
+ return { from: 'input', path: propPath };
972
+ }
973
+ }
974
+ // Check for identifier (simple variable reference)
975
+ if (ts.isIdentifier(expr)) {
976
+ const varName = expr.text;
977
+ if (context.outputVars.has(varName)) {
978
+ return { from: 'outputVar', name: varName };
979
+ }
980
+ if (context.contextVars.has(varName)) {
981
+ return { from: 'stateVar', name: varName };
982
+ }
983
+ if (varName === context.inputParamName) {
984
+ return { from: 'input', path: varName };
985
+ }
986
+ }
987
+ // Check for literals
988
+ if (ts.isStringLiteral(expr)) {
989
+ return { from: 'literal', value: expr.text };
990
+ }
991
+ if (ts.isNumericLiteral(expr)) {
992
+ return { from: 'literal', value: Number(expr.text) };
993
+ }
994
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) {
995
+ return { from: 'literal', value: true };
996
+ }
997
+ if (expr.kind === ts.SyntaxKind.FalseKeyword) {
998
+ return { from: 'literal', value: false };
999
+ }
1000
+ if (expr.kind === ts.SyntaxKind.NullKeyword) {
1001
+ return { from: 'literal', value: null };
1002
+ }
1003
+ // For any other expression (comparisons, method calls, etc.), capture as expression
1004
+ return { from: 'expression', expression: getSourceText(expr) };
1005
+ }
608
1006
  /**
609
1007
  * Extract return step
610
1008
  */
@@ -625,44 +1023,21 @@ function extractReturn(statement, context) {
625
1023
  }
626
1024
  let binding = null;
627
1025
  if (ts.isShorthandPropertyAssignment(prop)) {
628
- // { orgId } - must be an output variable
1026
+ // { orgId } - must be an output variable, context variable, or input
629
1027
  const varName = prop.name.text;
630
1028
  if (context.outputVars.has(varName)) {
631
1029
  binding = { from: 'outputVar', name: varName };
632
1030
  }
633
- }
634
- else if (ts.isPropertyAssignment(prop)) {
635
- const init = prop.initializer;
636
- // Check for property access (e.g., org.id, owner?.id)
637
- if (ts.isPropertyAccessExpression(init)) {
638
- const objName = ts.isIdentifier(init.expression)
639
- ? init.expression.text
640
- : null;
641
- const propPath = init.name.text;
642
- if (objName && context.outputVars.has(objName)) {
643
- binding = { from: 'outputVar', name: objName, path: propPath };
644
- }
645
- }
646
- // Check for optional chaining (e.g., owner?.id)
647
- if (init.kind === ts.SyntaxKind.PropertyAccessExpression ||
648
- init.kind === ts.SyntaxKind.NonNullExpression) {
649
- const text = init.getText();
650
- const match = text.match(/^(\w+)\??\.(\w+)$/);
651
- if (match) {
652
- const [, objName, propPath] = match;
653
- if (context.outputVars.has(objName)) {
654
- binding = { from: 'outputVar', name: objName, path: propPath };
655
- }
656
- }
1031
+ else if (context.contextVars.has(varName)) {
1032
+ binding = { from: 'stateVar', name: varName };
657
1033
  }
658
- // Check for identifier (simple variable reference)
659
- if (ts.isIdentifier(init)) {
660
- const varName = init.text;
661
- if (context.outputVars.has(varName)) {
662
- binding = { from: 'outputVar', name: varName };
663
- }
1034
+ else {
1035
+ binding = { from: 'input', path: varName };
664
1036
  }
665
1037
  }
1038
+ else if (ts.isPropertyAssignment(prop)) {
1039
+ binding = extractOutputBinding(prop.initializer, context);
1040
+ }
666
1041
  if (binding) {
667
1042
  outputs[propName] = binding;
668
1043
  }
@@ -680,6 +1055,17 @@ function extractReturn(statement, context) {
680
1055
  * Extract input sources from an argument node
681
1056
  */
682
1057
  function extractInputSources(node, context) {
1058
+ // Handle when data is passed directly (e.g., workflow.do('step', 'rpc', data))
1059
+ if (ts.isIdentifier(node)) {
1060
+ if (node.text === context.inputParamName) {
1061
+ // The entire input data is being passed through
1062
+ return 'passthrough';
1063
+ }
1064
+ // Check if it's an output variable being passed directly
1065
+ if (context.outputVars.has(node.text)) {
1066
+ return 'passthrough';
1067
+ }
1068
+ }
683
1069
  if (!ts.isObjectLiteralExpression(node)) {
684
1070
  return undefined;
685
1071
  }
@@ -693,9 +1079,12 @@ function extractInputSources(node, context) {
693
1079
  }
694
1080
  let source = null;
695
1081
  if (ts.isShorthandPropertyAssignment(prop)) {
696
- // { email } - could be from input or output var
1082
+ // { email } - could be from loop var, output var, or input
697
1083
  const varName = prop.name.text;
698
- if (context.outputVars.has(varName)) {
1084
+ if (context.loopVars.has(varName)) {
1085
+ source = { from: 'item', path: varName };
1086
+ }
1087
+ else if (context.outputVars.has(varName)) {
699
1088
  source = { from: 'outputVar', name: varName };
700
1089
  }
701
1090
  else {
@@ -744,6 +1133,10 @@ function extractInputSource(node, context) {
744
1133
  // Identifier: email, orgId
745
1134
  if (ts.isIdentifier(node)) {
746
1135
  const varName = node.text;
1136
+ // Check if it's a loop variable (from fanout)
1137
+ if (context.loopVars.has(varName)) {
1138
+ return { from: 'item', path: varName };
1139
+ }
747
1140
  if (context.outputVars.has(varName)) {
748
1141
  return { from: 'outputVar', name: varName };
749
1142
  }
@@ -799,5 +1192,93 @@ function extractInputSource(node, context) {
799
1192
  }
800
1193
  return { from: 'literal', value: arr };
801
1194
  }
1195
+ // No substitution template literal: `hello`
1196
+ if (ts.isNoSubstitutionTemplateLiteral(node)) {
1197
+ return { from: 'literal', value: node.text };
1198
+ }
1199
+ // Template expression with substitutions: `hello ${name}`
1200
+ if (ts.isTemplateExpression(node)) {
1201
+ const parts = [node.head.text];
1202
+ const expressions = [];
1203
+ for (const span of node.templateSpans) {
1204
+ // Extract each expression
1205
+ const exprSource = extractInputSource(span.expression, context);
1206
+ if (exprSource) {
1207
+ expressions.push(exprSource);
1208
+ }
1209
+ else {
1210
+ // Fallback: use source text as literal
1211
+ expressions.push({
1212
+ from: 'literal',
1213
+ value: getSourceText(span.expression),
1214
+ });
1215
+ }
1216
+ parts.push(span.literal.text);
1217
+ }
1218
+ return { from: 'template', parts, expressions };
1219
+ }
802
1220
  return null;
803
1221
  }
1222
+ /**
1223
+ * Extract a literal value from an expression
1224
+ */
1225
+ function extractLiteralValue(expr) {
1226
+ if (ts.isStringLiteral(expr)) {
1227
+ return expr.text;
1228
+ }
1229
+ if (ts.isNumericLiteral(expr)) {
1230
+ return Number(expr.text);
1231
+ }
1232
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) {
1233
+ return true;
1234
+ }
1235
+ if (expr.kind === ts.SyntaxKind.FalseKeyword) {
1236
+ return false;
1237
+ }
1238
+ if (expr.kind === ts.SyntaxKind.NullKeyword) {
1239
+ return null;
1240
+ }
1241
+ // Array literal
1242
+ if (ts.isArrayLiteralExpression(expr)) {
1243
+ const values = [];
1244
+ for (const el of expr.elements) {
1245
+ const v = extractLiteralValue(el);
1246
+ if (v === undefined)
1247
+ return undefined;
1248
+ values.push(v);
1249
+ }
1250
+ return values;
1251
+ }
1252
+ // Object literal (simple keys with literal values)
1253
+ if (ts.isObjectLiteralExpression(expr)) {
1254
+ const obj = {};
1255
+ for (const prop of expr.properties) {
1256
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
1257
+ const v = extractLiteralValue(prop.initializer);
1258
+ if (v === undefined)
1259
+ return undefined;
1260
+ obj[prop.name.text] = v;
1261
+ }
1262
+ else {
1263
+ return undefined;
1264
+ }
1265
+ }
1266
+ return obj;
1267
+ }
1268
+ return undefined;
1269
+ }
1270
+ /**
1271
+ * Infer a simple type string from a TypeScript type
1272
+ */
1273
+ function inferSimpleType(type, checker) {
1274
+ const typeStr = checker.typeToString(type);
1275
+ if (typeStr === 'string')
1276
+ return 'string';
1277
+ if (typeStr === 'number')
1278
+ return 'number';
1279
+ if (typeStr === 'boolean')
1280
+ return 'boolean';
1281
+ if (typeStr.endsWith('[]') || typeStr.startsWith('Array<'))
1282
+ return 'array';
1283
+ return 'object';
1284
+ }