@pikku/inspector 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/OPTIMIZATION-PLAN.md +195 -0
  3. package/dist/add/add-ai-agent.d.ts +2 -0
  4. package/dist/add/add-ai-agent.js +314 -0
  5. package/dist/add/add-channel.js +69 -61
  6. package/dist/add/add-cli.js +36 -18
  7. package/dist/add/add-file-with-factory.js +2 -0
  8. package/dist/add/add-functions.js +327 -59
  9. package/dist/add/add-http-route.d.ts +19 -10
  10. package/dist/add/add-http-route.js +153 -44
  11. package/dist/add/add-http-routes.d.ts +5 -0
  12. package/dist/add/add-http-routes.js +159 -0
  13. package/dist/add/add-keyed-wiring.d.ts +12 -0
  14. package/dist/add/add-keyed-wiring.js +97 -0
  15. package/dist/add/add-mcp-prompt.js +14 -9
  16. package/dist/add/add-mcp-resource.js +14 -9
  17. package/dist/add/add-middleware.d.ts +1 -4
  18. package/dist/add/add-middleware.js +364 -79
  19. package/dist/add/add-permission.d.ts +1 -1
  20. package/dist/add/add-permission.js +152 -40
  21. package/dist/add/add-queue-worker.js +18 -12
  22. package/dist/add/add-rpc-invocations.d.ts +3 -0
  23. package/dist/add/add-rpc-invocations.js +65 -25
  24. package/dist/add/add-schedule.js +11 -5
  25. package/dist/add/add-secret.d.ts +3 -0
  26. package/dist/add/add-secret.js +82 -0
  27. package/dist/add/add-trigger.d.ts +2 -0
  28. package/dist/add/add-trigger.js +87 -0
  29. package/dist/add/add-variable.d.ts +1 -0
  30. package/dist/add/add-variable.js +8 -0
  31. package/dist/add/add-workflow-graph.d.ts +7 -0
  32. package/dist/add/add-workflow-graph.js +396 -0
  33. package/dist/add/add-workflow.js +124 -26
  34. package/dist/error-codes.d.ts +16 -1
  35. package/dist/error-codes.js +21 -1
  36. package/dist/index.d.ts +9 -5
  37. package/dist/index.js +5 -2
  38. package/dist/inspector.d.ts +1 -1
  39. package/dist/inspector.js +106 -13
  40. package/dist/schema-generator.d.ts +1 -0
  41. package/dist/schema-generator.js +1 -0
  42. package/dist/types-map.js +10 -1
  43. package/dist/types.d.ts +180 -30
  44. package/dist/utils/compute-required-schemas.d.ts +4 -0
  45. package/dist/utils/compute-required-schemas.js +41 -0
  46. package/dist/utils/contract-hashes.d.ts +35 -0
  47. package/dist/utils/contract-hashes.js +202 -0
  48. package/dist/utils/custom-types-generator.d.ts +9 -0
  49. package/dist/utils/custom-types-generator.js +71 -0
  50. package/dist/utils/detect-schema-vendor.d.ts +22 -0
  51. package/dist/utils/detect-schema-vendor.js +76 -0
  52. package/dist/utils/ensure-function-metadata.d.ts +5 -2
  53. package/dist/utils/ensure-function-metadata.js +220 -6
  54. package/dist/utils/extract-function-name.d.ts +5 -16
  55. package/dist/utils/extract-function-name.js +93 -298
  56. package/dist/utils/extract-services.d.ts +2 -1
  57. package/dist/utils/extract-services.js +25 -1
  58. package/dist/utils/filter-inspector-state.js +107 -23
  59. package/dist/utils/get-property-value.d.ts +8 -2
  60. package/dist/utils/get-property-value.js +33 -4
  61. package/dist/utils/hash.d.ts +2 -0
  62. package/dist/utils/hash.js +23 -0
  63. package/dist/utils/middleware.d.ts +7 -30
  64. package/dist/utils/middleware.js +80 -66
  65. package/dist/utils/permissions.d.ts +2 -2
  66. package/dist/utils/permissions.js +10 -10
  67. package/dist/utils/post-process.d.ts +9 -10
  68. package/dist/utils/post-process.js +231 -24
  69. package/dist/utils/resolve-external-package.d.ts +12 -0
  70. package/dist/utils/resolve-external-package.js +34 -0
  71. package/dist/utils/resolve-function-types.d.ts +6 -0
  72. package/dist/utils/resolve-function-types.js +29 -0
  73. package/dist/utils/resolve-identifier.d.ts +10 -0
  74. package/dist/utils/resolve-identifier.js +36 -0
  75. package/dist/utils/resolve-versions.d.ts +2 -0
  76. package/dist/utils/resolve-versions.js +78 -0
  77. package/dist/utils/schema-generator.d.ts +9 -0
  78. package/dist/utils/schema-generator.js +209 -0
  79. package/dist/utils/serialize-inspector-state.d.ts +73 -13
  80. package/dist/utils/serialize-inspector-state.js +102 -6
  81. package/dist/utils/serialize-mcp-json.d.ts +2 -0
  82. package/dist/utils/serialize-mcp-json.js +99 -0
  83. package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
  84. package/dist/utils/serialize-middleware-groups-meta.js +28 -0
  85. package/dist/utils/serialize-openapi-json.d.ts +85 -0
  86. package/dist/utils/serialize-openapi-json.js +151 -0
  87. package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
  88. package/dist/utils/serialize-permissions-groups-meta.js +31 -0
  89. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
  90. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +830 -0
  91. package/dist/{workflow/extract-simple-workflow.d.ts → utils/workflow/dsl/extract-dsl-workflow.d.ts} +4 -2
  92. package/dist/{workflow/extract-simple-workflow.js → utils/workflow/dsl/extract-dsl-workflow.js} +572 -72
  93. package/dist/utils/workflow/dsl/index.d.ts +7 -0
  94. package/dist/utils/workflow/dsl/index.js +7 -0
  95. package/dist/{workflow → utils/workflow/dsl}/patterns.d.ts +21 -0
  96. package/dist/{workflow → utils/workflow/dsl}/patterns.js +90 -10
  97. package/dist/{workflow → utils/workflow/dsl}/validation.d.ts +2 -0
  98. package/dist/{workflow → utils/workflow/dsl}/validation.js +25 -7
  99. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
  100. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +318 -0
  101. package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
  102. package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
  103. package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
  104. package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
  105. package/dist/utils/workflow/graph/index.d.ts +8 -0
  106. package/dist/utils/workflow/graph/index.js +8 -0
  107. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +35 -0
  108. package/dist/utils/workflow/graph/serialize-workflow-graph.js +150 -0
  109. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +203 -0
  110. package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
  111. package/dist/visit.js +13 -2
  112. package/package.json +26 -4
  113. package/src/add/add-ai-agent.ts +468 -0
  114. package/src/add/add-channel.ts +82 -79
  115. package/src/add/add-cli.ts +49 -20
  116. package/src/add/add-file-with-factory.ts +2 -0
  117. package/src/add/add-functions.ts +429 -71
  118. package/src/add/add-http-route.ts +246 -65
  119. package/src/add/add-http-routes.ts +228 -0
  120. package/src/add/add-keyed-wiring.ts +151 -0
  121. package/src/add/add-mcp-prompt.ts +26 -15
  122. package/src/add/add-mcp-resource.ts +27 -15
  123. package/src/add/add-middleware.ts +482 -80
  124. package/src/add/add-permission.ts +199 -40
  125. package/src/add/add-queue-worker.ts +24 -19
  126. package/src/add/add-rpc-invocations.ts +78 -31
  127. package/src/add/add-schedule.ts +16 -11
  128. package/src/add/add-secret.ts +140 -0
  129. package/src/add/add-trigger.ts +154 -0
  130. package/src/add/add-variable.ts +9 -0
  131. package/src/add/add-workflow-graph.ts +522 -0
  132. package/src/add/add-workflow.ts +117 -30
  133. package/src/error-codes.ts +26 -1
  134. package/src/index.ts +27 -8
  135. package/src/inspector.ts +145 -17
  136. package/src/schema-generator.ts +1 -0
  137. package/src/types-map.ts +12 -1
  138. package/src/types.ts +192 -51
  139. package/src/utils/compute-required-schemas.ts +49 -0
  140. package/src/utils/contract-hashes.test.ts +528 -0
  141. package/src/utils/contract-hashes.ts +290 -0
  142. package/src/utils/custom-types-generator.ts +88 -0
  143. package/src/utils/detect-schema-vendor.ts +90 -0
  144. package/src/utils/ensure-function-metadata.ts +324 -7
  145. package/src/utils/extract-function-name.ts +108 -358
  146. package/src/utils/extract-services.ts +35 -2
  147. package/src/utils/filter-inspector-state.test.ts +34 -20
  148. package/src/utils/filter-inspector-state.ts +140 -31
  149. package/src/utils/get-property-value.ts +50 -5
  150. package/src/utils/hash.ts +26 -0
  151. package/src/utils/middleware.test.ts +204 -0
  152. package/src/utils/middleware.ts +129 -67
  153. package/src/utils/permissions.test.ts +35 -12
  154. package/src/utils/permissions.ts +10 -10
  155. package/src/utils/post-process.ts +283 -43
  156. package/src/utils/resolve-external-package.ts +42 -0
  157. package/src/utils/resolve-function-types.ts +42 -0
  158. package/src/utils/resolve-identifier.ts +46 -0
  159. package/src/utils/resolve-versions.test.ts +249 -0
  160. package/src/utils/resolve-versions.ts +105 -0
  161. package/src/utils/schema-generator.ts +329 -0
  162. package/src/utils/serialize-inspector-state.ts +181 -20
  163. package/src/utils/serialize-mcp-json.ts +145 -0
  164. package/src/utils/serialize-middleware-groups-meta.ts +33 -0
  165. package/src/utils/serialize-openapi-json.ts +277 -0
  166. package/src/utils/serialize-permissions-groups-meta.ts +35 -0
  167. package/src/utils/test-data/inspector-state.json +69 -66
  168. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1104 -0
  169. package/src/{workflow/extract-simple-workflow.ts → utils/workflow/dsl/extract-dsl-workflow.ts} +678 -85
  170. package/src/utils/workflow/dsl/index.ts +11 -0
  171. package/src/{workflow → utils/workflow/dsl}/patterns.ts +108 -11
  172. package/src/{workflow → utils/workflow/dsl}/validation.ts +34 -7
  173. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +422 -0
  174. package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
  175. package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
  176. package/src/utils/workflow/graph/index.ts +11 -0
  177. package/src/utils/workflow/graph/serialize-workflow-graph.ts +216 -0
  178. package/src/utils/workflow/graph/workflow-graph.types.ts +231 -0
  179. package/src/visit.ts +14 -2
  180. package/tsconfig.tsbuildinfo +1 -1
  181. package/dist/add/add-mcp-tool.d.ts +0 -2
  182. package/dist/add/add-mcp-tool.js +0 -81
  183. package/dist/utils/extract-service-metadata.d.ts +0 -19
  184. package/dist/utils/extract-service-metadata.js +0 -244
  185. package/dist/utils/write-service-metadata.d.ts +0 -13
  186. package/dist/utils/write-service-metadata.js +0 -37
  187. package/src/add/add-mcp-tool.ts +0 -141
  188. package/src/utils/extract-service-metadata.ts +0 -353
  189. package/src/utils/write-service-metadata.ts +0 -51
