@pikku/inspector 0.11.0 → 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 (109) hide show
  1. package/CHANGELOG.md +32 -2
  2. package/dist/add/add-channel.js +11 -10
  3. package/dist/add/add-file-with-factory.js +10 -10
  4. package/dist/add/add-forge-credential.d.ts +8 -0
  5. package/dist/add/add-forge-credential.js +77 -0
  6. package/dist/add/add-forge-node.d.ts +7 -0
  7. package/dist/add/add-forge-node.js +77 -0
  8. package/dist/add/add-functions.js +158 -51
  9. package/dist/add/add-http-route.js +28 -4
  10. package/dist/add/add-mcp-prompt.js +6 -5
  11. package/dist/add/add-mcp-resource.js +6 -5
  12. package/dist/add/add-mcp-tool.js +6 -5
  13. package/dist/add/add-middleware.js +1 -1
  14. package/dist/add/add-permission.js +1 -1
  15. package/dist/add/add-queue-worker.js +6 -5
  16. package/dist/add/add-rpc-invocations.d.ts +3 -0
  17. package/dist/add/add-rpc-invocations.js +51 -25
  18. package/dist/add/add-schedule.js +5 -4
  19. package/dist/add/add-workflow-graph.d.ts +6 -0
  20. package/dist/add/add-workflow-graph.js +659 -0
  21. package/dist/add/add-workflow.d.ts +1 -1
  22. package/dist/add/add-workflow.js +191 -69
  23. package/dist/error-codes.d.ts +3 -0
  24. package/dist/error-codes.js +3 -0
  25. package/dist/index.d.ts +5 -0
  26. package/dist/index.js +3 -0
  27. package/dist/inspector.js +29 -9
  28. package/dist/types.d.ts +47 -8
  29. package/dist/utils/extract-function-name.js +7 -7
  30. package/dist/utils/extract-function-node.d.ts +10 -0
  31. package/dist/utils/extract-function-node.js +38 -0
  32. package/dist/utils/extract-node-value.d.ts +8 -0
  33. package/dist/utils/extract-node-value.js +24 -0
  34. package/dist/utils/extract-service-metadata.d.ts +19 -0
  35. package/dist/utils/extract-service-metadata.js +244 -0
  36. package/dist/utils/get-files-and-methods.d.ts +3 -3
  37. package/dist/utils/get-files-and-methods.js +3 -3
  38. package/dist/utils/get-property-value.d.ts +14 -6
  39. package/dist/utils/get-property-value.js +55 -43
  40. package/dist/utils/post-process.d.ts +9 -0
  41. package/dist/utils/post-process.js +30 -3
  42. package/dist/utils/serialize-inspector-state.d.ts +42 -6
  43. package/dist/utils/serialize-inspector-state.js +36 -10
  44. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
  45. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +898 -0
  46. package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +17 -0
  47. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +1284 -0
  48. package/dist/utils/workflow/dsl/index.d.ts +7 -0
  49. package/dist/utils/workflow/dsl/index.js +7 -0
  50. package/dist/utils/workflow/dsl/patterns.d.ts +60 -0
  51. package/dist/utils/workflow/dsl/patterns.js +218 -0
  52. package/dist/utils/workflow/dsl/validation.d.ts +30 -0
  53. package/dist/utils/workflow/dsl/validation.js +142 -0
  54. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
  55. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +316 -0
  56. package/dist/utils/workflow/graph/index.d.ts +6 -0
  57. package/dist/utils/workflow/graph/index.js +6 -0
  58. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +43 -0
  59. package/dist/utils/workflow/graph/serialize-workflow-graph.js +152 -0
  60. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +229 -0
  61. package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
  62. package/dist/utils/write-service-metadata.d.ts +13 -0
  63. package/dist/utils/write-service-metadata.js +37 -0
  64. package/dist/visit.js +8 -2
  65. package/package.json +16 -4
  66. package/src/add/add-channel.ts +37 -17
  67. package/src/add/add-file-with-factory.ts +10 -10
  68. package/src/add/add-forge-credential.ts +119 -0
  69. package/src/add/add-forge-node.ts +132 -0
  70. package/src/add/add-functions.ts +199 -69
  71. package/src/add/add-http-route.ts +34 -5
  72. package/src/add/add-mcp-prompt.ts +11 -7
  73. package/src/add/add-mcp-resource.ts +11 -7
  74. package/src/add/add-mcp-tool.ts +11 -7
  75. package/src/add/add-middleware.ts +1 -1
  76. package/src/add/add-permission.ts +1 -1
  77. package/src/add/add-queue-worker.ts +11 -12
  78. package/src/add/add-rpc-invocations.ts +61 -31
  79. package/src/add/add-schedule.ts +10 -5
  80. package/src/add/add-workflow-graph.ts +864 -0
  81. package/src/add/add-workflow.ts +212 -116
  82. package/src/error-codes.ts +3 -0
  83. package/src/index.ts +12 -0
  84. package/src/inspector.ts +36 -10
  85. package/src/types.ts +43 -9
  86. package/src/utils/extract-function-name.ts +7 -7
  87. package/src/utils/extract-function-node.ts +58 -0
  88. package/src/utils/extract-node-value.ts +31 -0
  89. package/src/utils/extract-service-metadata.ts +353 -0
  90. package/src/utils/filter-inspector-state.test.ts +3 -3
  91. package/src/utils/filter-utils.test.ts +45 -51
  92. package/src/utils/get-files-and-methods.ts +11 -11
  93. package/src/utils/get-property-value.ts +67 -53
  94. package/src/utils/permissions.test.ts +3 -3
  95. package/src/utils/post-process.ts +56 -3
  96. package/src/utils/serialize-inspector-state.ts +67 -19
  97. package/src/utils/test-data/inspector-state.json +9 -9
  98. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1180 -0
  99. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +1608 -0
  100. package/src/utils/workflow/dsl/index.ts +11 -0
  101. package/src/utils/workflow/dsl/patterns.ts +279 -0
  102. package/src/utils/workflow/dsl/validation.ts +180 -0
  103. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +415 -0
  104. package/src/utils/workflow/graph/index.ts +6 -0
  105. package/src/utils/workflow/graph/serialize-workflow-graph.ts +223 -0
  106. package/src/utils/workflow/graph/workflow-graph.types.ts +280 -0
  107. package/src/utils/write-service-metadata.ts +51 -0
  108. package/src/visit.ts +9 -3
  109. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,1284 @@
