@pikku/inspector 0.12.9 → 0.12.10

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## 0.12.0
2
2
 
3
+ ## 0.12.10
4
+
5
+ ### Patch Changes
6
+
7
+ - ba8d6ff: Support inline functions in pikkuWorkflowComplexFunc with full DSL extraction
8
+ - d3ace0e: Inspector now captures the `deploy: 'serverless' | 'server' | 'auto'` option
9
+ from `pikkuFunc` / `pikkuSessionlessFunc` calls, alongside the other runtime
10
+ metadata (`expose`, `remote`, `mcp`, `readonly`, `approvalRequired`).
11
+
12
+ Previously this field was defined on `FunctionRuntimeMeta` but never read
13
+ from the user's source, so `deploy: 'server'` was silently dropped. That
14
+ left downstream consumers — notably `@pikku/cli`'s deployment analyzer,
15
+ which routes server-targeted functions to a container unit — treating
16
+ every function as `serverless` regardless of its declared intent.
17
+
18
+ - Updated dependencies [311c0c4]
19
+ - @pikku/core@0.12.18
20
+
3
21
  ## 0.12.9
4
22
 
5
23
  ### Patch Changes
@@ -245,6 +245,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
245
245
  let remote;
246
246
  let mcp;
247
247
  let readonly_;
248
+ let deploy;
248
249
  let approvalRequired;
249
250
  let approvalDescription;
250
251
  let version;
@@ -312,6 +313,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
312
313
  remote = getPropertyValue(firstArg, 'remote');
313
314
  mcp = getPropertyValue(firstArg, 'mcp');
314
315
  readonly_ = getPropertyValue(firstArg, 'readonly');
316
+ deploy = getPropertyValue(firstArg, 'deploy');
315
317
  approvalRequired = getPropertyValue(firstArg, 'approvalRequired');
316
318
  // Extract approvalDescription identifier reference
317
319
  for (const prop of firstArg.properties) {
@@ -583,6 +585,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
583
585
  remote: remote || undefined,
584
586
  mcp: mcpEnabled || undefined,
585
587
  readonly: readonly_ || undefined,
588
+ deploy: deploy || undefined,
586
589
  approvalRequired: approvalRequired || undefined,
587
590
  approvalDescription: approvalDescription || undefined,
588
591
  version,
@@ -5,7 +5,7 @@ import type { WorkflowStepMeta } from '@pikku/core/workflow';
5
5
  */
6
6
  export declare function collectInvokedRPCs(steps: WorkflowStepMeta[], rpcs: Set<string>): void;
7
7
  /**
8
- * Inspector for pikkuWorkflow() and pikkuSimpleWorkflow() calls
8
+ * Inspector for pikkuWorkflowFunc() and pikkuWorkflowComplexFunc() calls
9
9
  * Detects workflow registration and extracts metadata
10
10
  */
11
11
  export declare const addWorkflow: AddWiring;
@@ -142,7 +142,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
142
142
  });
143
143
  }
144
144
  /**
145
- * Inspector for pikkuWorkflow() and pikkuSimpleWorkflow() calls
145
+ * Inspector for pikkuWorkflowFunc() and pikkuWorkflowComplexFunc() calls
146
146
  * Detects workflow registration and extracts metadata
147
147
  */