@@ -6,19 +6,29 @@ import {
6
6
  ParallelGroupStepMeta,
7
7
  FanoutStepMeta,
8
8
  ReturnStepMeta,
9
+ CancelStepMeta,
10
+ SetStepMeta,
11
+ SwitchStepMeta,
12
+ SwitchCaseMeta,
13
+ FilterStepMeta,
14
+ ArrayPredicateStepMeta,
9
15
  InputSource,
10
16
  OutputBinding,
17
+ Condition,
18
+ WorkflowContext,
19
+ ContextVariable,
11
20
  } from '@pikku/core/workflow'
12
- import {
13
- extractStringLiteral,
14
- extractNumberLiteral,
15
- } from '../utils/extract-node-value.js'
16
21
  import {
17
22
  isWorkflowDoCall,
18
23
  isWorkflowSleepCall,
24
+ isThrowCancelException,
25
+ extractCancelReason,
19
26
  isParallelFanout,
20
27
  isParallelGroup,
21
28
  isSequentialFanout,
29
+ isArrayFilter,
30
+ isArraySome,
31
+ isArrayEvery,
22
32
  extractForOfVariable,
23
33
  isArrayType,
24
34
  getSourceText,
@@ -29,6 +39,10 @@ import {
29
39
  formatValidationErrors,
30
40
  ValidationError,
31
41
  } from './validation.js'
42
+ import {
43
+ extractStringLiteral,
44
+ extractNumberLiteral,
45
+ } from '../../extract-node-value.js'
32
46
 
33
47
  /**
34
48
  * Extraction context to track state during AST traversal
@@ -40,6 +54,30 @@ interface ExtractionContext {
40
54
  conditionalVars: Set<string>
41
55
  inputParamName: string
42
56
  errors: ValidationError[]
57
+ /** Loop variables in scope (for fanout item variables) */
58
+ loopVars: Set<string>
59
+ /** Context variables (top-level let/const with simple values) */
60
+ contextVars: Map<string, { type: string; default: unknown }>
61
+ /** Track nesting depth to detect block-scoped vars */
62
+ depth: number
63
+ }
64
+
65
+ /**
66
+ * Extract full source path from an expression (e.g., data.memberEmails)
67
+ */
68
+ function extractSourcePath(expr: ts.Expression): string | null {
69
+ if (ts.isIdentifier(expr)) {
70
+ return expr.text
71
+ }
72
+
73
+ if (ts.isPropertyAccessExpression(expr)) {
74
+ const base = extractSourcePath(expr.expression)
75
+ if (base) {
76
+ return `${base}.${expr.name.text}`
77
+ }
78
+ }
79
+
80
+ return null
43
81
  }