1
+ import * as ts from 'typescript';
2
+ import { isWorkflowDoCall, isWorkflowSleepCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, } from './patterns.js';
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
+ }
20
+ /**
21
+ * Extract simple workflow metadata from a function declaration
22
+ */
23
+ export function extractDSLWorkflow(funcNode, checker) {
24
+ try {
25
+ // Find the async arrow function
26
+ const arrowFunc = findWorkflowFunction(funcNode);
27
+ if (!arrowFunc) {
28
+ return {
29
+ status: 'error',
30
+ reason: 'Could not find async arrow function in workflow definition',
31
+ simple: false,
32
+ };
33
+ }
34
+ // Extract input parameter name (second parameter)
35
+ const inputParamName = extractInputParamName(arrowFunc);
36
+ if (!inputParamName) {
37
+ return {
38
+ status: 'error',
39
+ reason: 'Could not determine input parameter name',
40
+ simple: false,
41
+ };
42
+ }
43
+ // Initialize extraction context
44
+ const context = {
45
+ checker,
46
+ outputVars: new Map(),
47
+ arrayVars: new Set(),
48
+ conditionalVars: new Set(),
49
+ inputParamName,
50
+ errors: [],
51
+ loopVars: new Set(),
52
+ contextVars: new Map(),
53
+ depth: 0,
54
+ };
55
+ // Validate no disallowed patterns
56
+ const patternErrors = validateNoDisallowedPatterns(arrowFunc.body);
57
+ if (patternErrors.length > 0) {
58
+ return {
59
+ status: 'error',
60
+ reason: formatValidationErrors(patternErrors),
61
+ simple: false,
62
+ };
63
+ }
64
+ // Validate all workflow calls are awaited
65
+ const awaitErrors = validateAwaitedCalls(arrowFunc.body);
66
+ if (awaitErrors.length > 0) {
67
+ return {
68
+ status: 'error',
69
+ reason: formatValidationErrors(awaitErrors),
70
+ simple: false,
71
+ };
72
+ }
73
+ // Extract steps from function body
74
+ const steps = extractSteps(arrowFunc.body, context);
75
+ // Check for any accumulated errors
76
+ if (context.errors.length > 0) {
77
+ return {
78
+ status: 'error',
79
+ reason: formatValidationErrors(context.errors),
80
+ simple: false,
81
+ };
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
+ }
91
+ return {
92
+ status: 'ok',
93
+ steps,
94
+ context: Object.keys(workflowContext).length > 0 ? workflowContext : undefined,
95
+ simple: true,
96
+ };
97
+ }
98
+ catch (error) {
99
+ return {
100
+ status: 'error',
101
+ reason: error instanceof Error ? error.message : String(error),
102
+ simple: false,
103
+ };
104
+ }
105
+ }
106
+ /**
107
+ * Find the workflow function (async arrow function)
108
+ */
109
+ function findWorkflowFunction(node) {
110
+ // Handle pikkuWorkflowFunc(async () => {}) or pikkuWorkflowComplexFunc(async () => {})
111
+ if (ts.isCallExpression(node)) {
112
+ const arg = node.arguments[0];
113
+ if (arg && ts.isArrowFunction(arg)) {
114
+ return arg;
115
+ }
116
+ // Also check if first argument is an object with func property
117
+ if (arg && ts.isObjectLiteralExpression(arg)) {
118
+ for (const prop of arg.properties) {
119
+ if (ts.isPropertyAssignment(prop) &&
120
+ ts.isIdentifier(prop.name) &&
121
+ prop.name.text === 'func') {
122
+ if (ts.isArrowFunction(prop.initializer)) {
123
+ return prop.initializer;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ // Handle pikkuWorkflowFunc({ func: async () => {} })
130
+ if (ts.isObjectLiteralExpression(node)) {
131
+ for (const prop of node.properties) {
132
+ if (ts.isPropertyAssignment(prop) &&
133
+ ts.isIdentifier(prop.name) &&
134
+ prop.name.text === 'func') {
135
+ if (ts.isArrowFunction(prop.initializer)) {
136
+ return prop.initializer;
137
+ }
138
+ }
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+ /**
144
+ * Extract the input parameter name from the arrow function
145
+ */
146
+ function extractInputParamName(arrowFunc) {
147
+ if (arrowFunc.parameters.length < 2) {
148
+ return null;
149
+ }
150
+ const secondParam = arrowFunc.parameters[1];
151
+ if (ts.isIdentifier(secondParam.name)) {
152
+ return secondParam.name.text;
153
+ }
154
+ return null;
155
+ }
156
+ /**
157
+ * Extract steps from the function body
158
+ */
159
+ function extractSteps(body, context, incrementDepth = false) {
160
+ const steps = [];
161
+ if (!ts.isBlock(body)) {
162
+ return steps;
163
+ }
164
+ // Increment depth when entering a nested block
165
+ if (incrementDepth) {
166
+ context.depth++;
167
+ }
168
+ for (const statement of body.statements) {
169
+ const extracted = extractStep(statement, context);
170
+ if (extracted) {
171
+ steps.push(extracted);
172
+ }
173
+ }
174
+ // Restore depth
175
+ if (incrementDepth) {
176
+ context.depth--;
177
+ }
178
+ return steps;
179
+ }
180
+ /**
181
+ * Extract a single step from a statement
182
+ */
183
+ function extractStep(statement, context) {
184
+ // Variable declaration with workflow.do assignment
185
+ if (ts.isVariableStatement(statement)) {
186
+ return extractVariableDeclaration(statement, context);
187
+ }
188
+ // Expression statement (await workflow.do without assignment)
189
+ if (ts.isExpressionStatement(statement)) {
190
+ return extractExpressionStatement(statement, context);
191
+ }
192
+ // If statement (branch)
193
+ if (ts.isIfStatement(statement)) {
194
+ return extractBranch(statement, context);
195
+ }
196
+ // Switch statement
197
+ if (ts.isSwitchStatement(statement)) {
198
+ return extractSwitch(statement, context);
199
+ }
200
+ // For-of statement (sequential fanout)
201
+ if (ts.isForOfStatement(statement)) {
202
+ return extractSequentialFanout(statement, context);
203
+ }
204
+ // Return statement
205
+ if (ts.isReturnStatement(statement)) {
206
+ return extractReturn(statement, context);
207
+ }
208
+ // Throw statement (for WorkflowCancelledException)
209
+ if (ts.isThrowStatement(statement)) {
210
+ return extractThrowCancel(statement, context);
211
+ }
212
+ return null;
213
+ }
214
+ /**
215
+ * Extract variable declaration (const x = await workflow.do(...))
216
+ */
217
+ function extractVariableDeclaration(statement, context) {
218
+ const declList = statement.declarationList;
219
+ if (declList.declarations.length !== 1) {
220
+ return null;
221
+ }
222
+ const decl = declList.declarations[0];
223
+ if (!ts.isIdentifier(decl.name)) {
224
+ return null;
225
+ }
226
+ const varName = decl.name.text;
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
+ }
236
+ if (!init) {
237
+ return null;
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
+ }
247
+ // Check for await workflow.do(...)
248
+ if (ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)) {
249
+ const call = init.expression;
250
+ if (isWorkflowDoCall(call, context.checker)) {
251
+ const step = extractRpcStep(call, context, varName);
252
+ if (step) {
253
+ // Track output variable
254
+ const type = context.checker.getTypeAtLocation(decl);
255
+ context.outputVars.set(varName, { type, node: decl });
256
+ // Check if it's an array type
257
+ if (isArrayType(type, context.checker)) {
258
+ context.arrayVars.add(varName);
259
+ }
260
+ // Check if it's a conditional variable (let x: T | undefined)
261
+ if (declList.flags & ts.NodeFlags.Let) {
262
+ const typeNode = decl.type;
263
+ if (typeNode && ts.isUnionTypeNode(typeNode)) {
264
+ // Check if union includes undefined
265
+ const hasUndefined = typeNode.types.some((t) => (ts.isLiteralTypeNode(t) &&
266
+ t.literal.kind === ts.SyntaxKind.UndefinedKeyword) ||
267
+ t.kind === ts.SyntaxKind.UndefinedKeyword);
268
+ if (hasUndefined) {
269
+ context.conditionalVars.add(varName);
270
+ }
271
+ }
272
+ }
273
+ return step;
274
+ }
275
+ }
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
+ }
299
+ return null;
300
+ }
301
+ /**
302
+ * Extract expression statement (await workflow.do(...) without assignment)
303
+ */
304
+ function extractExpressionStatement(statement, context) {
305
+ let expr = statement.expression;
306
+ // Handle assignment: x = value or x = await workflow.do(...)
307
+ let outputVar;
308
+ if (ts.isBinaryExpression(expr) &&
309
+ expr.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
310
+ // Extract variable name from left side
311
+ if (ts.isIdentifier(expr.left)) {
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
+ }
330
+ }
331
+ // Use right side as the expression to extract from
332
+ expr = expr.right;
333
+ }
334
+ // await workflow.do(...)
335
+ if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
336
+ const call = expr.expression;
337
+ if (isWorkflowDoCall(call, context.checker)) {
338
+ const step = extractRpcStep(call, context, outputVar);
339
+ // Track output variable if this is an assignment
340
+ if (outputVar && step) {
341
+ const type = context.checker.getTypeAtLocation(expr);
342
+ context.outputVars.set(outputVar, { type, node: expr });
343
+ // Check if it's an array type
344
+ if (isArrayType(type, context.checker)) {
345
+ context.arrayVars.add(outputVar);
346
+ }
347
+ }
348
+ return step;
349
+ }
350
+ if (isWorkflowSleepCall(call, context.checker)) {
351
+ return extractSleepStep(call, context);
352
+ }
353
+ // Check for parallel group or fanout
354
+ if (isParallelFanout(call)) {
355
+ return extractParallelFanout(call, context);
356
+ }
357
+ if (isParallelGroup(call)) {
358
+ return extractParallelGroup(call, context);
359
+ }
360
+ }
361
+ return null;
362
+ }
363
+ /**
364
+ * Extract RPC step from workflow.do() call
365
+ */
366
+ function extractRpcStep(call, context, outputVar) {
367
+ const args = call.arguments;
368
+ if (args.length < 2) {
369
+ return null;
370
+ }
371
+ try {
372
+ const stepName = extractStringLiteral(args[0], context.checker);
373
+ const rpcName = extractStringLiteral(args[1], context.checker);
374
+ // Extract inputs from third argument
375
+ const inputs = args.length >= 3 ? extractInputSources(args[2], context) : undefined;
376
+ // Extract options from fourth argument
377
+ const options = args.length >= 4 && ts.isObjectLiteralExpression(args[3])
378
+ ? extractStepOptions(args[3], context)
379
+ : undefined;
380
+ return {
381
+ type: 'rpc',
382
+ stepName,
383
+ rpcName,
384
+ outputVar,
385
+ inputs,
386
+ options,
387
+ };
388
+ }
389
+ catch (error) {
390
+ context.errors.push({
391
+ message: `Failed to extract RPC step: ${error instanceof Error ? error.message : String(error)}`,
392
+ node: call,
393
+ });
394
+ return null;
395
+ }
396
+ }
397
+ /**
398
+ * Extract step options from options object
399
+ */
400
+ function extractStepOptions(optionsNode, context) {
401
+ const options = {};
402
+ for (const prop of optionsNode.properties) {
403
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
404
+ const propName = prop.name.text;
405
+ if (propName === 'retries') {
406
+ const retries = extractNumberLiteral(prop.initializer);
407
+ if (retries !== null) {
408
+ options.retries = retries;
409
+ }
410
+ }
411
+ else if (propName === 'retryDelay') {
412
+ try {
413
+ if (ts.isStringLiteral(prop.initializer)) {
414
+ options.retryDelay = prop.initializer.text;
415
+ }
416
+ else {
417
+ const delay = extractNumberLiteral(prop.initializer);
418
+ if (delay !== null) {
419
+ options.retryDelay = delay;
420
+ }
421
+ }
422
+ }
423
+ catch {
424
+ // Ignore extraction errors for retryDelay
425
+ }
426
+ }
427
+ else if (propName === 'description') {
428
+ try {
429
+ options.description = extractStringLiteral(prop.initializer, context.checker);
430
+ }
431
+ catch {
432
+ // Ignore extraction errors for description
433
+ }
434
+ }
435
+ }
436
+ }
437
+ return Object.keys(options).length > 0 ? options : undefined;
438
+ }
439
+ /**
440
+ * Extract sleep step from workflow.sleep() call
441
+ */
442
+ function extractSleepStep(call, context) {
443
+ const args = call.arguments;
444
+ if (args.length < 2) {
445
+ return null;
446
+ }
447
+ try {
448
+ const stepName = extractStringLiteral(args[0], context.checker);
449
+ let duration;
450
+ const numValue = extractNumberLiteral(args[1]);
451
+ if (numValue !== null) {
452
+ duration = numValue;
453
+ }
454
+ else {
455
+ duration = extractStringLiteral(args[1], context.checker);
456
+ }
457
+ return {
458
+ type: 'sleep',
459
+ stepName,
460
+ duration,
461
+ };
462
+ }
463
+ catch (error) {
464
+ context.errors.push({
465
+ message: `Failed to extract sleep step: ${error instanceof Error ? error.message : String(error)}`,
466
+ node: call,
467
+ });
468
+ return null;
469
+ }
470
+ }
471
+ /**
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)
518
+ */
519
+ function extractBranch(statement, context) {
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
+ }
549
+ return {
550
+ type: 'branch',
551
+ branches,
552
+ elseSteps,
553
+ };
554
+ }
555
+ /**
556
+ * Extract steps from a single statement (non-block)
557
+ */
558
+ function extractStepsFromStatement(statement, context) {
559
+ // Increment depth for single-statement blocks (if without braces)
560
+ context.depth++;
561
+ const step = extractStep(statement, context);
562
+ context.depth--;
563
+ return step ? [step] : [];
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
+ }
708
+ /**
709
+ * Extract parallel fanout from Promise.all(array.map(...))
710
+ */
711
+ function extractParallelFanout(call, context) {
712
+ const mapCall = call.arguments[0];
713
+ if (!ts.isCallExpression(mapCall)) {
714
+ return null;
715
+ }
716
+ if (!ts.isPropertyAccessExpression(mapCall.expression)) {
717
+ return null;
718
+ }
719
+ // Extract source array
720
+ const sourceExpr = mapCall.expression.expression;
721
+ const sourceVar = extractSourcePath(sourceExpr);
722
+ if (!sourceVar) {
723
+ return null;
724
+ }
725
+ // Extract map function
726
+ const mapFn = mapCall.arguments[0];
727
+ if (!ts.isArrowFunction(mapFn)) {
728
+ return null;
729
+ }
730
+ // Extract item variable
731
+ const itemParam = mapFn.parameters[0];
732
+ if (!itemParam || !ts.isIdentifier(itemParam.name)) {
733
+ return null;
734
+ }
735
+ const itemVar = itemParam.name.text;
736
+ // Extract workflow.do call from map body
737
+ let doCall = null;
738
+ if (ts.isCallExpression(mapFn.body)) {
739
+ doCall = mapFn.body;
740
+ }
741
+ else if (ts.isAwaitExpression(mapFn.body)) {
742
+ // Handle: async (email) => await workflow.do(...)
743
+ if (ts.isCallExpression(mapFn.body.expression)) {
744
+ doCall = mapFn.body.expression;
745
+ }
746
+ }
747
+ else if (ts.isBlock(mapFn.body)) {
748
+ // Look for workflow.do in block
749
+ for (const stmt of mapFn.body.statements) {
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) {
759
+ if (ts.isCallExpression(stmt.expression)) {
760
+ doCall = stmt.expression;
761
+ break;
762
+ }
763
+ else if (ts.isAwaitExpression(stmt.expression)) {
764
+ // Handle: return await workflow.do(...)
765
+ if (ts.isCallExpression(stmt.expression.expression)) {
766
+ doCall = stmt.expression.expression;
767
+ break;
768
+ }
769
+ }
770
+ }
771
+ }
772
+ }
773
+ if (!doCall || !isWorkflowDoCall(doCall, context.checker)) {
774
+ return null;
775
+ }
776
+ // Create a temporary context for the child step with the loop variable
777
+ const childContext = {
778
+ ...context,
779
+ outputVars: new Map(context.outputVars),
780
+ loopVars: new Set([...context.loopVars, itemVar]),
781
+ };
782
+ const childStep = extractRpcStep(doCall, childContext);
783
+ if (!childStep) {
784
+ return null;
785
+ }
786
+ return {
787
+ type: 'fanout',
788
+ stepName: childStep.stepName,
789
+ sourceVar,
790
+ itemVar,
791
+ mode: 'parallel',
792
+ child: childStep,
793
+ };
794
+ }
795
+ /**
796
+ * Extract parallel group from Promise.all([...])
797
+ */
798
+ function extractParallelGroup(call, context) {
799
+ const arrayArg = call.arguments[0];
800
+ if (!ts.isArrayLiteralExpression(arrayArg)) {
801
+ return null;
802
+ }
803
+ const children = [];
804
+ for (const elem of arrayArg.elements) {
805
+ if (ts.isCallExpression(elem) && isWorkflowDoCall(elem, context.checker)) {
806
+ const step = extractRpcStep(elem, context);
807
+ if (step) {
808
+ children.push(step);
809
+ }
810
+ }
811
+ }
812
+ if (children.length === 0) {
813
+ return null;
814
+ }
815
+ return {
816
+ type: 'parallel',
817
+ children,
818
+ };
819
+ }
820
+ /**
821
+ * Extract sequential fanout from for-of loop
822
+ */
823
+ function extractSequentialFanout(statement, context) {
824
+ if (!isSequentialFanout(statement)) {
825
+ return null;
826
+ }
827
+ const vars = extractForOfVariable(statement);
828
+ if (!vars) {
829
+ return null;
830
+ }
831
+ const { itemVar, sourceVar } = vars;
832
+ // Extract child step and optional sleep from loop body
833
+ if (!ts.isBlock(statement.statement)) {
834
+ return null;
835
+ }
836
+ let childStep = null;
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;
845
+ for (const stmt of statement.statement.statements) {
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(...))
870
+ if (ts.isExpressionStatement(stmt)) {
871
+ const expr = stmt.expression;
872
+ if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
873
+ const call = expr.expression;
874
+ if (isWorkflowDoCall(call, context.checker)) {
875
+ workflowDoCount++;
876
+ const step = extractRpcStep(call, childContext);
877
+ if (step) {
878
+ childStep = step;
879
+ }
880
+ }
881
+ if (isWorkflowSleepCall(call, context.checker)) {
882
+ // Extract duration for timeBetween
883
+ const args = call.arguments;
884
+ if (args.length >= 2) {
885
+ try {
886
+ const numValue = extractNumberLiteral(args[1]);
887
+ if (numValue !== null) {
888
+ timeBetween = `${numValue}ms`;
889
+ }
890
+ else {
891
+ timeBetween = extractStringLiteral(args[1], context.checker);
892
+ }
893
+ }
894
+ catch {
895
+ // Ignore extraction errors
896
+ }
897
+ }
898
+ }
899
+ }
900
+ }
901
+ // Look for if statement with sleep
902
+ if (ts.isIfStatement(stmt)) {
903
+ if (ts.isBlock(stmt.thenStatement)) {
904
+ for (const thenStmt of stmt.thenStatement.statements) {
905
+ if (ts.isExpressionStatement(thenStmt)) {
906
+ const expr = thenStmt.expression;
907
+ if (ts.isAwaitExpression(expr) &&
908
+ ts.isCallExpression(expr.expression)) {
909
+ const call = expr.expression;
910
+ if (isWorkflowSleepCall(call, context.checker)) {
911
+ const args = call.arguments;
912
+ if (args.length >= 2) {
913
+ try {
914
+ const numValue = extractNumberLiteral(args[1]);
915
+ if (numValue !== null) {
916
+ timeBetween = `${numValue}ms`;
917
+ }
918
+ else {
919
+ timeBetween = extractStringLiteral(args[1], context.checker);
920
+ }
921
+ }
922
+ catch {
923
+ // Ignore extraction errors
924
+ }
925
+ }
926
+ }
927
+ }
928
+ }
929
+ }
930
+ }
931
+ }
932
+ }
933
+ if (!childStep) {
934
+ return null;
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
+ }
944
+ return {
945
+ type: 'fanout',
946
+ stepName: childStep.stepName,
947
+ sourceVar,
948
+ itemVar,
949
+ mode: 'sequential',
950
+ child: childStep,
951
+ timeBetween,
952
+ };
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
+ }
1006
+ /**
1007
+ * Extract return step
1008
+ */
1009
+ function extractReturn(statement, context) {
1010
+ if (!statement.expression) {
1011
+ return null;
1012
+ }
1013
+ if (!ts.isObjectLiteralExpression(statement.expression)) {
1014
+ return null;
1015
+ }
1016
+ const outputs = {};
1017
+ for (const prop of statement.expression.properties) {
1018
+ if (ts.isPropertyAssignment(prop) ||
1019
+ ts.isShorthandPropertyAssignment(prop)) {
1020
+ const propName = ts.isIdentifier(prop.name) ? prop.name.text : null;
1021
+ if (!propName) {
1022
+ continue;
1023
+ }
1024
+ let binding = null;
1025
+ if (ts.isShorthandPropertyAssignment(prop)) {
1026
+ // { orgId } - must be an output variable, context variable, or input
1027
+ const varName = prop.name.text;
1028
+ if (context.outputVars.has(varName)) {
1029
+ binding = { from: 'outputVar', name: varName };
1030
+ }
1031
+ else if (context.contextVars.has(varName)) {
1032
+ binding = { from: 'stateVar', name: varName };
1033
+ }
1034
+ else {
1035
+ binding = { from: 'input', path: varName };
1036
+ }
1037
+ }
1038
+ else if (ts.isPropertyAssignment(prop)) {
1039
+ binding = extractOutputBinding(prop.initializer, context);
1040
+ }
1041
+ if (binding) {
1042
+ outputs[propName] = binding;
1043
+ }
1044
+ }
1045
+ }
1046
+ if (Object.keys(outputs).length === 0) {
1047
+ return null;
1048
+ }
1049
+ return {
1050
+ type: 'return',
1051
+ outputs,
1052
+ };
1053
+ }
1054
+ /**
1055
+ * Extract input sources from an argument node
1056
+ */
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
+ }
1069
+ if (!ts.isObjectLiteralExpression(node)) {
1070
+ return undefined;
1071
+ }
1072
+ const inputs = {};
1073
+ for (const prop of node.properties) {
1074
+ if (ts.isPropertyAssignment(prop) ||
1075
+ ts.isShorthandPropertyAssignment(prop)) {
1076
+ const propName = ts.isIdentifier(prop.name) ? prop.name.text : null;
1077
+ if (!propName) {
1078
+ continue;
1079
+ }
1080
+ let source = null;
1081
+ if (ts.isShorthandPropertyAssignment(prop)) {
1082
+ // { email } - could be from loop var, output var, or input
1083
+ const varName = prop.name.text;
1084
+ if (context.loopVars.has(varName)) {
1085
+ source = { from: 'item', path: varName };
1086
+ }
1087
+ else if (context.outputVars.has(varName)) {
1088
+ source = { from: 'outputVar', name: varName };
1089
+ }
1090
+ else {
1091
+ source = { from: 'input', path: varName };
1092
+ }
1093
+ }
1094
+ else if (ts.isPropertyAssignment(prop)) {
1095
+ source = extractInputSource(prop.initializer, context);
1096
+ }
1097
+ if (source) {
1098
+ inputs[propName] = source;
1099
+ }
1100
+ }
1101
+ if (ts.isSpreadAssignment(prop)) {
1102
+ // Handle spread: { ...data }
1103
+ if (ts.isIdentifier(prop.expression)) {
1104
+ const varName = prop.expression.text;
1105
+ if (varName === context.inputParamName) {
1106
+ // This is spreading the input data
1107
+ // We can't fully model this in v1, so we'll skip it
1108
+ continue;
1109
+ }
1110
+ }
1111
+ }
1112
+ }
1113
+ return Object.keys(inputs).length > 0 ? inputs : undefined;
1114
+ }
1115
+ /**
1116
+ * Extract a single input source
1117
+ */
1118
+ function extractInputSource(node, context) {
1119
+ // Property access: data.email, org.id
1120
+ if (ts.isPropertyAccessExpression(node)) {
1121
+ const objExpr = node.expression;
1122
+ const propName = node.name.text;
1123
+ if (ts.isIdentifier(objExpr)) {
1124
+ const objName = objExpr.text;
1125
+ if (objName === context.inputParamName) {
1126
+ return { from: 'input', path: propName };
1127
+ }
1128
+ if (context.outputVars.has(objName)) {
1129
+ return { from: 'outputVar', name: objName, path: propName };
1130
+ }
1131
+ }
1132
+ }
1133
+ // Identifier: email, orgId
1134
+ if (ts.isIdentifier(node)) {
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
+ }
1140
+ if (context.outputVars.has(varName)) {
1141
+ return { from: 'outputVar', name: varName };
1142
+ }
1143
+ // Assume it's from input
1144
+ return { from: 'input', path: varName };
1145
+ }
1146
+ // Literal: "string", 123, true, false, null
1147
+ if (ts.isStringLiteral(node) ||
1148
+ ts.isNumericLiteral(node) ||
1149
+ node.kind === ts.SyntaxKind.TrueKeyword ||
1150
+ node.kind === ts.SyntaxKind.FalseKeyword ||
1151
+ node.kind === ts.SyntaxKind.NullKeyword) {
1152
+ let value;
1153
+ if (ts.isStringLiteral(node)) {
1154
+ value = node.text;
1155
+ }
1156
+ else if (ts.isNumericLiteral(node)) {
1157
+ value = Number(node.text);
1158
+ }
1159
+ else if (node.kind === ts.SyntaxKind.TrueKeyword) {
1160
+ value = true;
1161
+ }
1162
+ else if (node.kind === ts.SyntaxKind.FalseKeyword) {
1163
+ value = false;
1164
+ }
1165
+ else if (node.kind === ts.SyntaxKind.NullKeyword) {
1166
+ value = null;
1167
+ }
1168
+ return { from: 'literal', value };
1169
+ }
1170
+ // Object literal
1171
+ if (ts.isObjectLiteralExpression(node)) {
1172
+ const obj = {};
1173
+ for (const prop of node.properties) {
1174
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
1175
+ const propName = prop.name.text;
1176
+ const propSource = extractInputSource(prop.initializer, context);
1177
+ if (propSource && propSource.from === 'literal') {
1178
+ obj[propName] = propSource.value;
1179
+ }
1180
+ }
1181
+ }
1182
+ return { from: 'literal', value: obj };
1183
+ }
1184
+ // Array literal
1185
+ if (ts.isArrayLiteralExpression(node)) {
1186
+ const arr = [];
1187
+ for (const elem of node.elements) {
1188
+ const elemSource = extractInputSource(elem, context);
1189
+ if (elemSource && elemSource.from === 'literal') {
1190
+ arr.push(elemSource.value);
1191
+ }
1192
+ }
1193
+ return { from: 'literal', value: arr };
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
+ }
1220
+ return null;
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
+ }