148
148
  export const addWorkflow = (logger, node, checker, state) => {
@@ -160,7 +160,7 @@ export const addWorkflow = (logger, node, checker, state) => {
160
160
  wrapperType = 'dsl';
161
161
  }
162
162
  else if (expression.text === 'pikkuWorkflowComplexFunc') {
163
- wrapperType = 'regular';
163
+ wrapperType = 'complex';
164
164
  }
165
165
  else {
166
166
  return;
@@ -220,7 +220,9 @@ export const addWorkflow = (logger, node, checker, state) => {
220
220
  let dsl = undefined;
221
221
  // Try DSL workflow extraction first
222
222
  // Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
223
- const result = extractDSLWorkflow(node, checker);
223
+ const result = extractDSLWorkflow(node, checker, {
224
+ allowInline: wrapperType === 'complex',
225
+ });
224
226
  if (result.status === 'ok' && result.steps) {
225
227
  // Extraction succeeded
226
228
  steps = result.steps;
@@ -252,7 +254,7 @@ export const addWorkflow = (logger, node, checker, state) => {
252
254
  // For pikkuWorkflowFunc, this is a critical error
253
255
  // But still track RPC invocations for function registration
254
256
  getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps);
255
- logger.critical(ErrorCode.INVALID_DSL_WORKFLOW, `Workflow '${workflowName}' uses pikkuWorkflowFunc but does not conform to DSL workflow rules:\n${result.reason || 'Unknown error'}`);
257
+ logger.critical(ErrorCode.INVALID_DSL_WORKFLOW, `Workflow '${workflowName}' does not conform to DSL workflow rules:\n${result.reason || 'Unknown error'}`);
256
258
  return;
257
259
  }
258
260
  else {
@@ -261,12 +263,10 @@ export const addWorkflow = (logger, node, checker, state) => {
261
263
  dsl = false;
262
264
  }
263
265
  }
264
- /**
265
- * For non-dsl workflows or pikkuWorkflowComplexFunc, run basic extraction
266
- * to ensure all RPC invocations are tracked for function registration.
267
- * This catches RPCs in Promise.all callbacks and other patterns DSL can't extract.
268
- */
269
- if (!dsl || wrapperType === 'regular') {
266
+ // For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
267
+ // patterns the DSL extractor doesn't handle (array+push, nested Promise.all
268
+ // with identifier args, etc.) are still registered as invoked functions.
269
+ if (wrapperType === 'complex') {
270
270
  getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps);
271
271
  }
272
272
  state.workflows.meta[workflowName] = {
@@ -20,6 +20,10 @@ export function extractStringLiteral(node, checker) {
20
20
  }
21
21
  return result;
22
22
  }
23
+ // Unwrap type assertions: `expr as Type` or `<Type>expr`
24
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
25
+ return extractStringLiteral(node.expression, checker);
26
+ }
23
27
  // Try to evaluate constant identifiers
24
28
  if (ts.isIdentifier(node)) {
25
29
  const symbol = checker.getSymbolAtLocation(node);
@@ -1,7 +1,7 @@
1
1
  import * as ts from 'typescript';
2
2
  import type { WorkflowStepMeta, WorkflowContext } from '@pikku/core/workflow';
3
3
  /**
4
- * Result of simple workflow extraction
4
+ * Result of DSL workflow extraction
5
5
  */
6
6
  export interface ExtractionResult {
7
7
  status: 'ok' | 'error';
@@ -9,9 +9,10 @@ export interface ExtractionResult {
9
9
  /** Workflow context (top-level variables) */
10
10
  context?: WorkflowContext;
11
11
  reason?: string;
12
- simple?: boolean;
13
12
  }
14
13
  /**
15
- * Extract simple workflow metadata from a function declaration
14
+ * Extract DSL workflow metadata from a function declaration
16
15
  */
17
- export declare function extractDSLWorkflow(funcNode: ts.Node, checker: ts.TypeChecker): ExtractionResult;
16
+ export declare function extractDSLWorkflow(funcNode: ts.Node, checker: ts.TypeChecker, options?: {
17
+ allowInline?: boolean;
18
+ }): ExtractionResult;
@@ -1,26 +1,11 @@
1
1
  import * as ts from 'typescript';
2
- import { isWorkflowDoCall, isWorkflowSleepCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, } from './patterns.js';
2
+ import { isWorkflowDoCall, isWorkflowSleepCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
3
3
  import { validateNoDisallowedPatterns, validateAwaitedCalls, formatValidationErrors, } from './validation.js';
4
4
  import { extractStringLiteral, extractNumberLiteral, } from '../../extract-node-value.js';
5
5
  /**
6
- * Extract full source path from an expression (e.g., data.memberEmails)
6
+ * Extract DSL workflow metadata from a function declaration
7
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) {
8
+ export function extractDSLWorkflow(funcNode, checker, options) {
24
9
  try {
25
10
  // Find the async arrow function
26
11
  const arrowFunc = findWorkflowFunction(funcNode);
@@ -28,7 +13,6 @@ export function extractDSLWorkflow(funcNode, checker) {
28
13
  return {
29
14
  status: 'error',
30
15
  reason: 'Could not find async arrow function in workflow definition',
31
- simple: false,
32
16
  };
33
17
  }
34
18
  // Extract input parameter name (second parameter)
@@ -37,7 +21,6 @@ export function extractDSLWorkflow(funcNode, checker) {
37
21
  return {
38
22
  status: 'error',
39
23
  reason: 'Could not determine input parameter name',
40
- simple: false,
41
24
  };
42
25
  }
43
26
  // Initialize extraction context
@@ -53,12 +36,13 @@ export function extractDSLWorkflow(funcNode, checker) {
53
36
  depth: 0,
54
37
  };
55
38
  // Validate no disallowed patterns
56
- const patternErrors = validateNoDisallowedPatterns(arrowFunc.body);
39
+ const patternErrors = validateNoDisallowedPatterns(arrowFunc.body, {
40
+ allowInline: options?.allowInline,
41
+ });
57
42
  if (patternErrors.length > 0) {
58
43
  return {
59
44
  status: 'error',
60
45
  reason: formatValidationErrors(patternErrors),
61
- simple: false,
62
46
  };
63
47
  }
64
48
  // Validate all workflow calls are awaited
@@ -67,7 +51,6 @@ export function extractDSLWorkflow(funcNode, checker) {
67
51
  return {
68
52
  status: 'error',
69
53
  reason: formatValidationErrors(awaitErrors),
70
- simple: false,
71
54
  };
72
55
  }
73
56
  // Extract steps from function body
@@ -77,7 +60,6 @@ export function extractDSLWorkflow(funcNode, checker) {
77
60
  return {
78
61
  status: 'error',
79
62
  reason: formatValidationErrors(context.errors),
80
- simple: false,
81
63
  };
82
64
  }
83
65
  // Build workflow context from extracted context variables
@@ -92,14 +74,12 @@ export function extractDSLWorkflow(funcNode, checker) {
92
74
  status: 'ok',
93
75
  steps,
94
76
  context: Object.keys(workflowContext).length > 0 ? workflowContext : undefined,
95
- simple: true,
96
77
  };
97
78
  }
98
79
  catch (error) {
99
80
  return {
100
81
  status: 'error',
101
82
  reason: error instanceof Error ? error.message : String(error),
102
- simple: false,
103
83
  };
104
84
  }
105
85
  }
@@ -248,7 +228,9 @@ function extractVariableDeclaration(statement, context) {
248
228
  if (ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)) {
249
229
  const call = init.expression;
250
230
  if (isWorkflowDoCall(call, context.checker)) {
251
- const step = extractRpcStep(call, context, varName);
231
+ const step = isInlineDoCall(call)
232
+ ? extractInlineStep(call, context)
233
+ : extractRpcStep(call, context, varName);
252
234
  if (step) {
253
235
  // Track output variable
254
236
  const type = context.checker.getTypeAtLocation(decl);
@@ -335,7 +317,9 @@ function extractExpressionStatement(statement, context) {
335
317
  if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
336
318
  const call = expr.expression;
337
319
  if (isWorkflowDoCall(call, context.checker)) {
338
- const step = extractRpcStep(call, context, outputVar);
320
+ const step = isInlineDoCall(call)
321
+ ? extractInlineStep(call, context)
322
+ : extractRpcStep(call, context, outputVar);
339
323
  // Track output variable if this is an assignment
340
324
  if (outputVar && step) {
341
325
  const type = context.checker.getTypeAtLocation(expr);
@@ -394,6 +378,41 @@ function extractRpcStep(call, context, outputVar) {
394
378
  return null;
395
379
  }
396
380
  }
381
+ /**
382
+ * Extract inline step from workflow.do() call with a function argument
383
+ */
384
+ function extractInlineStep(call, context) {
385
+ const args = call.arguments;
386
+ if (args.length < 2)
387
+ return null;
388
+ try {
389
+ const stepName = extractStringLiteral(args[0], context.checker);
390
+ const optionsArg = args.length >= 3 ? args[args.length - 1] : undefined;
391
+ const options = optionsArg && ts.isObjectLiteralExpression(optionsArg)
392
+ ? extractStepOptions(optionsArg, context)
393
+ : undefined;
394
+ return {
395
+ type: 'inline',
396
+ stepName,
397
+ options,
398
+ };
399
+ }
400
+ catch (error) {
401
+ context.errors.push({
402
+ message: `Failed to extract inline step: ${error instanceof Error ? error.message : String(error)}`,
403
+ node: call,
404
+ });
405
+ return null;
406
+ }
407
+ }
408
+ /**
409
+ * Check if the second argument of a workflow.do() call is a function
410
+ */
411
+ function isInlineDoCall(call) {
412
+ const secondArg = call.arguments[1];
413
+ return (!!secondArg &&
414
+ (ts.isArrowFunction(secondArg) || ts.isFunctionExpression(secondArg)));
415
+ }
397
416
  /**
398
417
  * Extract step options from options object
399
418
  */
@@ -1,6 +1,6 @@
1
1
  import * as ts from 'typescript';
2
2
  /**
3
- * Pattern detection helpers for simple workflow extraction
3
+ * Pattern detection helpers for DSL workflow extraction
4
4
  */
5
5
  /**
6
6
  * Check if a call expression is workflow.do()
@@ -43,6 +43,10 @@ export declare function isParallelGroup(node: ts.CallExpression): boolean;
43
43
  * Check if a for statement is a valid sequential fanout (for..of)
44
44
  */
45
45
  export declare function isSequentialFanout(node: ts.ForOfStatement): boolean;
46
+ /**
47
+ * Extract full source path from an expression (e.g., data.memberEmails)
48
+ */
49
+ export declare function extractSourcePath(expr: ts.Expression): string | null;
46
50
  /**
47
51
  * Extract the variable name from a for..of statement
48
52
  */
@@ -1,6 +1,6 @@
1
1
  import * as ts from 'typescript';
2
2
  /**
3
- * Pattern detection helpers for simple workflow extraction
3
+ * Pattern detection helpers for DSL workflow extraction
4
4
  */
5
5
  /**
6
6
  * Check if a call expression is workflow.do()
@@ -162,7 +162,7 @@ export function isSequentialFanout(node) {
162
162
  /**
163
163
  * Extract full source path from an expression (e.g., data.memberEmails)
164
164
  */
165
- function extractSourcePath(expr) {
165
+ export function extractSourcePath(expr) {
166
166
  if (ts.isIdentifier(expr)) {
167
167
  return expr.text;
168
168
  }
@@ -1,6 +1,6 @@
1
1
  import * as ts from 'typescript';
2
2
  /**
3
- * Validation rules for simple workflows
3
+ * Validation rules for DSL workflows
4
4
  */
5
5
  export interface ValidationError {
6
6
  message: string;
@@ -19,7 +19,9 @@ export interface ValidationError {
19
19
  * - ThrowStatement (for WorkflowCancelledException)
20
20
  * - Block (containers)
21
21
  */
22
- export declare function validateNoDisallowedPatterns(node: ts.Node): ValidationError[];
22
+ export declare function validateNoDisallowedPatterns(node: ts.Node, options?: {
23
+ allowInline?: boolean;
24
+ }): ValidationError[];
23
25
  /**
24
26
  * Validate that all workflow.do calls are awaited
25
27
  */
@@ -12,7 +12,7 @@ import * as ts from 'typescript';
12
12
  * - ThrowStatement (for WorkflowCancelledException)
13
13
  * - Block (containers)
14
14
  */
15
- export function validateNoDisallowedPatterns(node) {
15
+ export function validateNoDisallowedPatterns(node, options) {
16
16
  const errors = [];
17
17
  function visitBlock(block) {
18
18
  for (const statement of block.statements) {
@@ -30,7 +30,7 @@ export function validateNoDisallowedPatterns(node) {
30
30
  // Unknown/disallowed statement type
31
31
  const nodeType = ts.SyntaxKind[statement.kind];
32
32
  errors.push({
33
- message: `Statement type '${nodeType}' is not allowed in simple workflows. Allowed: const/let, if/else, switch/case, for..of, return, throw, and workflow calls. If this should be supported, please report the node type: ${nodeType}`,
33
+ message: `Statement type '${nodeType}' is not allowed in DSL workflows. Allowed: const/let, if/else, switch/case, for..of, return, throw, and workflow calls. If this should be supported, please report the node type: ${nodeType}`,
34
34
  node: statement,
35
35
  });
36
36
  }
@@ -40,7 +40,7 @@ export function validateNoDisallowedPatterns(node) {
40
40
  // Disallow while and do-while
41
41
  if (ts.isWhileStatement(node) || ts.isDoStatement(node)) {
42
42
  errors.push({
43
- message: 'while and do-while loops are not allowed in simple workflows',
43
+ message: 'while and do-while loops are not allowed in DSL workflows',
44
44
  node,
45
45
  });
46
46
  return;
@@ -48,13 +48,13 @@ export function validateNoDisallowedPatterns(node) {
48
48
  // Disallow for and for-in loops
49
49
  if (ts.isForInStatement(node) || ts.isForStatement(node)) {
50
50
  errors.push({
51
- message: 'for and for-in loops are not allowed in simple workflows. Use for-of instead.',
51
+ message: 'for and for-in loops are not allowed in DSL workflows. Use for-of instead.',
52
52
  node,
53
53
  });
54
54
  return;
55
55
  }
56
56
  // Check for inline workflow.do
57
- if (ts.isCallExpression(node)) {
57
+ if (!options?.allowInline && ts.isCallExpression(node)) {
58
58
  if (ts.isPropertyAccessExpression(node.expression)) {
59
59
  const propAccess = node.expression;
60
60
  if (propAccess.name.text === 'do' &&
@@ -65,7 +65,7 @@ export function validateNoDisallowedPatterns(node) {
65
65
  (ts.isArrowFunction(secondArg) ||
66
66
  ts.isFunctionExpression(secondArg))) {
67
67
  errors.push({
68
- message: 'Inline workflow.do with function argument is not allowed in simple workflows. Use RPC form instead.',
68
+ message: 'Inline workflow.do with function argument is not allowed in DSL workflows. Use pikkuWorkflowComplexFunc instead.',
69
69
  node,
70
70
  });
71
71
  return;
@@ -97,11 +97,19 @@ export function validateAwaitedCalls(node) {
97
97
  if (propAccess.name.text === 'all' &&
98
98
  ts.isIdentifier(propAccess.expression) &&
99
99
  propAccess.expression.text === 'Promise') {
100
- // console.log('[DEBUG] Found Promise.all, setting insidePromiseAll=true')
101
- // Visit children with insidePromiseAll = true
102
100
  ts.forEachChild(node, (child) => visit(child, parentIsAwait, true));
103
101
  return;
104
102
  }
103
+ // .push() on an array — workflow.do() inside is collecting promises
104
+ if (propAccess.name.text === 'push') {
105
+ ts.forEachChild(node, (child) => visit(child, parentIsAwait, true));
106
+ return;
107
+ }
108
+ }
109
+ // Array literal — workflow.do() inside is collecting promises
110
+ if (ts.isArrayLiteralExpression(node)) {
111
+ ts.forEachChild(node, (child) => visit(child, parentIsAwait, true));
112
+ return;
105
113
  }
106
114
  // Now check for workflow calls
107
115
  if (ts.isCallExpression(node)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.9",
3
+ "version": "0.12.10",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
38
- "@pikku/core": "^0.12.16",
38
+ "@pikku/core": "^0.12.18",
39
39
  "path-to-regexp": "^8.3.0",
40
40
  "ts-json-schema-generator": "^2.5.0",
41
41
  "tsx": "^4.21.0",
@@ -339,6 +339,7 @@ export const addFunctions: AddWiring = (
339
339
  let remote: boolean | undefined
340
340
  let mcp: boolean | undefined
341
341
  let readonly_: boolean | undefined
342
+ let deploy: 'serverless' | 'server' | 'auto' | undefined
342
343
  let approvalRequired: boolean | undefined
343
344
  let approvalDescription: string | undefined
344
345
  let version: number | undefined
@@ -422,6 +423,11 @@ export const addFunctions: AddWiring = (
422
423
  remote = getPropertyValue(firstArg, 'remote') as boolean | undefined
423
424
  mcp = getPropertyValue(firstArg, 'mcp') as boolean | undefined
424
425
  readonly_ = getPropertyValue(firstArg, 'readonly') as boolean | undefined
426
+ deploy = getPropertyValue(firstArg, 'deploy') as
427
+ | 'serverless'
428
+ | 'server'
429
+ | 'auto'
430
+ | undefined
425
431
  approvalRequired = getPropertyValue(firstArg, 'approvalRequired') as
426
432
  | boolean
427
433
  | undefined
@@ -783,6 +789,7 @@ export const addFunctions: AddWiring = (
783
789
  remote: remote || undefined,
784
790
  mcp: mcpEnabled || undefined,
785
791
  readonly: readonly_ || undefined,
792
+ deploy: deploy || undefined,
786
793
  approvalRequired: approvalRequired || undefined,
787
794
  approvalDescription: approvalDescription || undefined,
788
795
  version,
@@ -817,9 +824,7 @@ export const addFunctions: AddWiring = (
817
824
 
818
825
  if (mcpEnabled) {
819
826
  if (!description) {
820
- logger.warn(
821
- `MCP tool '${name}' is missing a description.`
822
- )
827
+ logger.warn(`MCP tool '${name}' is missing a description.`)
823
828
  }
824
829
  state.mcpEndpoints.files.add(node.getSourceFile().fileName)
825
830
  state.mcpEndpoints.toolsMeta[name] = {
@@ -155,7 +155,7 @@ function getWorkflowInvocations(
155
155
  }
156
156
 
157
157
  /**
158
- * Inspector for pikkuWorkflow() and pikkuSimpleWorkflow() calls
158
+ * Inspector for pikkuWorkflowFunc() and pikkuWorkflowComplexFunc() calls
159
159
  * Detects workflow registration and extracts metadata
160
160
  */
161
161
  export const addWorkflow: AddWiring = (logger, node, checker, state) => {
@@ -171,11 +171,11 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
171
171
  return
172
172
  }
173
173
 
174
- let wrapperType: 'dsl' | 'regular' | null = null
174
+ let wrapperType: 'dsl' | 'complex' | null = null
175
175
  if (expression.text === 'pikkuWorkflowFunc') {
176
176
  wrapperType = 'dsl'
177
177
  } else if (expression.text === 'pikkuWorkflowComplexFunc') {
178
- wrapperType = 'regular'
178
+ wrapperType = 'complex'
179
179
  } else {
180
180
  return
181
181
  }
@@ -267,7 +267,9 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
267
267
 
268
268
  // Try DSL workflow extraction first
269
269
  // Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
270
- const result = extractDSLWorkflow(node, checker)
270
+ const result = extractDSLWorkflow(node, checker, {
271
+ allowInline: wrapperType === 'complex',
272
+ })
271
273
 
272
274
  if (result.status === 'ok' && result.steps) {
273
275
  // Extraction succeeded
@@ -305,7 +307,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
305
307
  getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps)
306
308
  logger.critical(
307
309
  ErrorCode.INVALID_DSL_WORKFLOW,
308
- `Workflow '${workflowName}' uses pikkuWorkflowFunc but does not conform to DSL workflow rules:\n${result.reason || 'Unknown error'}`
310
+ `Workflow '${workflowName}' does not conform to DSL workflow rules:\n${result.reason || 'Unknown error'}`
309
311
  )
310
312
  return
311
313
  } else {
@@ -317,12 +319,10 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
317
319
  }
318
320
  }
319
321
 
320
- /**
321
- * For non-dsl workflows or pikkuWorkflowComplexFunc, run basic extraction
322
- * to ensure all RPC invocations are tracked for function registration.
323
- * This catches RPCs in Promise.all callbacks and other patterns DSL can't extract.
324
- */
325
- if (!dsl || wrapperType === 'regular') {
322
+ // For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
323
+ // patterns the DSL extractor doesn't handle (array+push, nested Promise.all
324
+ // with identifier args, etc.) are still registered as invoked functions.
325
+ if (wrapperType === 'complex') {
326
326
  getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps)
327
327
  }
328
328
 
@@ -27,6 +27,11 @@ export function extractStringLiteral(
27
27
  return result
28
28
  }
29
29
 
30
+ // Unwrap type assertions: `expr as Type` or `<Type>expr`
31
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
32
+ return extractStringLiteral(node.expression, checker)
33
+ }
34
+
30
35
  // Try to evaluate constant identifiers
31
36
  if (ts.isIdentifier(node)) {
32
37
  const symbol = checker.getSymbolAtLocation(node)