@pikku/inspector 0.12.46 → 0.12.48
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 +197 -0
- package/README.md +28 -2
- package/dist/add/add-feature.d.ts +19 -0
- package/dist/add/add-feature.js +200 -0
- package/dist/add/add-functions.js +24 -9
- package/dist/add/add-workflow.js +22 -29
- package/dist/error-codes.d.ts +3 -0
- package/dist/error-codes.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/inspector.js +39 -2
- package/dist/types.d.ts +43 -0
- package/dist/utils/extract-function-name.js +27 -0
- package/dist/utils/post-process.d.ts +26 -0
- package/dist/utils/post-process.js +98 -0
- package/dist/utils/serialize-inspector-state.d.ts +2 -1
- package/dist/utils/serialize-inspector-state.js +2 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +203 -19
- package/dist/utils/workflow/dsl/patterns.d.ts +21 -0
- package/dist/utils/workflow/dsl/patterns.js +30 -0
- package/dist/utils/workflow/dsl/validation.d.ts +5 -0
- package/dist/utils/workflow/dsl/validation.js +9 -0
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +36 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +5 -1
- package/dist/visit.js +2 -0
- package/package.json +4 -3
- package/src/add/add-feature.test.ts +306 -0
- package/src/add/add-feature.ts +253 -0
- package/src/add/add-functions.ts +29 -9
- package/src/add/add-scenario-step.test.ts +311 -0
- package/src/add/add-workflow-silent-drops.test.ts +98 -0
- package/src/add/add-workflow.ts +25 -37
- package/src/add/scenario-not-extractable.test.ts +159 -0
- package/src/add/scenario-services.test.ts +103 -0
- package/src/add/scenario-skip.test.ts +83 -0
- package/src/error-codes.ts +3 -0
- package/src/index.ts +1 -0
- package/src/inspector.ts +50 -1
- package/src/tsconfig-paths.test.ts +90 -0
- package/src/types.ts +45 -0
- package/src/utils/extract-function-name.ts +36 -0
- package/src/utils/post-process.test.ts +36 -2
- package/src/utils/post-process.ts +111 -0
- package/src/utils/serialize-inspector-state.ts +8 -1
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +236 -22
- package/src/utils/workflow/dsl/patterns.ts +38 -0
- package/src/utils/workflow/dsl/validation.ts +12 -0
- package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +35 -0
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +36 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +5 -0
- package/src/visit.ts +2 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, extractActorFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, isWorkflowApprovalCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
|
|
2
|
+
import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, isScenarioStepCall, getScenarioStepPhase, DYNAMIC_SCENARIO_STEP_TARGET, extractActorFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, isWorkflowApprovalCall, 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
|
/**
|
|
@@ -17,7 +17,7 @@ export function extractDSLWorkflow(funcNode, checker, options) {
|
|
|
17
17
|
}
|
|
18
18
|
// Extract input parameter name (second parameter)
|
|
19
19
|
const inputParamName = extractInputParamName(arrowFunc);
|
|
20
|
-
if (
|
|
20
|
+
if (inputParamName === undefined) {
|
|
21
21
|
return {
|
|
22
22
|
status: 'error',
|
|
23
23
|
reason: 'Could not determine input parameter name',
|
|
@@ -121,7 +121,13 @@ function findWorkflowFunction(node) {
|
|
|
121
121
|
return null;
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
124
|
-
* Extract the input parameter name from the arrow function
|
|
124
|
+
* Extract the input parameter name from the arrow function.
|
|
125
|
+
*
|
|
126
|
+
* Returns `null` when no input parameter is declared at all. That is
|
|
127
|
+
* legitimate — a workflow may ignore its input — and nothing in the body can
|
|
128
|
+
* reference it, so every name comparison against `null` correctly fails.
|
|
129
|
+
* Returns `undefined` when an input parameter is declared but is not a plain
|
|
130
|
+
* identifier (destructured), which the extractor cannot track by name.
|
|
125
131
|
*/
|
|
126
132
|
function extractInputParamName(arrowFunc) {
|
|
127
133
|
if (arrowFunc.parameters.length < 2) {
|
|
@@ -131,7 +137,7 @@ function extractInputParamName(arrowFunc) {
|
|
|
131
137
|
if (ts.isIdentifier(secondParam.name)) {
|
|
132
138
|
return secondParam.name.text;
|
|
133
139
|
}
|
|
134
|
-
return
|
|
140
|
+
return undefined;
|
|
135
141
|
}
|
|
136
142
|
/**
|
|
137
143
|
* Extract steps from the function body
|
|
@@ -179,7 +185,33 @@ function extractStep(statement, context) {
|
|
|
179
185
|
}
|
|
180
186
|
// For-of statement (sequential fanout)
|
|
181
187
|
if (ts.isForOfStatement(statement)) {
|
|
182
|
-
|
|
188
|
+
const reason = {};
|
|
189
|
+
const fanout = extractSequentialFanout(statement, context, reason);
|
|
190
|
+
if (!fanout && containsWorkflowCall(statement.statement, context.checker)) {
|
|
191
|
+
// A for-of the DSL can't model as a sequential fanout (its iterable is not
|
|
192
|
+
// a data-array identifier/field — e.g. a counting loop
|
|
193
|
+
// `for (const i of [...Array(N).keys()])`). Silently returning null here
|
|
194
|
+
// used to drop the loop AND every `workflow.do` inside it, so the workflow
|
|
195
|
+
// serialized with zero steps and its step functions were never registered
|
|
196
|
+
// (→ runtime "Function not found"). Fail loudly instead: pikkuWorkflowFunc
|
|
197
|
+
// reports INVALID_DSL_WORKFLOW; pikkuWorkflowComplexFunc falls back to the
|
|
198
|
+
// basic AST walk (which DOES register loop-invoked functions), so a genuine
|
|
199
|
+
// control-flow loop belongs in a complex workflow.
|
|
200
|
+
//
|
|
201
|
+
// Guarded on the body actually containing a workflow call: a loop that only
|
|
202
|
+
// massages locals (building a lookup, summing a total) has no step to lose,
|
|
203
|
+
// and erroring on it would be a false positive — the whole hazard is a
|
|
204
|
+
// dropped `workflow.do`, not a dropped `for`.
|
|
205
|
+
context.errors.push({
|
|
206
|
+
message: `The for-of loop '${statement.getText().slice(0, 60)}' can't be expressed in a DSL workflow — ` +
|
|
207
|
+
(reason.message ??
|
|
208
|
+
`its iterable must be a data array (an identifier or field like 'data.items'), not a computed/inline ` +
|
|
209
|
+
`iterable such as '[...Array(n).keys()]'. Use pikkuWorkflowComplexFunc for a control-flow/counting loop, ` +
|
|
210
|
+
`or iterate over a real input array.`),
|
|
211
|
+
node: statement,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return fanout;
|
|
183
215
|
}
|
|
184
216
|
// Return statement
|
|
185
217
|
if (ts.isReturnStatement(statement)) {
|
|
@@ -231,10 +263,12 @@ function extractVariableDeclaration(statement, context) {
|
|
|
231
263
|
// Check for await workflow.do(...)
|
|
232
264
|
if (ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)) {
|
|
233
265
|
const call = init.expression;
|
|
234
|
-
if (isWorkflowDoCall(call, context.checker)) {
|
|
235
|
-
const step =
|
|
236
|
-
?
|
|
237
|
-
:
|
|
266
|
+
if (isWorkflowDoCall(call, context.checker) || isScenarioStepCall(call)) {
|
|
267
|
+
const step = isScenarioStepCall(call)
|
|
268
|
+
? extractScenarioStep(call, context, varName)
|
|
269
|
+
: isInlineDoCall(call)
|
|
270
|
+
? extractInlineStep(call, context)
|
|
271
|
+
: extractRpcStep(call, context, varName);
|
|
238
272
|
if (step) {
|
|
239
273
|
// Track output variable
|
|
240
274
|
const type = context.checker.getTypeAtLocation(decl);
|
|
@@ -356,6 +390,7 @@ function extractDestructuredDeclaration(statement, decl, context) {
|
|
|
356
390
|
: null;
|
|
357
391
|
const destructuresAStep = stepCall !== null &&
|
|
358
392
|
(isWorkflowDoCall(stepCall, context.checker) ||
|
|
393
|
+
isScenarioStepCall(stepCall) ||
|
|
359
394
|
isParallelGroup(stepCall) ||
|
|
360
395
|
isParallelFanout(stepCall));
|
|
361
396
|
if (!destructuresAStep)
|
|
@@ -413,10 +448,12 @@ function extractExpressionStatement(statement, context) {
|
|
|
413
448
|
// await workflow.do(...)
|
|
414
449
|
if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
|
|
415
450
|
const call = expr.expression;
|
|
416
|
-
if (isWorkflowDoCall(call, context.checker)) {
|
|
417
|
-
const step =
|
|
418
|
-
?
|
|
419
|
-
:
|
|
451
|
+
if (isWorkflowDoCall(call, context.checker) || isScenarioStepCall(call)) {
|
|
452
|
+
const step = isScenarioStepCall(call)
|
|
453
|
+
? extractScenarioStep(call, context, outputVar)
|
|
454
|
+
: isInlineDoCall(call)
|
|
455
|
+
? extractInlineStep(call, context)
|
|
456
|
+
: extractRpcStep(call, context, outputVar);
|
|
420
457
|
// Track output variable if this is an assignment
|
|
421
458
|
if (outputVar && step) {
|
|
422
459
|
const type = context.checker.getTypeAtLocation(expr);
|
|
@@ -496,6 +533,57 @@ function extractRpcStep(call, context, outputVar) {
|
|
|
496
533
|
return null;
|
|
497
534
|
}
|
|
498
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Extract a scenario step from scenario.step/given/when/then(stepName,
|
|
538
|
+
* stepFunc, data?, options?).
|
|
539
|
+
*
|
|
540
|
+
* The step target must be a static string literal so it can be bundled, typed
|
|
541
|
+
* and drawn. When it isn't, the step is still recorded — with a `<dynamic>`
|
|
542
|
+
* target — so post-processing can raise PKU678 rather than the call silently
|
|
543
|
+
* vanishing from the graph.
|
|
544
|
+
*/
|
|
545
|
+
function extractScenarioStep(call, context, outputVar) {
|
|
546
|
+
const phase = getScenarioStepPhase(call);
|
|
547
|
+
if (!phase) {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
const args = call.arguments;
|
|
551
|
+
if (args.length < 2) {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
const stepName = extractStringLiteral(args[0], context.checker);
|
|
556
|
+
let stepFunc = DYNAMIC_SCENARIO_STEP_TARGET;
|
|
557
|
+
try {
|
|
558
|
+
stepFunc = extractStringLiteral(args[1], context.checker);
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
// Left as '<dynamic>' — validated in post-processing (PKU678).
|
|
562
|
+
}
|
|
563
|
+
const inputs = args.length >= 3 ? extractInputSources(args[2], context) : undefined;
|
|
564
|
+
const optionsArg = args.length >= 4 ? args[3] : undefined;
|
|
565
|
+
const options = optionsArg && ts.isObjectLiteralExpression(optionsArg)
|
|
566
|
+
? extractStepOptions(optionsArg, context)
|
|
567
|
+
: undefined;
|
|
568
|
+
return {
|
|
569
|
+
type: 'scenarioStep',
|
|
570
|
+
stepName,
|
|
571
|
+
stepFunc,
|
|
572
|
+
phase,
|
|
573
|
+
outputVar,
|
|
574
|
+
inputs,
|
|
575
|
+
options,
|
|
576
|
+
actor: extractActorFromOptions(optionsArg),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
context.errors.push({
|
|
581
|
+
message: `Failed to extract scenario step: ${error instanceof Error ? error.message : String(error)}`,
|
|
582
|
+
node: call,
|
|
583
|
+
});
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
499
587
|
/**
|
|
500
588
|
* Extract inline step from workflow.do() call with a function argument
|
|
501
589
|
*/
|
|
@@ -952,11 +1040,14 @@ function extractFanoutBodyStep(stmt, childContext, body) {
|
|
|
952
1040
|
return;
|
|
953
1041
|
}
|
|
954
1042
|
const call = init.expression;
|
|
955
|
-
if (!isWorkflowDoCall(call, childContext.checker)
|
|
1043
|
+
if (!isWorkflowDoCall(call, childContext.checker) &&
|
|
1044
|
+
!isScenarioStepCall(call)) {
|
|
956
1045
|
return;
|
|
957
1046
|
}
|
|
958
1047
|
const varName = ts.isIdentifier(decl.name) ? decl.name.text : undefined;
|
|
959
|
-
const step =
|
|
1048
|
+
const step = isScenarioStepCall(call)
|
|
1049
|
+
? extractScenarioStep(call, childContext, varName)
|
|
1050
|
+
: extractRpcStep(call, childContext, varName);
|
|
960
1051
|
if (!step) {
|
|
961
1052
|
return;
|
|
962
1053
|
}
|
|
@@ -999,6 +1090,13 @@ function extractFanoutBodyStep(stmt, childContext, body) {
|
|
|
999
1090
|
}
|
|
1000
1091
|
return;
|
|
1001
1092
|
}
|
|
1093
|
+
if (isScenarioStepCall(call)) {
|
|
1094
|
+
const scenarioStep = extractScenarioStep(call, childContext);
|
|
1095
|
+
if (scenarioStep) {
|
|
1096
|
+
body.push(scenarioStep);
|
|
1097
|
+
}
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1002
1100
|
if (!isWorkflowDoCall(call, childContext.checker)) {
|
|
1003
1101
|
return;
|
|
1004
1102
|
}
|
|
@@ -1059,7 +1157,13 @@ function extractParallelFanout(call, context) {
|
|
|
1059
1157
|
: ts.isCallExpression(expr)
|
|
1060
1158
|
? expr
|
|
1061
1159
|
: null;
|
|
1062
|
-
if (doCall &&
|
|
1160
|
+
if (doCall && isScenarioStepCall(doCall)) {
|
|
1161
|
+
const step = extractScenarioStep(doCall, childContext);
|
|
1162
|
+
if (step) {
|
|
1163
|
+
body.push(step);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
else if (doCall && isWorkflowDoCall(doCall, context.checker)) {
|
|
1063
1167
|
const step = extractRpcStep(doCall, childContext);
|
|
1064
1168
|
if (step) {
|
|
1065
1169
|
body.push(step);
|
|
@@ -1087,7 +1191,16 @@ function extractParallelGroup(call, context) {
|
|
|
1087
1191
|
}
|
|
1088
1192
|
const children = [];
|
|
1089
1193
|
for (const elem of arrayArg.elements) {
|
|
1090
|
-
if (ts.isCallExpression(elem)
|
|
1194
|
+
if (!ts.isCallExpression(elem)) {
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (isScenarioStepCall(elem)) {
|
|
1198
|
+
const step = extractScenarioStep(elem, context);
|
|
1199
|
+
if (step) {
|
|
1200
|
+
children.push(step);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
else if (isWorkflowDoCall(elem, context.checker)) {
|
|
1091
1204
|
const step = extractRpcStep(elem, context);
|
|
1092
1205
|
if (step) {
|
|
1093
1206
|
children.push(step);
|
|
@@ -1105,7 +1218,35 @@ function extractParallelGroup(call, context) {
|
|
|
1105
1218
|
/**
|
|
1106
1219
|
* Extract sequential fanout from for-of loop
|
|
1107
1220
|
*/
|
|
1108
|
-
|
|
1221
|
+
/**
|
|
1222
|
+
* Does this subtree call the workflow wire at all?
|
|
1223
|
+
*
|
|
1224
|
+
* Used to tell "the DSL can't model this loop, and a step would be silently
|
|
1225
|
+
* lost" from "the DSL can't model this loop, and there is nothing in it to
|
|
1226
|
+
* lose". Only the first is worth failing a build over.
|
|
1227
|
+
*/
|
|
1228
|
+
function containsWorkflowCall(node, checker) {
|
|
1229
|
+
let found = false;
|
|
1230
|
+
const visit = (child) => {
|
|
1231
|
+
if (found)
|
|
1232
|
+
return;
|
|
1233
|
+
if (ts.isCallExpression(child)) {
|
|
1234
|
+
if (isWorkflowDoCall(child, checker) ||
|
|
1235
|
+
isScenarioStepCall(child) ||
|
|
1236
|
+
isWorkflowSleepCall(child, checker) ||
|
|
1237
|
+
isWorkflowSuspendCall(child, checker) ||
|
|
1238
|
+
isWorkflowApprovalCall(child, checker) ||
|
|
1239
|
+
isWorkflowExpectEventuallyCall(child)) {
|
|
1240
|
+
found = true;
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
ts.forEachChild(child, visit);
|
|
1245
|
+
};
|
|
1246
|
+
visit(node);
|
|
1247
|
+
return found;
|
|
1248
|
+
}
|
|
1249
|
+
function extractSequentialFanout(statement, context, reason) {
|
|
1109
1250
|
if (!isSequentialFanout(statement)) {
|
|
1110
1251
|
return null;
|
|
1111
1252
|
}
|
|
@@ -1189,6 +1330,16 @@ function extractSequentialFanout(statement, context) {
|
|
|
1189
1330
|
}
|
|
1190
1331
|
}
|
|
1191
1332
|
if (body.length === 0) {
|
|
1333
|
+
// The iterable was fine — the body yielded nothing the DSL can model. The
|
|
1334
|
+
// usual cause is a `workflow.do` nested inside an `if`/`switch`, because
|
|
1335
|
+
// `FanoutStepMeta['body']` is a flat list of steps with no branch member:
|
|
1336
|
+
// the DSL has no way to say "conditionally run this step per item".
|
|
1337
|
+
const branching = bodyStatements.some((stmt) => ts.isIfStatement(stmt) || ts.isSwitchStatement(stmt));
|
|
1338
|
+
if (reason) {
|
|
1339
|
+
reason.message = branching
|
|
1340
|
+
? `its body only calls 'workflow.do' inside an if/switch, and a DSL fanout body is a flat list of steps with no branch member — the DSL cannot express a per-item condition. Use pikkuWorkflowComplexFunc, or lift the condition out of the loop.`
|
|
1341
|
+
: `its body contains no 'workflow.do' call the DSL can model. Use pikkuWorkflowComplexFunc for a loop that does other work.`;
|
|
1342
|
+
}
|
|
1192
1343
|
return null;
|
|
1193
1344
|
}
|
|
1194
1345
|
return {
|
|
@@ -1374,7 +1525,11 @@ function extractInputSources(node, context) {
|
|
|
1374
1525
|
source = { from: 'outputVar', name: varName };
|
|
1375
1526
|
}
|
|
1376
1527
|
else {
|
|
1377
|
-
|
|
1528
|
+
const constant = resolveConstantValue(prop.name, context.checker);
|
|
1529
|
+
source =
|
|
1530
|
+
constant !== undefined
|
|
1531
|
+
? { from: 'literal', value: constant }
|
|
1532
|
+
: { from: 'input', path: varName };
|
|
1378
1533
|
}
|
|
1379
1534
|
}
|
|
1380
1535
|
else if (ts.isPropertyAssignment(prop)) {
|
|
@@ -1449,6 +1604,10 @@ function extractInputSource(node, context) {
|
|
|
1449
1604
|
if (context.outputVars.has(varName)) {
|
|
1450
1605
|
return { from: 'outputVar', name: varName };
|
|
1451
1606
|
}
|
|
1607
|
+
const constant = resolveConstantValue(node, context.checker);
|
|
1608
|
+
if (constant !== undefined) {
|
|
1609
|
+
return { from: 'literal', value: constant };
|
|
1610
|
+
}
|
|
1452
1611
|
// Assume it's from input
|
|
1453
1612
|
return { from: 'input', path: varName };
|
|
1454
1613
|
}
|
|
@@ -1528,6 +1687,31 @@ function extractInputSource(node, context) {
|
|
|
1528
1687
|
}
|
|
1529
1688
|
return null;
|
|
1530
1689
|
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Resolve an identifier that names a constant to the value it was declared with.
|
|
1692
|
+
*
|
|
1693
|
+
* Without this, a name the extractor does not recognise falls through to
|
|
1694
|
+
* `{ from: 'input' }` and is serialized as `{ $ref: 'trigger', path: <name> }` —
|
|
1695
|
+
* so a module-level `const RESOURCE_ID = 'x'` used as step input silently
|
|
1696
|
+
* becomes a read of a trigger field that does not exist. Only `const`
|
|
1697
|
+
* declarations with a fully literal initializer resolve; anything else (a name
|
|
1698
|
+
* bound out of the trigger, a computed value) falls through unchanged.
|
|
1699
|
+
*/
|
|
1700
|
+
function resolveConstantValue(node, checker) {
|
|
1701
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
1702
|
+
const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
|
|
1703
|
+
if (!declaration || !ts.isVariableDeclaration(declaration)) {
|
|
1704
|
+
return undefined;
|
|
1705
|
+
}
|
|
1706
|
+
const declarationList = declaration.parent;
|
|
1707
|
+
if (!ts.isVariableDeclarationList(declarationList) ||
|
|
1708
|
+
!(declarationList.flags & ts.NodeFlags.Const)) {
|
|
1709
|
+
return undefined;
|
|
1710
|
+
}
|
|
1711
|
+
return declaration.initializer
|
|
1712
|
+
? extractLiteralValue(declaration.initializer)
|
|
1713
|
+
: undefined;
|
|
1714
|
+
}
|
|
1531
1715
|
/**
|
|
1532
1716
|
* Extract a literal value from an expression
|
|
1533
1717
|
*/
|
|
@@ -13,6 +13,26 @@ export declare function isWorkflowWireIdentifier(expr: ts.Expression): boolean;
|
|
|
13
13
|
* (both are RPC steps; expectEventually is the polling variant used by scenarios)
|
|
14
14
|
*/
|
|
15
15
|
export declare function isWorkflowDoCall(node: ts.CallExpression, checker: ts.TypeChecker): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Placeholder recorded when a scenario step's target isn't a static string
|
|
18
|
+
* literal. The step is kept rather than dropped so post-processing can raise
|
|
19
|
+
* PKU678 against it — a dropped step is the silent-failure mode this repo has
|
|
20
|
+
* hit repeatedly on workflow graph round-trips.
|
|
21
|
+
*/
|
|
22
|
+
export declare const DYNAMIC_SCENARIO_STEP_TARGET = "<dynamic>";
|
|
23
|
+
declare const SCENARIO_STEP_PHASES: readonly ["step", "given", "when", "then"];
|
|
24
|
+
export type ScenarioStepPhaseName = (typeof SCENARIO_STEP_PHASES)[number];
|
|
25
|
+
/**
|
|
26
|
+
* Check if a call expression is scenario.step()/given()/when()/then() — a
|
|
27
|
+
* declared `pikkuScenarioStep` invoked by name. `given`/`when`/`then` are pure
|
|
28
|
+
* sugar over `step`: the phase only changes the prose a reporter renders.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isScenarioStepCall(node: ts.CallExpression): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The Gherkin-style phase of a scenario step call, or undefined when the call
|
|
33
|
+
* isn't one.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getScenarioStepPhase(node: ts.CallExpression): ScenarioStepPhaseName | undefined;
|
|
16
36
|
/**
|
|
17
37
|
* Check if a call expression is workflow.expectEventually()
|
|
18
38
|
*/
|
|
@@ -87,3 +107,4 @@ export declare function isArrayType(type: ts.Type, checker: ts.TypeChecker): boo
|
|
|
87
107
|
* Get the source text of a node (for condition expressions)
|
|
88
108
|
*/
|
|
89
109
|
export declare function getSourceText(node: ts.Node): string;
|
|
110
|
+
export {};
|
|
@@ -24,6 +24,36 @@ export function isWorkflowDoCall(node, checker) {
|
|
|
24
24
|
propAccess.name.text === 'expectEventually') &&
|
|
25
25
|
isWorkflowWireIdentifier(propAccess.expression));
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Placeholder recorded when a scenario step's target isn't a static string
|
|
29
|
+
* literal. The step is kept rather than dropped so post-processing can raise
|
|
30
|
+
* PKU678 against it — a dropped step is the silent-failure mode this repo has
|
|
31
|
+
* hit repeatedly on workflow graph round-trips.
|
|
32
|
+
*/
|
|
33
|
+
export const DYNAMIC_SCENARIO_STEP_TARGET = '<dynamic>';
|
|
34
|
+
const SCENARIO_STEP_PHASES = ['step', 'given', 'when', 'then'];
|
|
35
|
+
/**
|
|
36
|
+
* Check if a call expression is scenario.step()/given()/when()/then() — a
|
|
37
|
+
* declared `pikkuScenarioStep` invoked by name. `given`/`when`/`then` are pure
|
|
38
|
+
* sugar over `step`: the phase only changes the prose a reporter renders.
|
|
39
|
+
*/
|
|
40
|
+
export function isScenarioStepCall(node) {
|
|
41
|
+
return getScenarioStepPhase(node) !== undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The Gherkin-style phase of a scenario step call, or undefined when the call
|
|
45
|
+
* isn't one.
|
|
46
|
+
*/
|
|
47
|
+
export function getScenarioStepPhase(node) {
|
|
48
|
+
if (!ts.isPropertyAccessExpression(node.expression)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const propAccess = node.expression;
|
|
52
|
+
if (!isWorkflowWireIdentifier(propAccess.expression)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
return SCENARIO_STEP_PHASES.find((phase) => phase === propAccess.name.text);
|
|
56
|
+
}
|
|
27
57
|
/**
|
|
28
58
|
* Check if a call expression is workflow.expectEventually()
|
|
29
59
|
*/
|
|
@@ -18,6 +18,11 @@ export interface ValidationError {
|
|
|
18
18
|
* - ReturnStatement
|
|
19
19
|
* - ThrowStatement (for WorkflowCancelledException)
|
|
20
20
|
* - Block (containers)
|
|
21
|
+
*
|
|
22
|
+
* When `allowInline` is set (complex workflows and scenarios), the bodies of
|
|
23
|
+
* closures are skipped: a callback is ordinary TypeScript, not DSL, so holding
|
|
24
|
+
* it to the statement whitelist would reject valid code. Plain DSL workflows
|
|
25
|
+
* still descend into callbacks, which is what validates fanout bodies.
|
|
21
26
|
*/
|
|
22
27
|
export declare function validateNoDisallowedPatterns(node: ts.Node, options?: {
|
|
23
28
|
allowInline?: boolean;
|
|
@@ -12,6 +12,11 @@ import { isWorkflowWireIdentifier } from './patterns.js';
|
|
|
12
12
|
* - ReturnStatement
|
|
13
13
|
* - ThrowStatement (for WorkflowCancelledException)
|
|
14
14
|
* - Block (containers)
|
|
15
|
+
*
|
|
16
|
+
* When `allowInline` is set (complex workflows and scenarios), the bodies of
|
|
17
|
+
* closures are skipped: a callback is ordinary TypeScript, not DSL, so holding
|
|
18
|
+
* it to the statement whitelist would reject valid code. Plain DSL workflows
|
|
19
|
+
* still descend into callbacks, which is what validates fanout bodies.
|
|
15
20
|
*/
|
|
16
21
|
export function validateNoDisallowedPatterns(node, options) {
|
|
17
22
|
const errors = [];
|
|
@@ -38,6 +43,10 @@ export function validateNoDisallowedPatterns(node, options) {
|
|
|
38
43
|
}
|
|
39
44
|
}
|
|
40
45
|
function visitNode(node) {
|
|
46
|
+
if (options?.allowInline &&
|
|
47
|
+
(ts.isArrowFunction(node) || ts.isFunctionExpression(node))) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
41
50
|
// Disallow while and do-while
|
|
42
51
|
if (ts.isWhileStatement(node) || ts.isDoStatement(node)) {
|
|
43
52
|
errors.push({
|
|
@@ -103,6 +103,41 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
103
103
|
}
|
|
104
104
|
return [node];
|
|
105
105
|
}
|
|
106
|
+
// A declared step is a function node like any other — the console draws it
|
|
107
|
+
// the same way, with the phase available for the "Given/When/Then" prose.
|
|
108
|
+
case 'scenarioStep': {
|
|
109
|
+
const node = {
|
|
110
|
+
nodeId,
|
|
111
|
+
rpcName: step.stepFunc,
|
|
112
|
+
stepName: step.stepName,
|
|
113
|
+
scenarioStepPhase: step.phase,
|
|
114
|
+
next: nextNodeId,
|
|
115
|
+
};
|
|
116
|
+
if (step.actor) {
|
|
117
|
+
node.actor = step.actor;
|
|
118
|
+
}
|
|
119
|
+
if (step.inputs) {
|
|
120
|
+
if (step.inputs === 'passthrough') {
|
|
121
|
+
node.input = { $passthrough: { $ref: 'trigger' } };
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
node.input = {};
|
|
125
|
+
for (const [key, source] of Object.entries(step.inputs)) {
|
|
126
|
+
node.input[key] = convertInputSource(source);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (step.outputVar) {
|
|
131
|
+
node.outputVar = step.outputVar;
|
|
132
|
+
}
|
|
133
|
+
if (step.options) {
|
|
134
|
+
node.options = {
|
|
135
|
+
retries: step.options.retries,
|
|
136
|
+
retryDelay: step.options.retryDelay,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return [node];
|
|
140
|
+
}
|
|
106
141
|
case 'sleep': {
|
|
107
142
|
const node = {
|
|
108
143
|
nodeId,
|
|
@@ -384,6 +419,7 @@ export function convertDslToGraph(workflowName, meta) {
|
|
|
384
419
|
title: meta.title,
|
|
385
420
|
description: meta.description,
|
|
386
421
|
tags: meta.tags,
|
|
422
|
+
skip: meta.skip,
|
|
387
423
|
actors: meta.actors,
|
|
388
424
|
context: meta.context,
|
|
389
425
|
nodes: nodesRecord,
|
|
@@ -67,7 +67,7 @@ export interface NodeOptions {
|
|
|
67
67
|
* Flow node types for control flow (no RPC call)
|
|
68
68
|
*/
|
|
69
69
|
export type FlowType = 'sleep' | 'suspend' | 'approval' | 'branch' | 'parallel' | 'fanout' | 'inline' | 'switch' | 'filter' | 'arrayPredicate' | 'return' | 'cancel' | 'set';
|
|
70
|
-
import type { ContextVariable, WorkflowContext, WorkflowPlannedStep } from '@pikku/core/workflow';
|
|
70
|
+
import type { ContextVariable, ScenarioStepPhase, WorkflowContext, WorkflowPlannedStep } from '@pikku/core/workflow';
|
|
71
71
|
export type { ContextVariable, WorkflowContext };
|
|
72
72
|
/**
|
|
73
73
|
* Base node properties shared by all node types
|
|
@@ -102,6 +102,8 @@ export interface FunctionNode extends BaseNode {
|
|
|
102
102
|
actor?: string;
|
|
103
103
|
/** True for workflow.expectEventually polling steps (scenarios) */
|
|
104
104
|
expectEventually?: boolean;
|
|
105
|
+
/** Gherkin-style phase for a declared scenario step (scenario.given/when/then) */
|
|
106
|
+
scenarioStepPhase?: ScenarioStepPhase;
|
|
105
107
|
}
|
|
106
108
|
/**
|
|
107
109
|
* Flow node - control flow only, no RPC call
|
|
@@ -150,6 +152,8 @@ export interface SerializedWorkflowGraph {
|
|
|
150
152
|
tags?: string[];
|
|
151
153
|
/** Actor names a scenario's steps run as */
|
|
152
154
|
actors?: string[];
|
|
155
|
+
/** Why a scenario is held out of a default run, stated where it is declared */
|
|
156
|
+
skip?: string;
|
|
153
157
|
/** If true, workflow always executes inline without queues */
|
|
154
158
|
inline?: boolean;
|
|
155
159
|
/** Workflow context/state variables (from Zod schema) */
|
package/dist/visit.js
CHANGED
|
@@ -25,6 +25,7 @@ import { addCredential } from './add/add-credential.js';
|
|
|
25
25
|
import { addScope } from './add/add-scope.js';
|
|
26
26
|
import { addVariable } from './add/add-variable.js';
|
|
27
27
|
import { addWorkflowGraph } from './add/add-workflow-graph.js';
|
|
28
|
+
import { addFeature } from './add/add-feature.js';
|
|
28
29
|
import { addAIAgent } from './add/add-ai-agent.js';
|
|
29
30
|
import { addApprovalDescription } from './add/add-approval-description.js';
|
|
30
31
|
export const visitSetup = (logger, checker, node, state, options) => {
|
|
@@ -81,6 +82,7 @@ export const visitRoutes = (logger, checker, node, state, options) => {
|
|
|
81
82
|
addMCPResource(logger, node, checker, state, nextOptions);
|
|
82
83
|
addMCPPrompt(logger, node, checker, state, nextOptions);
|
|
83
84
|
addWorkflowGraph(logger, node, checker, state, nextOptions);
|
|
85
|
+
addFeature(logger, node, checker, state, nextOptions);
|
|
84
86
|
addAIAgent(logger, node, checker, state, nextOptions);
|
|
85
87
|
ts.forEachChild(node, (child) => visitRoutes(logger, checker, child, state, nextOptions));
|
|
86
88
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.48",
|
|
4
|
+
"description": "TypeScript inspector for Pikku — discovers functions and wirings to drive code generation",
|
|
4
5
|
"author": "yasser.fadl@gmail.com",
|
|
5
6
|
"license": "BUSL-1.1",
|
|
6
7
|
"type": "module",
|
|
@@ -44,10 +45,10 @@
|
|
|
44
45
|
"zod-to-ts": "^2.1.0"
|
|
45
46
|
},
|
|
46
47
|
"peerDependencies": {
|
|
47
|
-
"@pikku/core": "^0.12.
|
|
48
|
+
"@pikku/core": "^0.12.70"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
|
-
"@pikku/core": "^0.12.
|
|
51
|
+
"@pikku/core": "^0.12.70",
|
|
51
52
|
"@types/node": "^24.11.0"
|
|
52
53
|
},
|
|
53
54
|
"engines": {
|