@pikku/inspector 0.12.47 → 0.12.49
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 +218 -0
- package/LICENSE +106 -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 +38 -9
- package/dist/add/add-workflow.js +22 -29
- package/dist/add/scenario-instrumentation.d.ts +24 -0
- package/dist/add/scenario-instrumentation.js +29 -0
- package/dist/error-codes.d.ts +3 -0
- package/dist/error-codes.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +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 +92 -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 +47 -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-instrumentation.test.ts +89 -0
- package/src/add/scenario-instrumentation.ts +36 -0
- package/src/add/scenario-marker.test.ts +89 -0
- 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 +6 -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 +30 -1
- package/src/utils/post-process.ts +103 -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
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
SwitchCaseMeta,
|
|
16
16
|
FilterStepMeta,
|
|
17
17
|
ArrayPredicateStepMeta,
|
|
18
|
+
ScenarioStepMeta,
|
|
18
19
|
InputSource,
|
|
19
20
|
OutputBinding,
|
|
20
21
|
Condition,
|
|
@@ -24,6 +25,9 @@ import type {
|
|
|
24
25
|
import {
|
|
25
26
|
isWorkflowDoCall,
|
|
26
27
|
isWorkflowExpectEventuallyCall,
|
|
28
|
+
isScenarioStepCall,
|
|
29
|
+
getScenarioStepPhase,
|
|
30
|
+
DYNAMIC_SCENARIO_STEP_TARGET,
|
|
27
31
|
extractActorFromOptions,
|
|
28
32
|
isWorkflowSleepCall,
|
|
29
33
|
isWorkflowSuspendCall,
|
|
@@ -60,7 +64,7 @@ interface ExtractionContext {
|
|
|
60
64
|
outputVars: Map<string, { type: ts.Type; node: ts.Node }>
|
|
61
65
|
arrayVars: Set<string>
|
|
62
66
|
conditionalVars: Set<string>
|
|
63
|
-
inputParamName: string
|
|
67
|
+
inputParamName: string | null
|
|
64
68
|
errors: ValidationError[]
|
|
65
69
|
/** Loop variables in scope (for fanout item variables) */
|
|
66
70
|
loopVars: Set<string>
|
|
@@ -101,7 +105,7 @@ export function extractDSLWorkflow(
|
|
|
101
105
|
|
|
102
106
|
// Extract input parameter name (second parameter)
|
|
103
107
|
const inputParamName = extractInputParamName(arrowFunc)
|
|
104
|
-
if (
|
|
108
|
+
if (inputParamName === undefined) {
|
|
105
109
|
return {
|
|
106
110
|
status: 'error',
|
|
107
111
|
reason: 'Could not determine input parameter name',
|
|
@@ -220,9 +224,17 @@ function findWorkflowFunction(node: ts.Node): ts.ArrowFunction | null {
|
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
/**
|
|
223
|
-
* Extract the input parameter name from the arrow function
|
|
227
|
+
* Extract the input parameter name from the arrow function.
|
|
228
|
+
*
|
|
229
|
+
* Returns `null` when no input parameter is declared at all. That is
|
|
230
|
+
* legitimate — a workflow may ignore its input — and nothing in the body can
|
|
231
|
+
* reference it, so every name comparison against `null` correctly fails.
|
|
232
|
+
* Returns `undefined` when an input parameter is declared but is not a plain
|
|
233
|
+
* identifier (destructured), which the extractor cannot track by name.
|
|
224
234
|
*/
|
|
225
|
-
function extractInputParamName(
|
|
235
|
+
function extractInputParamName(
|
|
236
|
+
arrowFunc: ts.ArrowFunction
|
|
237
|
+
): string | null | undefined {
|
|
226
238
|
if (arrowFunc.parameters.length < 2) {
|
|
227
239
|
return null
|
|
228
240
|
}
|
|
@@ -232,7 +244,7 @@ function extractInputParamName(arrowFunc: ts.ArrowFunction): string | null {
|
|
|
232
244
|
return secondParam.name.text
|
|
233
245
|
}
|
|
234
246
|
|
|
235
|
-
return
|
|
247
|
+
return undefined
|
|
236
248
|
}
|
|
237
249
|
|
|
238
250
|
/**
|
|
@@ -298,7 +310,34 @@ function extractStep(
|
|
|
298
310
|
|
|
299
311
|
// For-of statement (sequential fanout)
|
|
300
312
|
if (ts.isForOfStatement(statement)) {
|
|
301
|
-
|
|
313
|
+
const reason: { message?: string } = {}
|
|
314
|
+
const fanout = extractSequentialFanout(statement, context, reason)
|
|
315
|
+
if (!fanout && containsWorkflowCall(statement.statement, context.checker)) {
|
|
316
|
+
// A for-of the DSL can't model as a sequential fanout (its iterable is not
|
|
317
|
+
// a data-array identifier/field — e.g. a counting loop
|
|
318
|
+
// `for (const i of [...Array(N).keys()])`). Silently returning null here
|
|
319
|
+
// used to drop the loop AND every `workflow.do` inside it, so the workflow
|
|
320
|
+
// serialized with zero steps and its step functions were never registered
|
|
321
|
+
// (→ runtime "Function not found"). Fail loudly instead: pikkuWorkflowFunc
|
|
322
|
+
// reports INVALID_DSL_WORKFLOW; pikkuWorkflowComplexFunc falls back to the
|
|
323
|
+
// basic AST walk (which DOES register loop-invoked functions), so a genuine
|
|
324
|
+
// control-flow loop belongs in a complex workflow.
|
|
325
|
+
//
|
|
326
|
+
// Guarded on the body actually containing a workflow call: a loop that only
|
|
327
|
+
// massages locals (building a lookup, summing a total) has no step to lose,
|
|
328
|
+
// and erroring on it would be a false positive — the whole hazard is a
|
|
329
|
+
// dropped `workflow.do`, not a dropped `for`.
|
|
330
|
+
context.errors.push({
|
|
331
|
+
message:
|
|
332
|
+
`The for-of loop '${statement.getText().slice(0, 60)}' can't be expressed in a DSL workflow — ` +
|
|
333
|
+
(reason.message ??
|
|
334
|
+
`its iterable must be a data array (an identifier or field like 'data.items'), not a computed/inline ` +
|
|
335
|
+
`iterable such as '[...Array(n).keys()]'. Use pikkuWorkflowComplexFunc for a control-flow/counting loop, ` +
|
|
336
|
+
`or iterate over a real input array.`),
|
|
337
|
+
node: statement,
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
return fanout
|
|
302
341
|
}
|
|
303
342
|
|
|
304
343
|
// Return statement
|
|
@@ -363,10 +402,12 @@ function extractVariableDeclaration(
|
|
|
363
402
|
// Check for await workflow.do(...)
|
|
364
403
|
if (ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)) {
|
|
365
404
|
const call = init.expression
|
|
366
|
-
if (isWorkflowDoCall(call, context.checker)) {
|
|
367
|
-
const step =
|
|
368
|
-
?
|
|
369
|
-
:
|
|
405
|
+
if (isWorkflowDoCall(call, context.checker) || isScenarioStepCall(call)) {
|
|
406
|
+
const step = isScenarioStepCall(call)
|
|
407
|
+
? extractScenarioStep(call, context, varName)
|
|
408
|
+
: isInlineDoCall(call)
|
|
409
|
+
? extractInlineStep(call, context)
|
|
410
|
+
: extractRpcStep(call, context, varName)
|
|
370
411
|
if (step) {
|
|
371
412
|
// Track output variable
|
|
372
413
|
const type = context.checker.getTypeAtLocation(decl)
|
|
@@ -515,6 +556,7 @@ function extractDestructuredDeclaration(
|
|
|
515
556
|
const destructuresAStep =
|
|
516
557
|
stepCall !== null &&
|
|
517
558
|
(isWorkflowDoCall(stepCall, context.checker) ||
|
|
559
|
+
isScenarioStepCall(stepCall) ||
|
|
518
560
|
isParallelGroup(stepCall) ||
|
|
519
561
|
isParallelFanout(stepCall))
|
|
520
562
|
if (!destructuresAStep) return null
|
|
@@ -584,10 +626,12 @@ function extractExpressionStatement(
|
|
|
584
626
|
if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
|
|
585
627
|
const call = expr.expression
|
|
586
628
|
|
|
587
|
-
if (isWorkflowDoCall(call, context.checker)) {
|
|
588
|
-
const step =
|
|
589
|
-
?
|
|
590
|
-
:
|
|
629
|
+
if (isWorkflowDoCall(call, context.checker) || isScenarioStepCall(call)) {
|
|
630
|
+
const step = isScenarioStepCall(call)
|
|
631
|
+
? extractScenarioStep(call, context, outputVar)
|
|
632
|
+
: isInlineDoCall(call)
|
|
633
|
+
? extractInlineStep(call, context)
|
|
634
|
+
: extractRpcStep(call, context, outputVar)
|
|
591
635
|
|
|
592
636
|
// Track output variable if this is an assignment
|
|
593
637
|
if (outputVar && step) {
|
|
@@ -692,6 +736,68 @@ function extractRpcStep(
|
|
|
692
736
|
}
|
|
693
737
|
}
|
|
694
738
|
|
|
739
|
+
/**
|
|
740
|
+
* Extract a scenario step from scenario.step/given/when/then(stepName,
|
|
741
|
+
* stepFunc, data?, options?).
|
|
742
|
+
*
|
|
743
|
+
* The step target must be a static string literal so it can be bundled, typed
|
|
744
|
+
* and drawn. When it isn't, the step is still recorded — with a `<dynamic>`
|
|
745
|
+
* target — so post-processing can raise PKU678 rather than the call silently
|
|
746
|
+
* vanishing from the graph.
|
|
747
|
+
*/
|
|
748
|
+
function extractScenarioStep(
|
|
749
|
+
call: ts.CallExpression,
|
|
750
|
+
context: ExtractionContext,
|
|
751
|
+
outputVar?: string
|
|
752
|
+
): ScenarioStepMeta | null {
|
|
753
|
+
const phase = getScenarioStepPhase(call)
|
|
754
|
+
if (!phase) {
|
|
755
|
+
return null
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const args = call.arguments
|
|
759
|
+
if (args.length < 2) {
|
|
760
|
+
return null
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
try {
|
|
764
|
+
const stepName = extractStringLiteral(args[0], context.checker)
|
|
765
|
+
|
|
766
|
+
let stepFunc = DYNAMIC_SCENARIO_STEP_TARGET
|
|
767
|
+
try {
|
|
768
|
+
stepFunc = extractStringLiteral(args[1], context.checker)
|
|
769
|
+
} catch {
|
|
770
|
+
// Left as '<dynamic>' — validated in post-processing (PKU678).
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const inputs =
|
|
774
|
+
args.length >= 3 ? extractInputSources(args[2], context) : undefined
|
|
775
|
+
|
|
776
|
+
const optionsArg = args.length >= 4 ? args[3] : undefined
|
|
777
|
+
const options =
|
|
778
|
+
optionsArg && ts.isObjectLiteralExpression(optionsArg)
|
|
779
|
+
? extractStepOptions(optionsArg, context)
|
|
780
|
+
: undefined
|
|
781
|
+
|
|
782
|
+
return {
|
|
783
|
+
type: 'scenarioStep',
|
|
784
|
+
stepName,
|
|
785
|
+
stepFunc,
|
|
786
|
+
phase,
|
|
787
|
+
outputVar,
|
|
788
|
+
inputs,
|
|
789
|
+
options,
|
|
790
|
+
actor: extractActorFromOptions(optionsArg),
|
|
791
|
+
}
|
|
792
|
+
} catch (error) {
|
|
793
|
+
context.errors.push({
|
|
794
|
+
message: `Failed to extract scenario step: ${error instanceof Error ? error.message : String(error)}`,
|
|
795
|
+
node: call,
|
|
796
|
+
})
|
|
797
|
+
return null
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
695
801
|
/**
|
|
696
802
|
* Extract inline step from workflow.do() call with a function argument
|
|
697
803
|
*/
|
|
@@ -1225,7 +1331,7 @@ function extractArrayPredicate(
|
|
|
1225
1331
|
function extractFanoutBodyStep(
|
|
1226
1332
|
stmt: ts.Statement,
|
|
1227
1333
|
childContext: ExtractionContext,
|
|
1228
|
-
body: Array<RpcStepMeta | SleepStepMeta | SuspendStepMeta>
|
|
1334
|
+
body: Array<RpcStepMeta | SleepStepMeta | SuspendStepMeta | ScenarioStepMeta>
|
|
1229
1335
|
): void {
|
|
1230
1336
|
if (ts.isVariableStatement(stmt)) {
|
|
1231
1337
|
const declList = stmt.declarationList
|
|
@@ -1242,11 +1348,16 @@ function extractFanoutBodyStep(
|
|
|
1242
1348
|
return
|
|
1243
1349
|
}
|
|
1244
1350
|
const call = init.expression
|
|
1245
|
-
if (
|
|
1351
|
+
if (
|
|
1352
|
+
!isWorkflowDoCall(call, childContext.checker) &&
|
|
1353
|
+
!isScenarioStepCall(call)
|
|
1354
|
+
) {
|
|
1246
1355
|
return
|
|
1247
1356
|
}
|
|
1248
1357
|
const varName = ts.isIdentifier(decl.name) ? decl.name.text : undefined
|
|
1249
|
-
const step =
|
|
1358
|
+
const step = isScenarioStepCall(call)
|
|
1359
|
+
? extractScenarioStep(call, childContext, varName)
|
|
1360
|
+
: extractRpcStep(call, childContext, varName)
|
|
1250
1361
|
if (!step) {
|
|
1251
1362
|
return
|
|
1252
1363
|
}
|
|
@@ -1290,6 +1401,13 @@ function extractFanoutBodyStep(
|
|
|
1290
1401
|
}
|
|
1291
1402
|
return
|
|
1292
1403
|
}
|
|
1404
|
+
if (isScenarioStepCall(call)) {
|
|
1405
|
+
const scenarioStep = extractScenarioStep(call, childContext)
|
|
1406
|
+
if (scenarioStep) {
|
|
1407
|
+
body.push(scenarioStep)
|
|
1408
|
+
}
|
|
1409
|
+
return
|
|
1410
|
+
}
|
|
1293
1411
|
if (!isWorkflowDoCall(call, childContext.checker)) {
|
|
1294
1412
|
return
|
|
1295
1413
|
}
|
|
@@ -1363,7 +1481,12 @@ function extractParallelFanout(
|
|
|
1363
1481
|
? expr
|
|
1364
1482
|
: null
|
|
1365
1483
|
|
|
1366
|
-
if (doCall &&
|
|
1484
|
+
if (doCall && isScenarioStepCall(doCall)) {
|
|
1485
|
+
const step = extractScenarioStep(doCall, childContext)
|
|
1486
|
+
if (step) {
|
|
1487
|
+
body.push(step)
|
|
1488
|
+
}
|
|
1489
|
+
} else if (doCall && isWorkflowDoCall(doCall, context.checker)) {
|
|
1367
1490
|
const step = extractRpcStep(doCall, childContext)
|
|
1368
1491
|
if (step) {
|
|
1369
1492
|
body.push(step)
|
|
@@ -1396,10 +1519,18 @@ function extractParallelGroup(
|
|
|
1396
1519
|
return null
|
|
1397
1520
|
}
|
|
1398
1521
|
|
|
1399
|
-
const children: RpcStepMeta
|
|
1522
|
+
const children: Array<RpcStepMeta | ScenarioStepMeta> = []
|
|
1400
1523
|
|
|
1401
1524
|
for (const elem of arrayArg.elements) {
|
|
1402
|
-
if (ts.isCallExpression(elem)
|
|
1525
|
+
if (!ts.isCallExpression(elem)) {
|
|
1526
|
+
continue
|
|
1527
|
+
}
|
|
1528
|
+
if (isScenarioStepCall(elem)) {
|
|
1529
|
+
const step = extractScenarioStep(elem, context)
|
|
1530
|
+
if (step) {
|
|
1531
|
+
children.push(step)
|
|
1532
|
+
}
|
|
1533
|
+
} else if (isWorkflowDoCall(elem, context.checker)) {
|
|
1403
1534
|
const step = extractRpcStep(elem, context)
|
|
1404
1535
|
if (step) {
|
|
1405
1536
|
children.push(step)
|
|
@@ -1420,9 +1551,40 @@ function extractParallelGroup(
|
|
|
1420
1551
|
/**
|
|
1421
1552
|
* Extract sequential fanout from for-of loop
|
|
1422
1553
|
*/
|
|
1554
|
+
/**
|
|
1555
|
+
* Does this subtree call the workflow wire at all?
|
|
1556
|
+
*
|
|
1557
|
+
* Used to tell "the DSL can't model this loop, and a step would be silently
|
|
1558
|
+
* lost" from "the DSL can't model this loop, and there is nothing in it to
|
|
1559
|
+
* lose". Only the first is worth failing a build over.
|
|
1560
|
+
*/
|
|
1561
|
+
function containsWorkflowCall(node: ts.Node, checker: ts.TypeChecker): boolean {
|
|
1562
|
+
let found = false
|
|
1563
|
+
const visit = (child: ts.Node): void => {
|
|
1564
|
+
if (found) return
|
|
1565
|
+
if (ts.isCallExpression(child)) {
|
|
1566
|
+
if (
|
|
1567
|
+
isWorkflowDoCall(child, checker) ||
|
|
1568
|
+
isScenarioStepCall(child) ||
|
|
1569
|
+
isWorkflowSleepCall(child, checker) ||
|
|
1570
|
+
isWorkflowSuspendCall(child, checker) ||
|
|
1571
|
+
isWorkflowApprovalCall(child, checker) ||
|
|
1572
|
+
isWorkflowExpectEventuallyCall(child)
|
|
1573
|
+
) {
|
|
1574
|
+
found = true
|
|
1575
|
+
return
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
ts.forEachChild(child, visit)
|
|
1579
|
+
}
|
|
1580
|
+
visit(node)
|
|
1581
|
+
return found
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1423
1584
|
function extractSequentialFanout(
|
|
1424
1585
|
statement: ts.ForOfStatement,
|
|
1425
|
-
context: ExtractionContext
|
|
1586
|
+
context: ExtractionContext,
|
|
1587
|
+
reason?: { message?: string }
|
|
1426
1588
|
): FanoutStepMeta | null {
|
|
1427
1589
|
if (!isSequentialFanout(statement)) {
|
|
1428
1590
|
return null
|
|
@@ -1521,6 +1683,18 @@ function extractSequentialFanout(
|
|
|
1521
1683
|
}
|
|
1522
1684
|
|
|
1523
1685
|
if (body.length === 0) {
|
|
1686
|
+
// The iterable was fine — the body yielded nothing the DSL can model. The
|
|
1687
|
+
// usual cause is a `workflow.do` nested inside an `if`/`switch`, because
|
|
1688
|
+
// `FanoutStepMeta['body']` is a flat list of steps with no branch member:
|
|
1689
|
+
// the DSL has no way to say "conditionally run this step per item".
|
|
1690
|
+
const branching = bodyStatements.some(
|
|
1691
|
+
(stmt) => ts.isIfStatement(stmt) || ts.isSwitchStatement(stmt)
|
|
1692
|
+
)
|
|
1693
|
+
if (reason) {
|
|
1694
|
+
reason.message = branching
|
|
1695
|
+
? `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.`
|
|
1696
|
+
: `its body contains no 'workflow.do' call the DSL can model. Use pikkuWorkflowComplexFunc for a loop that does other work.`
|
|
1697
|
+
}
|
|
1524
1698
|
return null
|
|
1525
1699
|
}
|
|
1526
1700
|
|
|
@@ -1741,7 +1915,11 @@ function extractInputSources(
|
|
|
1741
1915
|
} else if (context.outputVars.has(varName)) {
|
|
1742
1916
|
source = { from: 'outputVar', name: varName }
|
|
1743
1917
|
} else {
|
|
1744
|
-
|
|
1918
|
+
const constant = resolveConstantValue(prop.name, context.checker)
|
|
1919
|
+
source =
|
|
1920
|
+
constant !== undefined
|
|
1921
|
+
? { from: 'literal', value: constant }
|
|
1922
|
+
: { from: 'input', path: varName }
|
|
1745
1923
|
}
|
|
1746
1924
|
} else if (ts.isPropertyAssignment(prop)) {
|
|
1747
1925
|
source = extractInputSource(prop.initializer, context)
|
|
@@ -1831,6 +2009,11 @@ function extractInputSource(
|
|
|
1831
2009
|
return { from: 'outputVar', name: varName }
|
|
1832
2010
|
}
|
|
1833
2011
|
|
|
2012
|
+
const constant = resolveConstantValue(node, context.checker)
|
|
2013
|
+
if (constant !== undefined) {
|
|
2014
|
+
return { from: 'literal', value: constant }
|
|
2015
|
+
}
|
|
2016
|
+
|
|
1834
2017
|
// Assume it's from input
|
|
1835
2018
|
return { from: 'input', path: varName }
|
|
1836
2019
|
}
|
|
@@ -1916,6 +2099,37 @@ function extractInputSource(
|
|
|
1916
2099
|
return null
|
|
1917
2100
|
}
|
|
1918
2101
|
|
|
2102
|
+
/**
|
|
2103
|
+
* Resolve an identifier that names a constant to the value it was declared with.
|
|
2104
|
+
*
|
|
2105
|
+
* Without this, a name the extractor does not recognise falls through to
|
|
2106
|
+
* `{ from: 'input' }` and is serialized as `{ $ref: 'trigger', path: <name> }` —
|
|
2107
|
+
* so a module-level `const RESOURCE_ID = 'x'` used as step input silently
|
|
2108
|
+
* becomes a read of a trigger field that does not exist. Only `const`
|
|
2109
|
+
* declarations with a fully literal initializer resolve; anything else (a name
|
|
2110
|
+
* bound out of the trigger, a computed value) falls through unchanged.
|
|
2111
|
+
*/
|
|
2112
|
+
function resolveConstantValue(
|
|
2113
|
+
node: ts.Identifier,
|
|
2114
|
+
checker: ts.TypeChecker
|
|
2115
|
+
): unknown | undefined {
|
|
2116
|
+
const symbol = checker.getSymbolAtLocation(node)
|
|
2117
|
+
const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0]
|
|
2118
|
+
if (!declaration || !ts.isVariableDeclaration(declaration)) {
|
|
2119
|
+
return undefined
|
|
2120
|
+
}
|
|
2121
|
+
const declarationList = declaration.parent
|
|
2122
|
+
if (
|
|
2123
|
+
!ts.isVariableDeclarationList(declarationList) ||
|
|
2124
|
+
!(declarationList.flags & ts.NodeFlags.Const)
|
|
2125
|
+
) {
|
|
2126
|
+
return undefined
|
|
2127
|
+
}
|
|
2128
|
+
return declaration.initializer
|
|
2129
|
+
? extractLiteralValue(declaration.initializer)
|
|
2130
|
+
: undefined
|
|
2131
|
+
}
|
|
2132
|
+
|
|
1919
2133
|
/**
|
|
1920
2134
|
* Extract a literal value from an expression
|
|
1921
2135
|
*/
|
|
@@ -36,6 +36,44 @@ export function isWorkflowDoCall(
|
|
|
36
36
|
)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Placeholder recorded when a scenario step's target isn't a static string
|
|
41
|
+
* literal. The step is kept rather than dropped so post-processing can raise
|
|
42
|
+
* PKU678 against it — a dropped step is the silent-failure mode this repo has
|
|
43
|
+
* hit repeatedly on workflow graph round-trips.
|
|
44
|
+
*/
|
|
45
|
+
export const DYNAMIC_SCENARIO_STEP_TARGET = '<dynamic>'
|
|
46
|
+
|
|
47
|
+
const SCENARIO_STEP_PHASES = ['step', 'given', 'when', 'then'] as const
|
|
48
|
+
|
|
49
|
+
export type ScenarioStepPhaseName = (typeof SCENARIO_STEP_PHASES)[number]
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if a call expression is scenario.step()/given()/when()/then() — a
|
|
53
|
+
* declared `pikkuScenarioStep` invoked by name. `given`/`when`/`then` are pure
|
|
54
|
+
* sugar over `step`: the phase only changes the prose a reporter renders.
|
|
55
|
+
*/
|
|
56
|
+
export function isScenarioStepCall(node: ts.CallExpression): boolean {
|
|
57
|
+
return getScenarioStepPhase(node) !== undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The Gherkin-style phase of a scenario step call, or undefined when the call
|
|
62
|
+
* isn't one.
|
|
63
|
+
*/
|
|
64
|
+
export function getScenarioStepPhase(
|
|
65
|
+
node: ts.CallExpression
|
|
66
|
+
): ScenarioStepPhaseName | undefined {
|
|
67
|
+
if (!ts.isPropertyAccessExpression(node.expression)) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
const propAccess = node.expression
|
|
71
|
+
if (!isWorkflowWireIdentifier(propAccess.expression)) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
return SCENARIO_STEP_PHASES.find((phase) => phase === propAccess.name.text)
|
|
75
|
+
}
|
|
76
|
+
|
|
39
77
|
/**
|
|
40
78
|
* Check if a call expression is workflow.expectEventually()
|
|
41
79
|
*/
|
|
@@ -22,6 +22,11 @@ export interface ValidationError {
|
|
|
22
22
|
* - ReturnStatement
|
|
23
23
|
* - ThrowStatement (for WorkflowCancelledException)
|
|
24
24
|
* - Block (containers)
|
|
25
|
+
*
|
|
26
|
+
* When `allowInline` is set (complex workflows and scenarios), the bodies of
|
|
27
|
+
* closures are skipped: a callback is ordinary TypeScript, not DSL, so holding
|
|
28
|
+
* it to the statement whitelist would reject valid code. Plain DSL workflows
|
|
29
|
+
* still descend into callbacks, which is what validates fanout bodies.
|
|
25
30
|
*/
|
|
26
31
|
export function validateNoDisallowedPatterns(
|
|
27
32
|
node: ts.Node,
|
|
@@ -54,6 +59,13 @@ export function validateNoDisallowedPatterns(
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
function visitNode(node: ts.Node) {
|
|
62
|
+
if (
|
|
63
|
+
options?.allowInline &&
|
|
64
|
+
(ts.isArrowFunction(node) || ts.isFunctionExpression(node))
|
|
65
|
+
) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
// Disallow while and do-while
|
|
58
70
|
if (ts.isWhileStatement(node) || ts.isDoStatement(node)) {
|
|
59
71
|
errors.push({
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { convertDslToGraph } from './convert-dsl-to-graph.js'
|
|
4
|
+
import type { WorkflowsMeta } from '@pikku/core'
|
|
5
|
+
|
|
6
|
+
const scenarioMeta = (
|
|
7
|
+
overrides: Partial<WorkflowsMeta[string]> = {}
|
|
8
|
+
): WorkflowsMeta[string] => ({
|
|
9
|
+
pikkuFuncId: 'installAddonFreshNameScenario',
|
|
10
|
+
name: 'installAddonFreshNameScenario',
|
|
11
|
+
scenario: true,
|
|
12
|
+
steps: [],
|
|
13
|
+
...overrides,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('convertDslToGraph: a scenario carries its skip reason into the graph', () => {
|
|
17
|
+
const graph = convertDslToGraph(
|
|
18
|
+
'installAddonFreshNameScenario',
|
|
19
|
+
scenarioMeta({ skip: 'npm cannot install inside this yarn workspace' })
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
assert.strictEqual(
|
|
23
|
+
graph.skip,
|
|
24
|
+
'npm cannot install inside this yarn workspace'
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('convertDslToGraph: a scenario with no skip leaves it unset', () => {
|
|
29
|
+
const graph = convertDslToGraph(
|
|
30
|
+
'installAddonFreshNameScenario',
|
|
31
|
+
scenarioMeta()
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
assert.strictEqual(graph.skip, undefined)
|
|
35
|
+
})
|
|
@@ -138,6 +138,41 @@ function convertStepToNode(
|
|
|
138
138
|
return [node]
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// A declared step is a function node like any other — the console draws it
|
|
142
|
+
// the same way, with the phase available for the "Given/When/Then" prose.
|
|
143
|
+
case 'scenarioStep': {
|
|
144
|
+
const node: FunctionNode = {
|
|
145
|
+
nodeId,
|
|
146
|
+
rpcName: step.stepFunc,
|
|
147
|
+
stepName: step.stepName,
|
|
148
|
+
scenarioStepPhase: step.phase,
|
|
149
|
+
next: nextNodeId,
|
|
150
|
+
}
|
|
151
|
+
if (step.actor) {
|
|
152
|
+
node.actor = step.actor
|
|
153
|
+
}
|
|
154
|
+
if (step.inputs) {
|
|
155
|
+
if (step.inputs === 'passthrough') {
|
|
156
|
+
node.input = { $passthrough: { $ref: 'trigger' } }
|
|
157
|
+
} else {
|
|
158
|
+
node.input = {}
|
|
159
|
+
for (const [key, source] of Object.entries(step.inputs)) {
|
|
160
|
+
node.input[key] = convertInputSource(source as any)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (step.outputVar) {
|
|
165
|
+
node.outputVar = step.outputVar
|
|
166
|
+
}
|
|
167
|
+
if (step.options) {
|
|
168
|
+
node.options = {
|
|
169
|
+
retries: step.options.retries,
|
|
170
|
+
retryDelay: step.options.retryDelay,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return [node]
|
|
174
|
+
}
|
|
175
|
+
|
|
141
176
|
case 'sleep': {
|
|
142
177
|
const node: FlowNode = {
|
|
143
178
|
nodeId,
|
|
@@ -484,6 +519,7 @@ export function convertDslToGraph(
|
|
|
484
519
|
title: meta.title,
|
|
485
520
|
description: meta.description,
|
|
486
521
|
tags: meta.tags,
|
|
522
|
+
skip: meta.skip,
|
|
487
523
|
actors: meta.actors,
|
|
488
524
|
context: meta.context,
|
|
489
525
|
nodes: nodesRecord,
|
|
@@ -113,6 +113,7 @@ export type FlowType =
|
|
|
113
113
|
// Import and re-export context types from core
|
|
114
114
|
import type {
|
|
115
115
|
ContextVariable,
|
|
116
|
+
ScenarioStepPhase,
|
|
116
117
|
WorkflowContext,
|
|
117
118
|
WorkflowPlannedStep,
|
|
118
119
|
} from '@pikku/core/workflow'
|
|
@@ -153,6 +154,8 @@ export interface FunctionNode extends BaseNode {
|
|
|
153
154
|
actor?: string
|
|
154
155
|
/** True for workflow.expectEventually polling steps (scenarios) */
|
|
155
156
|
expectEventually?: boolean
|
|
157
|
+
/** Gherkin-style phase for a declared scenario step (scenario.given/when/then) */
|
|
158
|
+
scenarioStepPhase?: ScenarioStepPhase
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
/**
|
|
@@ -210,6 +213,8 @@ export interface SerializedWorkflowGraph {
|
|
|
210
213
|
tags?: string[]
|
|
211
214
|
/** Actor names a scenario's steps run as */
|
|
212
215
|
actors?: string[]
|
|
216
|
+
/** Why a scenario is held out of a default run, stated where it is declared */
|
|
217
|
+
skip?: string
|
|
213
218
|
/** If true, workflow always executes inline without queues */
|
|
214
219
|
inline?: boolean
|
|
215
220
|
/** Workflow context/state variables (from Zod schema) */
|
package/src/visit.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { addCredential } from './add/add-credential.js'
|
|
|
30
30
|
import { addScope } from './add/add-scope.js'
|
|
31
31
|
import { addVariable } from './add/add-variable.js'
|
|
32
32
|
import { addWorkflowGraph } from './add/add-workflow-graph.js'
|
|
33
|
+
import { addFeature } from './add/add-feature.js'
|
|
33
34
|
import { addAIAgent } from './add/add-ai-agent.js'
|
|
34
35
|
import { addApprovalDescription } from './add/add-approval-description.js'
|
|
35
36
|
|
|
@@ -165,6 +166,7 @@ export const visitRoutes = (
|
|
|
165
166
|
addMCPResource(logger, node, checker, state, nextOptions)
|
|
166
167
|
addMCPPrompt(logger, node, checker, state, nextOptions)
|
|
167
168
|
addWorkflowGraph(logger, node, checker, state, nextOptions)
|
|
169
|
+
addFeature(logger, node, checker, state, nextOptions)
|
|
168
170
|
addAIAgent(logger, node, checker, state, nextOptions)
|
|
169
171
|
|
|
170
172
|
ts.forEachChild(node, (child) =>
|