@pikku/inspector 0.12.8 → 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.
Files changed (32) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/add/add-functions.js +5 -3
  3. package/dist/add/add-mcp-prompt.js +4 -0
  4. package/dist/add/add-mcp-resource.js +4 -0
  5. package/dist/add/add-workflow.d.ts +1 -1
  6. package/dist/add/add-workflow.js +11 -11
  7. package/dist/utils/extract-function-name.d.ts +1 -0
  8. package/dist/utils/extract-function-name.js +27 -32
  9. package/dist/utils/extract-node-value.js +10 -1
  10. package/dist/utils/filter-inspector-state.js +7 -3
  11. package/dist/utils/resolve-versions.js +30 -0
  12. package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +5 -4
  13. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +47 -28
  14. package/dist/utils/workflow/dsl/patterns.d.ts +5 -1
  15. package/dist/utils/workflow/dsl/patterns.js +2 -2
  16. package/dist/utils/workflow/dsl/validation.d.ts +4 -2
  17. package/dist/utils/workflow/dsl/validation.js +16 -8
  18. package/package.json +2 -2
  19. package/src/add/add-functions.ts +9 -6
  20. package/src/add/add-mcp-prompt.ts +5 -0
  21. package/src/add/add-mcp-resource.ts +5 -0
  22. package/src/add/add-workflow.ts +12 -12
  23. package/src/utils/extract-function-name.ts +36 -37
  24. package/src/utils/extract-node-value.test.ts +67 -0
  25. package/src/utils/extract-node-value.ts +10 -1
  26. package/src/utils/filter-inspector-state.ts +7 -3
  27. package/src/utils/resolve-versions.test.ts +141 -0
  28. package/src/utils/resolve-versions.ts +37 -0
  29. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +58 -32
  30. package/src/utils/workflow/dsl/patterns.ts +2 -2
  31. package/src/utils/workflow/dsl/validation.ts +21 -9
  32. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
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
+
21
+ ## 0.12.9
22
+
23
+ ### Patch Changes
24
+
25
+ - 2ac6468: Fix workflow inspector crash when workflow.do() data object has a 'description' property
26
+ - fbcf5b9: Add version awareness to RPC handler: versioned functions now appear in the exposed RPC type map (e.g. `getData@v1`, `getData@v2`), enabling type-safe `rpc.invoke('getData@v1', data)` calls. Tree-shaking respects specific version filters without pulling in all versions. HTTP wirings correctly resolve versioned function IDs.
27
+ - Updated dependencies [fbcf5b9]
28
+ - @pikku/core@0.12.16
29
+
3
30
  ## 0.12.8
4
31
 
5
32
  ### 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,
@@ -615,15 +618,14 @@ export const addFunctions = (logger, node, checker, state, options) => {
615
618
  }