44
82
 
45
83
  /**
@@ -48,6 +86,8 @@ interface ExtractionContext {
48
86
  export interface ExtractionResult {
49
87
  status: 'ok' | 'error'
50
88
  steps?: WorkflowStepMeta[]
89
+ /** Workflow context (top-level variables) */
90
+ context?: WorkflowContext
51
91
  reason?: string
52
92
  simple?: boolean
53
93
  }
@@ -55,7 +95,7 @@ export interface ExtractionResult {
55
95
  /**
56
96
  * Extract simple workflow metadata from a function declaration
57
97
  */
58
- export function extractSimpleWorkflow(
98
+ export function extractDSLWorkflow(
59
99
  funcNode: ts.Node,
60
100
  checker: ts.TypeChecker
61
101
  ): ExtractionResult {
@@ -88,6 +128,9 @@ export function extractSimpleWorkflow(
88
128
  conditionalVars: new Set(),
89
129
  inputParamName,
90
130
  errors: [],
131
+ loopVars: new Set(),
132
+ contextVars: new Map(),
133
+ depth: 0,
91
134
  }
92
135
 
93
136
  // Validate no disallowed patterns
@@ -122,9 +165,20 @@ export function extractSimpleWorkflow(
122
165
  }
123
166
  }
124
167
 
168
+ // Build workflow context from extracted context variables
169
+ const workflowContext: WorkflowContext = {}
170
+ for (const [name, info] of context.contextVars) {
171
+ workflowContext[name] = {
172
+ type: info.type as ContextVariable['type'],
173
+ default: info.default,
174
+ }
175
+ }
176
+
125
177
  return {
126
178
  status: 'ok',
127
179
  steps,
180
+ context:
181
+ Object.keys(workflowContext).length > 0 ? workflowContext : undefined,
128
182
  simple: true,
129
183
  }
130
184
  } catch (error) {
@@ -140,7 +194,7 @@ export function extractSimpleWorkflow(
140
194
  * Find the workflow function (async arrow function)
141
195
  */
142
196
  function findWorkflowFunction(node: ts.Node): ts.ArrowFunction | null {
143
- // Handle pikkuSimpleWorkflowFunc(async () => {}) or pikkuWorkflowFunc(async () => {})
197
+ // Handle pikkuWorkflowFunc(async () => {}) or pikkuWorkflowComplexFunc(async () => {})
144
198
  if (ts.isCallExpression(node)) {
145
199
  const arg = node.arguments[0]
146
200
  if (arg && ts.isArrowFunction(arg)) {
@@ -162,7 +216,7 @@ function findWorkflowFunction(node: ts.Node): ts.ArrowFunction | null {
162
216
  }
163
217
  }
164
218
 
165
- // Handle pikkuSimpleWorkflowFunc({ func: async () => {} })
219
+ // Handle pikkuWorkflowFunc({ func: async () => {} })
166
220
  if (ts.isObjectLiteralExpression(node)) {
167
221
  for (const prop of node.properties) {
168
222
  if (
@@ -201,7 +255,8 @@ function extractInputParamName(arrowFunc: ts.ArrowFunction): string | null {
201
255
  */
202
256
  function extractSteps(
203
257
  body: ts.Node,
204
- context: ExtractionContext
258
+ context: ExtractionContext,
259
+ incrementDepth = false
205
260
  ): WorkflowStepMeta[] {
206
261
  const steps: WorkflowStepMeta[] = []
207
262
 
@@ -209,6 +264,11 @@ function extractSteps(
209
264
  return steps
210
265
  }
211
266
 
267
+ // Increment depth when entering a nested block
268
+ if (incrementDepth) {
269
+ context.depth++
270
+ }
271
+
212
272
  for (const statement of body.statements) {
213
273
  const extracted = extractStep(statement, context)
214
274
  if (extracted) {
@@ -216,6 +276,11 @@ function extractSteps(
216
276
  }
217
277
  }
218
278
 
279
+ // Restore depth
280
+ if (incrementDepth) {
281
+ context.depth--
282
+ }
283
+
219
284
  return steps
220
285
  }
221
286
 
@@ -241,6 +306,11 @@ function extractStep(
241
306
  return extractBranch(statement, context)
242
307
  }
243
308
 
309
+ // Switch statement
310
+ if (ts.isSwitchStatement(statement)) {
311
+ return extractSwitch(statement, context)
312
+ }
313
+
244
314
  // For-of statement (sequential fanout)
245
315
  if (ts.isForOfStatement(statement)) {
246
316
  return extractSequentialFanout(statement, context)
@@ -251,6 +321,11 @@ function extractStep(
251
321
  return extractReturn(statement, context)
252
322
  }
253
323
 
324
+ // Throw statement (for WorkflowCancelledException)
325
+ if (ts.isThrowStatement(statement)) {
326
+ return extractThrowCancel(statement, context)
327
+ }
328
+
254
329
  return null
255
330
  }
256
331
 
@@ -274,10 +349,28 @@ function extractVariableDeclaration(
274
349
  const varName = decl.name.text
275
350
  const init = decl.initializer
276
351
 
352
+ // Check for block-scoped variable declarations (not allowed)
353
+ if (context.depth > 0) {
354
+ context.errors.push({
355
+ message: `Variable declaration '${varName}' inside block is not supported in DSL workflows. Move all let/const declarations to the top level.`,
356
+ node: statement,
357
+ })
358
+ return null
359
+ }
360
+
277
361
  if (!init) {
278
362
  return null
279
363
  }
280
364
 
365
+ // Check for simple literal/expression context variable (let x = 'value')
366
+ const literalValue = extractLiteralValue(init)
367
+ if (literalValue !== undefined) {
368
+ const tsType = context.checker.getTypeAtLocation(decl)
369
+ const typeStr = inferSimpleType(tsType, context.checker)
370
+ context.contextVars.set(varName, { type: typeStr, default: literalValue })
371
+ return null // No step emitted, just register the context var
372
+ }
373
+
281
374
  // Check for await workflow.do(...)
282
375
  if (ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)) {
283
376
  const call = init.expression
@@ -315,6 +408,30 @@ function extractVariableDeclaration(
315
408
  }
316
409
  }
317
410
 
411
+ // Check for array.filter(...)
412
+ if (ts.isCallExpression(init)) {
413
+ if (isArrayFilter(init)) {
414
+ const filterStep = extractArrayFilter(init, context, varName)
415
+ if (filterStep) {
416
+ const type = context.checker.getTypeAtLocation(decl)
417
+ context.outputVars.set(varName, { type, node: decl })
418
+ if (isArrayType(type, context.checker)) {
419
+ context.arrayVars.add(varName)
420
+ }
421
+ return filterStep
422
+ }
423
+ }
424
+
425
+ if (isArraySome(init) || isArrayEvery(init)) {
426
+ const predicateStep = extractArrayPredicate(init, context, varName)
427
+ if (predicateStep) {
428
+ const type = context.checker.getTypeAtLocation(decl)
429
+ context.outputVars.set(varName, { type, node: decl })
430
+ return predicateStep
431
+ }
432
+ }
433
+ }
434
+
318
435
  return null
319
436
  }
320
437
 
@@ -327,7 +444,7 @@ function extractExpressionStatement(
327
444
  ): WorkflowStepMeta | null {
328
445
  let expr = statement.expression
329
446
 
330
- // Handle assignment: owner = await workflow.do(...)
447
+ // Handle assignment: x = value or x = await workflow.do(...)
331
448
  let outputVar: string | undefined
332
449
  if (
333
450
  ts.isBinaryExpression(expr) &&
@@ -336,6 +453,24 @@ function extractExpressionStatement(
336
453
  // Extract variable name from left side
337
454
  if (ts.isIdentifier(expr.left)) {
338
455
  outputVar = expr.left.text
456
+
457
+ // Check if this is an assignment to a context variable (set step)
458
+ if (context.contextVars.has(outputVar)) {
459
+ const literalValue = extractLiteralValue(expr.right)
460
+ if (literalValue !== undefined) {
461
+ return {
462
+ type: 'set',
463
+ variable: outputVar,
464
+ value: literalValue,
465
+ } as SetStepMeta
466
+ }
467
+ // Non-literal assignment to context var - use expression as string
468
+ return {
469
+ type: 'set',
470
+ variable: outputVar,
471
+ value: getSourceText(expr.right),
472
+ } as SetStepMeta
473
+ }
339
474
  }
340
475
  // Use right side as the expression to extract from
341
476
  expr = expr.right
@@ -510,32 +645,102 @@ function extractSleepStep(
510
645
  }
511
646
 
512
647
  /**
513
- * Extract branch step from if statement
648
+ * Extract cancel step from throw WorkflowCancelledException statement
649
+ */
650
+ function extractThrowCancel(
651
+ statement: ts.ThrowStatement,
652
+ context: ExtractionContext
653
+ ): CancelStepMeta | null {
654
+ if (!isThrowCancelException(statement)) {
655
+ return null
656
+ }
657
+
658
+ const reason = extractCancelReason(statement, context.checker)
659
+ return {
660
+ type: 'cancel',
661
+ reason,
662
+ }
663
+ }
664
+
665
+ /**
666
+ * Parse a condition expression into a Condition structure
667
+ */
668
+ function parseCondition(expr: ts.Expression): Condition {
669
+ // Handle binary expressions (&&, ||)
670
+ if (ts.isBinaryExpression(expr)) {
671
+ const operator = expr.operatorToken.kind
672
+
673
+ // AND operator (&&)
674
+ if (operator === ts.SyntaxKind.AmpersandAmpersandToken) {
675
+ return {
676
+ type: 'and',
677
+ conditions: [parseCondition(expr.left), parseCondition(expr.right)],
678
+ }
679
+ }
680
+
681
+ // OR operator (||)
682
+ if (operator === ts.SyntaxKind.BarBarToken) {
683
+ return {
684
+ type: 'or',
685
+ conditions: [parseCondition(expr.left), parseCondition(expr.right)],
686
+ }
687
+ }
688
+ }
689
+
690
+ // Handle parenthesized expressions - unwrap and parse inner
691
+ if (ts.isParenthesizedExpression(expr)) {
692
+ return parseCondition(expr.expression)
693
+ }
694
+
695
+ // Simple condition (comparison, function call, variable, etc.)
696
+ return {
697
+ type: 'simple',
698
+ expression: getSourceText(expr),
699
+ }
700
+ }
701
+
702
+ /**
703
+ * Extract branch step from if statement (supports if/else-if/else chains)
514
704
  */
515
705
  function extractBranch(
516
706
  statement: ts.IfStatement,
517
707
  context: ExtractionContext
518
708
  ): BranchStepMeta | null {
519
- const condition = getSourceText(statement.expression)
520
-
521
- // Handle both block statements and single statements
522
- const thenSteps = ts.isBlock(statement.thenStatement)
523
- ? extractSteps(statement.thenStatement, context)
524
- : extractStepsFromStatement(statement.thenStatement, context)
525
-
526
- const elseSteps = statement.elseStatement
527
- ? ts.isBlock(statement.elseStatement)
528
- ? extractSteps(statement.elseStatement, context)
529
- : extractStepsFromStatement(statement.elseStatement, context)
530
- : undefined
709
+ const branches: BranchStepMeta['branches'] = []
710
+ let elseSteps: BranchStepMeta['elseSteps']
711
+
712
+ // Walk the if/else-if chain
713
+ let current: ts.IfStatement | undefined = statement
714
+ while (current) {
715
+ const condition = parseCondition(current.expression)
716
+ const steps = ts.isBlock(current.thenStatement)
717
+ ? extractSteps(current.thenStatement, context, true)
718
+ : extractStepsFromStatement(current.thenStatement, context)
719
+
720
+ branches.push({ condition, steps })
721
+
722
+ // Check for else-if or else
723
+ if (current.elseStatement) {
724
+ if (ts.isIfStatement(current.elseStatement)) {
725
+ // else-if: continue the chain
726
+ current = current.elseStatement
727
+ } else {
728
+ // else: extract the final else block and stop
729
+ elseSteps = ts.isBlock(current.elseStatement)
730
+ ? extractSteps(current.elseStatement, context, true)
731
+ : extractStepsFromStatement(current.elseStatement, context)
732
+ current = undefined
733
+ }
734
+ } else {
735
+ // No else clause
736
+ current = undefined
737
+ }
738
+ }
531
739
 
532
740
  return {
533
741
  type: 'branch',
534
- condition,
535
- branches: {
536
- then: thenSteps,
537
- else: elseSteps,
538
- },
742
+ branches,
743
+ elseSteps,
539
744
  }
540
745
  }
541
746
 
@@ -546,10 +751,198 @@ function extractStepsFromStatement(
546
751
  statement: ts.Statement,
547
752
  context: ExtractionContext
548
753
  ): WorkflowStepMeta[] {
754
+ // Increment depth for single-statement blocks (if without braces)
755
+ context.depth++
549
756
  const step = extractStep(statement, context)
757
+ context.depth--
550
758
  return step ? [step] : []
551
759
  }
552
760
 
761
+ /**
762
+ * Extract switch statement
763
+ */
764
+ function extractSwitch(
765
+ statement: ts.SwitchStatement,
766
+ context: ExtractionContext
767
+ ): SwitchStepMeta | null {
768
+ const expression = getSourceText(statement.expression)
769
+ const cases: SwitchCaseMeta[] = []
770
+ let defaultSteps: WorkflowStepMeta[] | undefined
771
+
772
+ for (const clause of statement.caseBlock.clauses) {
773
+ if (ts.isCaseClause(clause)) {
774
+ const caseValue = extractCaseValue(clause.expression)
775
+ const steps = extractCaseSteps(clause.statements, context)
776
+
777
+ cases.push({
778
+ value: caseValue.value,
779
+ expression: caseValue.expression,
780
+ steps,
781
+ })
782
+ } else if (ts.isDefaultClause(clause)) {
783
+ defaultSteps = extractCaseSteps(clause.statements, context)
784
+ }
785
+ }
786
+
787
+ return {
788
+ type: 'switch',
789
+ expression,
790
+ cases,
791
+ defaultSteps,
792
+ }
793
+ }
794
+
795
+ /**
796
+ * Extract case value from expression
797
+ */
798
+ function extractCaseValue(expr: ts.Expression): {
799
+ value?: string | number | boolean | null
800
+ expression?: string
801
+ } {
802
+ if (ts.isStringLiteral(expr)) {
803
+ return { value: expr.text }
804
+ }
805
+ if (ts.isNumericLiteral(expr)) {
806
+ return { value: Number(expr.text) }
807
+ }
808
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) {
809
+ return { value: true }
810
+ }
811
+ if (expr.kind === ts.SyntaxKind.FalseKeyword) {
812
+ return { value: false }
813
+ }
814
+ if (expr.kind === ts.SyntaxKind.NullKeyword) {
815
+ return { value: null }
816
+ }
817
+
818
+ return { expression: getSourceText(expr) }
819
+ }
820
+
821
+ /**
822
+ * Extract steps from case statements, stopping at break
823
+ */
824
+ function extractCaseSteps(
825
+ statements: ts.NodeArray<ts.Statement>,
826
+ context: ExtractionContext
827
+ ): WorkflowStepMeta[] {
828
+ const steps: WorkflowStepMeta[] = []
829
+
830
+ // Increment depth for case blocks
831
+ context.depth++
832
+
833
+ for (const statement of statements) {
834
+ if (ts.isBreakStatement(statement)) {
835
+ break
836
+ }
837
+
838
+ const step = extractStep(statement, context)
839
+ if (step) {
840
+ steps.push(step)
841
+ }
842
+ }
843
+
844
+ // Restore depth
845
+ context.depth--
846
+
847
+ return steps
848
+ }
849
+
850
+ /**
851
+ * Extract array filter operation
852
+ */
853
+ function extractArrayFilter(
854
+ call: ts.CallExpression,
855
+ context: ExtractionContext,
856
+ outputVar?: string
857
+ ): FilterStepMeta | null {
858
+ if (!ts.isPropertyAccessExpression(call.expression)) {
859
+ return null
860
+ }
861
+
862
+ const sourceExpr = call.expression.expression
863
+ const sourceVar = extractSourcePath(sourceExpr)
864
+
865
+ if (!sourceVar) {
866
+ return null
867
+ }
868
+
869
+ const filterFn = call.arguments[0]
870
+ if (!filterFn || !ts.isArrowFunction(filterFn)) {
871
+ return null
872
+ }
873
+
874
+ const itemParam = filterFn.parameters[0]
875
+ if (!itemParam || !ts.isIdentifier(itemParam.name)) {
876
+ return null
877
+ }
878
+
879
+ const itemVar = itemParam.name.text
880
+
881
+ let condition: Condition
882
+ if (ts.isBlock(filterFn.body)) {
883
+ return null
884
+ } else {
885
+ condition = parseCondition(filterFn.body)
886
+ }
887
+
888
+ return {
889
+ type: 'filter',
890
+ sourceVar,
891
+ itemVar,
892
+ condition,
893
+ outputVar,
894
+ }
895
+ }
896
+
897
+ /**
898
+ * Extract array predicate operation (some/every)
899
+ */
900
+ function extractArrayPredicate(
901
+ call: ts.CallExpression,
902
+ context: ExtractionContext,
903
+ outputVar?: string
904
+ ): ArrayPredicateStepMeta | null {
905
+ if (!ts.isPropertyAccessExpression(call.expression)) {
906
+ return null
907
+ }
908
+
909
+ const mode = call.expression.name.text as 'some' | 'every'
910
+ const sourceExpr = call.expression.expression
911
+ const sourceVar = extractSourcePath(sourceExpr)
912
+
913
+ if (!sourceVar) {
914
+ return null
915
+ }
916
+
917
+ const predicateFn = call.arguments[0]
918
+ if (!predicateFn || !ts.isArrowFunction(predicateFn)) {
919
+ return null
920
+ }
921
+
922
+ const itemParam = predicateFn.parameters[0]
923
+ if (!itemParam || !ts.isIdentifier(itemParam.name)) {
924
+ return null
925
+ }
926
+
927
+ const itemVar = itemParam.name.text
928
+
929
+ let condition: Condition
930
+ if (ts.isBlock(predicateFn.body)) {
931
+ return null
932
+ } else {
933
+ condition = parseCondition(predicateFn.body)
934
+ }
935
+
936
+ return {
937
+ type: 'arrayPredicate',
938
+ mode,
939
+ sourceVar,
940
+ itemVar,
941
+ condition,
942
+ outputVar,
943
+ }
944
+ }
945
+
553
946
  /**
554
947
  * Extract parallel fanout from Promise.all(array.map(...))
555
948
  */
@@ -568,16 +961,7 @@ function extractParallelFanout(
568
961
 
569
962
  // Extract source array
570
963
  const sourceExpr = mapCall.expression.expression
571
- let sourceVar: string | null = null
572
-
573
- if (ts.isIdentifier(sourceExpr)) {
574
- sourceVar = sourceExpr.text
575
- } else if (
576
- ts.isPropertyAccessExpression(sourceExpr) &&
577
- ts.isIdentifier(sourceExpr.expression)
578
- ) {
579
- sourceVar = sourceExpr.expression.text
580
- }
964
+ const sourceVar = extractSourcePath(sourceExpr)
581
965
 
582
966
  if (!sourceVar) {
583
967
  return null
@@ -610,7 +994,16 @@ function extractParallelFanout(
610
994
  } else if (ts.isBlock(mapFn.body)) {
611
995
  // Look for workflow.do in block
612
996
  for (const stmt of mapFn.body.statements) {
613
- if (ts.isReturnStatement(stmt) && stmt.expression) {
997
+ if (ts.isExpressionStatement(stmt)) {
998
+ // Handle: await workflow.do(...)
999
+ if (
1000
+ ts.isAwaitExpression(stmt.expression) &&
1001
+ ts.isCallExpression(stmt.expression.expression)
1002
+ ) {
1003
+ doCall = stmt.expression.expression
1004
+ break
1005
+ }
1006
+ } else if (ts.isReturnStatement(stmt) && stmt.expression) {
614
1007
  if (ts.isCallExpression(stmt.expression)) {
615
1008
  doCall = stmt.expression
616
1009
  break
@@ -629,10 +1022,11 @@ function extractParallelFanout(
629
1022
  return null
630
1023
  }
631
1024
 
632
- // Create a temporary context for the child step
1025
+ // Create a temporary context for the child step with the loop variable
633
1026
  const childContext: ExtractionContext = {
634
1027
  ...context,
635
1028
  outputVars: new Map(context.outputVars),
1029
+ loopVars: new Set([...context.loopVars, itemVar]),
636
1030
  }
637
1031
 
638
1032
  const childStep = extractRpcStep(doCall, childContext)
@@ -709,8 +1103,43 @@ function extractSequentialFanout(
709
1103
  let childStep: RpcStepMeta | null = null
710
1104
  let timeBetween: string | undefined = undefined
711
1105
 
1106
+ // Create a child context with the loop variable added
1107
+ const childContext: ExtractionContext = {
1108
+ ...context,
1109
+ outputVars: new Map(context.outputVars),
1110
+ loopVars: new Set([...context.loopVars, itemVar]),
1111
+ }
1112
+
1113
+ let workflowDoCount = 0
1114
+
712
1115
  for (const stmt of statement.statement.statements) {
713
- // Look for workflow.do
1116
+ // Look for workflow.do in VariableStatement (const x = await workflow.do(...))
1117
+ if (ts.isVariableStatement(stmt)) {
1118
+ const declList = stmt.declarationList
1119
+ if (declList.declarations.length === 1) {
1120
+ const decl = declList.declarations[0]
1121
+ const init = decl.initializer
1122
+ if (
1123
+ init &&
1124
+ ts.isAwaitExpression(init) &&
1125
+ ts.isCallExpression(init.expression)
1126
+ ) {
1127
+ const call = init.expression
1128
+ if (isWorkflowDoCall(call, context.checker)) {
1129
+ workflowDoCount++
1130
+ const varName = ts.isIdentifier(decl.name)
1131
+ ? decl.name.text
1132
+ : undefined
1133
+ const step = extractRpcStep(call, childContext, varName)
1134
+ if (step) {
1135
+ childStep = step
1136
+ }
1137
+ }
1138
+ }
1139
+ }
1140
+ }
1141
+
1142
+ // Look for workflow.do in ExpressionStatement (await workflow.do(...))
714
1143
  if (ts.isExpressionStatement(stmt)) {
715
1144
  const expr = stmt.expression
716
1145
 
@@ -718,7 +1147,8 @@ function extractSequentialFanout(
718
1147
  const call = expr.expression
719
1148
 
720
1149
  if (isWorkflowDoCall(call, context.checker)) {
721
- const step = extractRpcStep(call, context)
1150
+ workflowDoCount++
1151
+ const step = extractRpcStep(call, childContext)
722
1152
  if (step) {
723
1153
  childStep = step
724
1154
  }
@@ -785,6 +1215,15 @@ function extractSequentialFanout(
785
1215
  return null
786
1216
  }
787
1217
 
1218
+ // If there are multiple workflow.do calls, the loop is too complex for DSL
1219
+ if (workflowDoCount > 1) {
1220
+ context.errors.push({
1221
+ message: `For-of loop has ${workflowDoCount} workflow.do calls but DSL only supports 1. Use pikkuWorkflowComplexFunc for complex loops.`,
1222
+ node: statement,
1223
+ })
1224
+ return null
1225
+ }
1226
+
788
1227
  return {
789
1228
  type: 'fanout',
790
1229
  stepName: childStep.stepName,
@@ -796,6 +1235,66 @@ function extractSequentialFanout(
796
1235
  }
797
1236
  }
798
1237
 
1238
+ /**
1239
+ * Extract a single output binding from an expression
1240
+ */
1241
+ function extractOutputBinding(
1242
+ expr: ts.Expression,
1243
+ context: ExtractionContext
1244
+ ): OutputBinding | null {
1245
+ // Check for property access (e.g., org.id, payment.status)
1246
+ if (ts.isPropertyAccessExpression(expr)) {
1247
+ const objName = ts.isIdentifier(expr.expression)
1248
+ ? expr.expression.text
1249
+ : null
1250
+ const propPath = expr.name.text
1251
+
1252
+ if (objName && context.outputVars.has(objName)) {
1253
+ return { from: 'outputVar', name: objName, path: propPath }
1254
+ }
1255
+ if (objName && context.contextVars.has(objName)) {
1256
+ return { from: 'stateVar', name: objName, path: propPath }
1257
+ }
1258
+ if (objName === context.inputParamName) {
1259
+ return { from: 'input', path: propPath }
1260
+ }
1261
+ }
1262
+
1263
+ // Check for identifier (simple variable reference)
1264
+ if (ts.isIdentifier(expr)) {
1265
+ const varName = expr.text
1266
+ if (context.outputVars.has(varName)) {
1267
+ return { from: 'outputVar', name: varName }
1268
+ }
1269
+ if (context.contextVars.has(varName)) {
1270
+ return { from: 'stateVar', name: varName }
1271
+ }
1272
+ if (varName === context.inputParamName) {
1273
+ return { from: 'input', path: varName }
1274
+ }
1275
+ }
1276
+
1277
+ // Check for literals
1278
+ if (ts.isStringLiteral(expr)) {
1279
+ return { from: 'literal', value: expr.text }
1280
+ }
1281
+ if (ts.isNumericLiteral(expr)) {
1282
+ return { from: 'literal', value: Number(expr.text) }
1283
+ }
1284
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) {
1285
+ return { from: 'literal', value: true }
1286
+ }
1287
+ if (expr.kind === ts.SyntaxKind.FalseKeyword) {
1288
+ return { from: 'literal', value: false }
1289
+ }
1290
+ if (expr.kind === ts.SyntaxKind.NullKeyword) {
1291
+ return { from: 'literal', value: null }
1292
+ }
1293
+
1294
+ // For any other expression (comparisons, method calls, etc.), capture as expression
1295
+ return { from: 'expression', expression: getSourceText(expr) }
1296
+ }
1297
+
799
1298
  /**
800
1299
  * Extract return step
801
1300
  */
@@ -826,48 +1325,17 @@ function extractReturn(
826
1325
  let binding: OutputBinding | null = null
827
1326
 
828
1327
  if (ts.isShorthandPropertyAssignment(prop)) {
829
- // { orgId } - must be an output variable
1328
+ // { orgId } - must be an output variable, context variable, or input
830
1329
  const varName = prop.name.text
831
1330
  if (context.outputVars.has(varName)) {
832
1331
  binding = { from: 'outputVar', name: varName }
1332
+ } else if (context.contextVars.has(varName)) {
1333
+ binding = { from: 'stateVar', name: varName }
1334
+ } else {
1335
+ binding = { from: 'input', path: varName }
833
1336
  }
834
1337
  } else if (ts.isPropertyAssignment(prop)) {
835
- const init = prop.initializer
836
-
837
- // Check for property access (e.g., org.id, owner?.id)
838
- if (ts.isPropertyAccessExpression(init)) {
839
- const objName = ts.isIdentifier(init.expression)
840
- ? init.expression.text
841
- : null
842
- const propPath = init.name.text
843
-
844
- if (objName && context.outputVars.has(objName)) {
845
- binding = { from: 'outputVar', name: objName, path: propPath }
846
- }
847
- }
848
-
849
- // Check for optional chaining (e.g., owner?.id)
850
- if (
851
- init.kind === ts.SyntaxKind.PropertyAccessExpression ||
852
- init.kind === ts.SyntaxKind.NonNullExpression
853
- ) {
854
- const text = init.getText()
855
- const match = text.match(/^(\w+)\??\.(\w+)$/)
856
- if (match) {
857
- const [, objName, propPath] = match
858
- if (context.outputVars.has(objName)) {
859
- binding = { from: 'outputVar', name: objName, path: propPath }
860
- }
861
- }
862
- }
863
-
864
- // Check for identifier (simple variable reference)
865
- if (ts.isIdentifier(init)) {
866
- const varName = init.text
867
- if (context.outputVars.has(varName)) {
868
- binding = { from: 'outputVar', name: varName }
869
- }
870
- }
1338
+ binding = extractOutputBinding(prop.initializer, context)
871
1339
  }
872
1340
 
873
1341
  if (binding) {
@@ -892,7 +1360,19 @@ function extractReturn(
892
1360
  function extractInputSources(
893
1361
  node: ts.Node,
894
1362
  context: ExtractionContext
895
- ): Record<string, InputSource> | undefined {
1363
+ ): Record<string, InputSource> | 'passthrough' | undefined {
1364
+ // Handle when data is passed directly (e.g., workflow.do('step', 'rpc', data))
1365
+ if (ts.isIdentifier(node)) {
1366
+ if (node.text === context.inputParamName) {
1367
+ // The entire input data is being passed through
1368
+ return 'passthrough'
1369
+ }
1370
+ // Check if it's an output variable being passed directly
1371
+ if (context.outputVars.has(node.text)) {
1372
+ return 'passthrough'
1373
+ }
1374
+ }
1375
+
896
1376
  if (!ts.isObjectLiteralExpression(node)) {
897
1377
  return undefined
898
1378
  }
@@ -912,9 +1392,11 @@ function extractInputSources(
912
1392
  let source: InputSource | null = null
913
1393
 
914
1394
  if (ts.isShorthandPropertyAssignment(prop)) {
915
- // { email } - could be from input or output var
1395
+ // { email } - could be from loop var, output var, or input
916
1396
  const varName = prop.name.text
917
- if (context.outputVars.has(varName)) {
1397
+ if (context.loopVars.has(varName)) {
1398
+ source = { from: 'item', path: varName }
1399
+ } else if (context.outputVars.has(varName)) {
918
1400
  source = { from: 'outputVar', name: varName }
919
1401
  } else {
920
1402
  source = { from: 'input', path: varName }
@@ -944,6 +1426,26 @@ function extractInputSources(
944
1426
  return Object.keys(inputs).length > 0 ? inputs : undefined
945
1427
  }
946
1428
 
1429
+ function inputSourceToInlineValue(source: InputSource): unknown {
1430
+ switch (source.from) {
1431
+ case 'literal':
1432
+ return source.value
1433
+ case 'input':
1434
+ return { $ref: 'trigger', path: source.path }
1435
+ case 'outputVar':
1436
+ return { $ref: source.name, path: source.path }
1437
+ case 'item':
1438
+ return { $ref: '$item', path: source.path }
1439
+ case 'template':
1440
+ return {
1441
+ $template: {
1442
+ parts: source.parts,
1443
+ expressions: source.expressions.map(inputSourceToInlineValue),
1444
+ },
1445
+ }
1446
+ }
1447
+ }
1448
+
947
1449
  /**
948
1450
  * Extract a single input source
949
1451
  */
@@ -973,6 +1475,11 @@ function extractInputSource(
973
1475
  if (ts.isIdentifier(node)) {
974
1476
  const varName = node.text
975
1477
 
1478
+ // Check if it's a loop variable (from fanout)
1479
+ if (context.loopVars.has(varName)) {
1480
+ return { from: 'item', path: varName }
1481
+ }
1482
+
976
1483
  if (context.outputVars.has(varName)) {
977
1484
  return { from: 'outputVar', name: varName }
978
1485
  }
@@ -1011,8 +1518,8 @@ function extractInputSource(
1011
1518
  if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
1012
1519
  const propName = prop.name.text
1013
1520
  const propSource = extractInputSource(prop.initializer, context)
1014
- if (propSource && propSource.from === 'literal') {
1015
- obj[propName] = propSource.value
1521
+ if (propSource) {
1522
+ obj[propName] = inputSourceToInlineValue(propSource)
1016
1523
  }
1017
1524
  }
1018
1525
  }
@@ -1024,12 +1531,98 @@ function extractInputSource(
1024
1531
  const arr: unknown[] = []
1025
1532
  for (const elem of node.elements) {
1026
1533
  const elemSource = extractInputSource(elem, context)
1027
- if (elemSource && elemSource.from === 'literal') {
1028
- arr.push(elemSource.value)
1534
+ if (elemSource) {
1535
+ arr.push(inputSourceToInlineValue(elemSource))
1029
1536
  }
1030
1537
  }
1031
1538
  return { from: 'literal', value: arr }
1032
1539
  }
1033
1540
 
1541
+ // No substitution template literal: `hello`
1542
+ if (ts.isNoSubstitutionTemplateLiteral(node)) {
1543
+ return { from: 'literal', value: node.text }
1544
+ }
1545
+
1546
+ // Template expression with substitutions: `hello ${name}`
1547
+ if (ts.isTemplateExpression(node)) {
1548
+ const parts: string[] = [node.head.text]
1549
+ const expressions: InputSource[] = []
1550
+
1551
+ for (const span of node.templateSpans) {
1552
+ // Extract each expression
1553
+ const exprSource = extractInputSource(span.expression, context)
1554
+ if (exprSource) {
1555
+ expressions.push(exprSource)
1556
+ } else {
1557
+ // Fallback: use source text as literal
1558
+ expressions.push({
1559
+ from: 'literal',
1560
+ value: getSourceText(span.expression),
1561
+ })
1562
+ }
1563
+ parts.push(span.literal.text)
1564
+ }
1565
+
1566
+ return { from: 'template', parts, expressions }
1567
+ }
1568
+
1034
1569
  return null
1035
1570
  }
1571
+
1572
+ /**
1573
+ * Extract a literal value from an expression
1574
+ */
1575
+ function extractLiteralValue(expr: ts.Expression): unknown | undefined {
1576
+ if (ts.isStringLiteral(expr)) {
1577
+ return expr.text
1578
+ }
1579
+ if (ts.isNumericLiteral(expr)) {
1580
+ return Number(expr.text)
1581
+ }
1582
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) {
1583
+ return true
1584
+ }
1585
+ if (expr.kind === ts.SyntaxKind.FalseKeyword) {
1586
+ return false
1587
+ }
1588
+ if (expr.kind === ts.SyntaxKind.NullKeyword) {
1589
+ return null
1590
+ }
1591
+ // Array literal
1592
+ if (ts.isArrayLiteralExpression(expr)) {
1593
+ const values: unknown[] = []
1594
+ for (const el of expr.elements) {
1595
+ const v = extractLiteralValue(el)
1596
+ if (v === undefined) return undefined
1597
+ values.push(v)
1598
+ }
1599
+ return values
1600
+ }
1601
+ // Object literal (simple keys with literal values)
1602
+ if (ts.isObjectLiteralExpression(expr)) {
1603
+ const obj: Record<string, unknown> = {}
1604
+ for (const prop of expr.properties) {
1605
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
1606
+ const v = extractLiteralValue(prop.initializer)
1607
+ if (v === undefined) return undefined
1608
+ obj[prop.name.text] = v
1609
+ } else {
1610
+ return undefined
1611
+ }
1612
+ }
1613
+ return obj
1614
+ }
1615
+ return undefined
1616
+ }
1617
+
1618
+ /**
1619
+ * Infer a simple type string from a TypeScript type
1620
+ */
1621
+ function inferSimpleType(type: ts.Type, checker: ts.TypeChecker): string {
1622
+ const typeStr = checker.typeToString(type)
1623
+ if (typeStr === 'string') return 'string'
1624
+ if (typeStr === 'number') return 'number'
1625
+ if (typeStr === 'boolean') return 'boolean'
1626
+ if (typeStr.endsWith('[]') || typeStr.startsWith('Array<')) return 'array'
1627
+ return 'object'
1628
+ }