616
619
  if (mcpEnabled) {
617
620
  if (!description) {
618
- logger.critical(ErrorCode.MISSING_DESCRIPTION, `MCP tool '${name}' is missing a description.`);
619
- return;
621
+ logger.warn(`MCP tool '${name}' is missing a description.`);
620
622
  }
621
623
  state.mcpEndpoints.files.add(node.getSourceFile().fileName);
622
624
  state.mcpEndpoints.toolsMeta[name] = {
623
625
  pikkuFuncId,
624
626
  name,
625
627
  title: title || undefined,
626
- description,
628
+ description: description || undefined,
627
629
  summary,
628
630
  errors,
629
631
  tags,
@@ -58,6 +58,10 @@ export const addMCPPrompt = (logger, node, checker, state, options) => {
58
58
  }
59
59
  const inputSchema = fnMeta.inputs?.[0] || null;
60
60
  const outputSchema = fnMeta.outputs?.[0] || null;
61
+ if (!fnMeta.outputSchemaName) {
62
+ fnMeta.outputSchemaName = 'MCPPromptResponse';
63
+ fnMeta.outputs = ['MCPPromptResponse'];
64
+ }
61
65
  // --- resolve middleware ---
62
66
  const middleware = resolveMiddleware(state, obj, tags, checker);
63
67
  // --- resolve permissions ---
@@ -67,6 +67,10 @@ export const addMCPResource = (logger, node, checker, state, options) => {
67
67
  }
68
68
  const inputSchema = fnMeta.inputs?.[0] || null;
69
69
  const outputSchema = fnMeta.outputs?.[0] || null;
70
+ if (!fnMeta.outputSchemaName) {
71
+ fnMeta.outputSchemaName = 'MCPResourceResponse';
72
+ fnMeta.outputs = ['MCPResourceResponse'];
73
+ }
70
74
  // --- resolve middleware ---
71
75
  const middleware = resolveMiddleware(state, obj, tags, checker);
72
76
  // --- resolve permissions ---
@@ -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;
@@ -88,7 +88,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
88
88
  // workflow.do(stepName, rpcName|fn, data?, options?)
89
89
  const stepNameArg = args[0];
90
90
  const secondArg = args[1];
91
- const optionsArg = args.length >= 3 ? args[args.length - 1] : undefined;
91
+ const optionsArg = args.length >= 4 ? args[args.length - 1] : undefined;
92
92
  const stepName = extractStringLiteral(stepNameArg, checker);
93
93
  const description = extractDescription(optionsArg, checker) ?? undefined;
94
94
  // Determine form by checking 2nd argument type
@@ -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] = {
@@ -6,6 +6,7 @@ export type ExtractedFunctionName = {
6
6
  exportedName: string | null;
7
7
  propertyName: string | null;
8
8
  isHelper: boolean;
9
+ version: number | null;
9
10
  };
10
11
  export declare function makeContextBasedId(wiringType: string, ...segments: string[]): string;
11
12
  export declare function funcIdToTypeName(id: string): string;
@@ -1,5 +1,6 @@
1
1
  import * as ts from 'typescript';
2
2
  import { randomUUID } from 'crypto';
3
+ import { formatVersionedId } from '@pikku/core';
3
4
  export function makeContextBasedId(wiringType, ...segments) {
4
5
  return [wiringType, ...segments].join(':');
5
6
  }
@@ -20,6 +21,7 @@ export function extractFunctionName(callExpr, checker, rootDir) {
20
21
  propertyName: null,
21
22
  explicitName: null,
22
23
  isHelper: false,
24
+ version: null,
23
25
  };
24
26
  const workflowHelpers = new Set([
25
27
  'workflow',
@@ -103,16 +105,7 @@ export function extractFunctionName(callExpr, checker, rootDir) {
103
105
  // Check for object with 'name' property in first argument
104
106
  const firstArg = args[0];
105
107
  if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
106
- for (const prop of firstArg.properties) {
107
- if (ts.isPropertyAssignment(prop) &&
108
- ts.isIdentifier(prop.name) &&
109
- prop.name.text === 'override' &&
110
- ts.isStringLiteral(prop.initializer)) {
111
- // Priority 1: Object with override property
112
- result.explicitName = prop.initializer.text;
113
- break;
114
- }
115
- }
108
+ extractOverrideAndVersion(firstArg, result);
116
109
  }
117
110
  // Special handling for pikkuSessionlessFunc pattern - use the arrow function directly
118
111
  if (expression.text.startsWith('pikku')) {
@@ -274,19 +267,9 @@ export function extractFunctionName(callExpr, checker, rootDir) {
274
267
  if (ts.isCallExpression(decl.initializer) &&
275
268
  ts.isIdentifier(decl.initializer.expression) &&
276
269
  decl.initializer.expression.text.startsWith('pikku')) {
277
- // Check for object with 'override' property in first argument
278
270
  const firstArg = decl.initializer.arguments[0];
279
271
  if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
280
- for (const prop of firstArg.properties) {
281
- if (ts.isPropertyAssignment(prop) &&
282
- ts.isIdentifier(prop.name) &&
283
- prop.name.text === 'override' &&
284
- ts.isStringLiteral(prop.initializer)) {
285
- // Priority 1: Object with override property
286
- result.explicitName = prop.initializer.text;
287
- break;
288
- }
289
- }
272
+ extractOverrideAndVersion(firstArg, result);
290
273
  }
291
274
  if (decl.initializer.expression.text.startsWith('pikku')) {
292
275
  if (firstArg &&
@@ -340,17 +323,7 @@ export function extractFunctionName(callExpr, checker, rootDir) {
340
323
  else if (ts.isCallExpression(callExpr)) {
341
324
  const firstArg = callExpr.arguments[0];
342
325
  if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
343
- for (const prop of firstArg.properties) {
344
- if (ts.isPropertyAssignment(prop) &&
345
- ts.isIdentifier(prop.name) &&
346
- prop.name.text === 'override' &&
347
- ts.isStringLiteral(prop.initializer) &&
348
- !result.explicitName // Only set if not already set
349
- ) {
350
- result.explicitName = prop.initializer.text;
351
- break;
352
- }
353
- }
326
+ extractOverrideAndVersion(firstArg, result);
354
327
  }
355
328
  }
356
329
  // Apply name priority logic
@@ -364,6 +337,9 @@ export function extractFunctionName(callExpr, checker, rootDir) {
364
337
  else {
365
338
  result.pikkuFuncId = `__temp_${randomUUID()}`;
366
339
  }
340
+ if (result.version !== null) {
341
+ result.pikkuFuncId = formatVersionedId(result.pikkuFuncId, result.version);
342
+ }
367
343
  return result;
368
344
  }
369
345
  /**
@@ -420,3 +396,22 @@ export function isNamedExport(declaration, checker) {
420
396
  }
421
397
  return false;
422
398
  }
399
+ function extractOverrideAndVersion(objLiteral, result) {
400
+ for (const prop of objLiteral.properties) {
401
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
402
+ if (prop.name.text === 'override' &&
403
+ ts.isStringLiteral(prop.initializer) &&
404
+ !result.explicitName) {
405
+ result.explicitName = prop.initializer.text;
406
+ }
407
+ else if (prop.name.text === 'version' &&
408
+ ts.isNumericLiteral(prop.initializer) &&
409
+ result.version === null) {
410
+ const parsed = Number(prop.initializer.text);
411
+ if (Number.isInteger(parsed) && parsed >= 1) {
412
+ result.version = parsed;
413
+ }
414
+ }
415
+ }
416
+ }
417
+ }
@@ -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);
@@ -84,7 +88,12 @@ export function extractDescription(optionsNode, checker) {
84
88
  if (!optionsNode || !ts.isObjectLiteralExpression(optionsNode)) {
85
89
  return null;
86
90
  }
87
- return extractPropertyString(optionsNode, 'description', checker);
91
+ try {
92
+ return extractPropertyString(optionsNode, 'description', checker);
93
+ }
94
+ catch {
95
+ return null;
96
+ }
88
97
  }
89
98
  /**
90
99
  * Extract duration value (number or string)
@@ -532,11 +532,15 @@ export function filterInspectorState(state, filters, logger) {
532
532
  filteredState.serviceAggregation.usedFunctions.add(funcId);
533
533
  }
534
534
  }
535
- // Post-filter version expansion: include all versions of matched functions
535
+ // Post-filter version expansion: when an unversioned base name is matched,
536
+ // include all its versions. Specific version matches (e.g. analyzeData@v1)
537
+ // do NOT expand to include other versions.
536
538
  const includedBaseNames = new Set();
537
539
  for (const funcId of filteredState.serviceAggregation.usedFunctions) {
538
- const { baseName } = parseVersionedId(funcId);
539
- includedBaseNames.add(baseName);
540
+ const { baseName, version } = parseVersionedId(funcId);
541
+ if (version === null) {
542
+ includedBaseNames.add(baseName);
543
+ }
540
544
  }
541
545
  if (includedBaseNames.size > 0) {
542
546
  for (const funcId of Object.keys(state.functions.meta)) {
@@ -66,13 +66,43 @@ export function resolveLatestVersions(state, logger) {
66
66
  if (state.rpc.exposedMeta[baseName] === oldId) {
67
67
  state.rpc.exposedMeta[baseName] = newId;
68
68
  }
69
+ updateWiringReferences(state, oldId, newId);
69
70
  }
70
71
  else {
71
72
  const latest = group.explicit.reduce((a, b) => a.version > b.version ? a : b);
72
73
  state.rpc.internalMeta[baseName] = latest.id;
73
74
  }
75
+ if (state.rpc.exposedMeta[baseName]) {
76
+ const latestId = state.rpc.internalMeta[baseName];
77
+ state.rpc.exposedMeta[baseName] = latestId;
78
+ for (const entry of group.explicit) {
79
+ state.rpc.exposedMeta[entry.id] = entry.id;
80
+ const fileEntry = state.rpc.internalFiles.get(entry.id);
81
+ if (fileEntry) {
82
+ state.rpc.exposedFiles.set(entry.id, fileEntry);
83
+ }
84
+ }
85
+ if (group.unversioned) {
86
+ state.rpc.exposedMeta[latestId] = latestId;
87
+ const fileEntry = state.rpc.internalFiles.get(latestId);
88
+ if (fileEntry) {
89
+ state.rpc.exposedFiles.set(latestId, fileEntry);
90
+ }
91
+ }
92
+ }
74
93
  for (const entry of group.explicit) {
75
94
  state.rpc.invokedFunctions.add(entry.id);
76
95
  }
77
96
  }
78
97
  }
98
+ function updateWiringReferences(state, oldId, newId) {
99
+ if (state.http) {
100
+ for (const methods of Object.values(state.http.meta)) {
101
+ for (const meta of Object.values(methods)) {
102
+ if (meta.pikkuFuncId === oldId) {
103
+ meta.pikkuFuncId = newId;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
@@ -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
  */