@pikku/inspector 0.12.31 → 0.12.34
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 +52 -0
- package/dist/add/add-functions.js +4 -2
- package/dist/add/add-workflow.d.ts +2 -2
- package/dist/add/add-workflow.js +63 -9
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +1 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +9 -4
- package/dist/utils/workflow/dsl/patterns.d.ts +12 -1
- package/dist/utils/workflow/dsl/patterns.js +37 -2
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +15 -2
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +10 -1
- package/package.json +2 -2
- package/src/add/add-functions.ts +6 -2
- package/src/add/add-workflow.ts +76 -10
- package/src/error-codes.ts +1 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +12 -3
- package/src/utils/workflow/dsl/patterns.ts +48 -2
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +15 -2
- package/src/utils/workflow/graph/workflow-graph.types.ts +10 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
+
## 0.12.34
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 92bd643: User flows in the console: workflow graph extraction now captures
|
|
6
|
+
`workflow.expectEventually` steps and per-step actor names (`{ actor:
|
|
7
|
+
actors.x }`), workflow meta carries `actors`/`title` into the serialized
|
|
8
|
+
graph, the CLI emits `user-flow-actors.gen.json` for the new
|
|
9
|
+
`MetaService.getUserFlowActorsMeta()`, and the console Workflows page gains a
|
|
10
|
+
Workflows / User Flows / Personas toggle. Also fixes complex-workflow graphs
|
|
11
|
+
being clobbered by a duplicate basic-extraction pass after successful DSL
|
|
12
|
+
extraction.
|
|
13
|
+
- Updated dependencies [35a9bab]
|
|
14
|
+
- Updated dependencies [92bd643]
|
|
15
|
+
- @pikku/core@0.12.50
|
|
16
|
+
|
|
17
|
+
## 0.12.33
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 4c17f7e: user flows: actors move onto the workflow wire + `pikku userflow` command
|
|
22
|
+
- Actors are no longer a singleton service: `startWorkflow(..., { actors })`
|
|
23
|
+
registers them per run and they arrive on the wire —
|
|
24
|
+
`func: async ({ logger }, input, { workflow, actors })`.
|
|
25
|
+
- Inspector enforces user flows are pure remote stories (PKU673): a
|
|
26
|
+
pikkuUserFlow func may only destructure `logger`/`config` from services.
|
|
27
|
+
- New `pikku userflow run <environment> [--flows a,b] [--tags x,y]` runs flows
|
|
28
|
+
against `userFlows.environments` from pikku.config.json (secret from
|
|
29
|
+
USER_FLOW_ACTOR_SECRET env), refusing internal (non-actor) steps so runs
|
|
30
|
+
against staging/production never touch local services; non-zero exit on
|
|
31
|
+
failure. `pikku userflow list` prints names, descriptions and tags.
|
|
32
|
+
- Workflow meta now carries `title` (parity with HTTP routes/functions).
|
|
33
|
+
|
|
34
|
+
- Updated dependencies [4c17f7e]
|
|
35
|
+
- @pikku/core@0.12.49
|
|
36
|
+
|
|
37
|
+
## 0.12.32
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- 8dfddc3: pikkuUserFlow: user flows as workflows. A complex workflow whose steps can run
|
|
42
|
+
as actors over the real transport — `workflow.do(step, rpc, data, { actor:
|
|
43
|
+
actors.yasser })` — plus `workflow.expectEventually(...)` for polling async
|
|
44
|
+
effects. Actor steps never queue and never dispatch internally, so auth
|
|
45
|
+
middleware/permissions are exercised end-to-end; flows double as e2e tests and
|
|
46
|
+
staged/production health checks. Ships UserFlowActor types +
|
|
47
|
+
createHttpUserFlowActors (lazy sign-in via `/auth/sign-in/actor` with a
|
|
48
|
+
server-held secret), inspector source `'user-flow'`, and a console badge.
|
|
49
|
+
- Updated dependencies [5f2c566]
|
|
50
|
+
- Updated dependencies [8dfddc3]
|
|
51
|
+
- @pikku/core@0.12.48
|
|
52
|
+
|
|
1
53
|
## 0.12.31
|
|
2
54
|
|
|
3
55
|
### Patch Changes
|
|
@@ -250,9 +250,11 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
250
250
|
if (!ts.isIdentifier(expression)) {
|
|
251
251
|
return;
|
|
252
252
|
}
|
|
253
|
-
// Match identifiers that contain both "pikku" and "func" (case insensitive)
|
|
253
|
+
// Match identifiers that contain both "pikku" and "func" (case insensitive),
|
|
254
|
+
// plus pikkuUserFlow (a workflow wrapper without "func" in its name)
|
|
254
255
|
const pikkuFuncPattern = /pikku.*func/i;
|
|
255
|
-
if (!pikkuFuncPattern.test(expression.text)
|
|
256
|
+
if (!pikkuFuncPattern.test(expression.text) &&
|
|
257
|
+
expression.text !== 'pikkuUserFlow') {
|
|
256
258
|
return;
|
|
257
259
|
}
|
|
258
260
|
// only handle calls like pikkuFunc(...)
|
|
@@ -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 pikkuWorkflowFunc()
|
|
9
|
-
* Detects workflow registration and extracts metadata
|
|
8
|
+
* Inspector for pikkuWorkflowFunc(), pikkuWorkflowComplexFunc() and
|
|
9
|
+
* pikkuUserFlow() calls. Detects workflow registration and extracts metadata.
|
|
10
10
|
*/
|
|
11
11
|
export declare const addWorkflow: AddWiring;
|
package/dist/add/add-workflow.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ErrorCode } from '../error-codes.js';
|
|
|
5
5
|
import { extractStringLiteral, isStringLike, isFunctionLike, extractDescription, extractDuration, } from '../utils/extract-node-value.js';
|
|
6
6
|
import { getCommonWireMetaData, getPropertyValue, } from '../utils/get-property-value.js';
|
|
7
7
|
import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js';
|
|
8
|
-
import { getSourceText } from '../utils/workflow/dsl/patterns.js';
|
|
8
|
+
import { getSourceText, extractActorFromOptions, } from '../utils/workflow/dsl/patterns.js';
|
|
9
9
|
/**
|
|
10
10
|
* Extract a workflow step's display name without letting a non-static name
|
|
11
11
|
* (e.g. a function call) abort the scan. The step name is cosmetic, so a
|
|
@@ -86,14 +86,18 @@ export function collectInvokedRPCs(steps, rpcs) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
* Scan for workflow.do(), workflow.
|
|
89
|
+
* Scan for workflow.do(), workflow.expectEventually(), workflow.sleep(), and
|
|
90
|
+
* workflow.cancel() calls to extract workflow steps
|
|
90
91
|
*/
|
|
91
92
|
function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
92
93
|
// Look for property access expressions: workflow.do or workflow.sleep
|
|
93
94
|
if (ts.isPropertyAccessExpression(node)) {
|
|
94
95
|
const { name } = node;
|
|
95
96
|
// Check if this is accessing 'do' or 'sleep' property
|
|
96
|
-
if (name.text === 'do' ||
|
|
97
|
+
if (name.text === 'do' ||
|
|
98
|
+
name.text === 'sleep' ||
|
|
99
|
+
name.text === 'cancel' ||
|
|
100
|
+
name.text === 'expectEventually') {
|
|
97
101
|
// Check if the parent is a call expression
|
|
98
102
|
const parent = node.parent;
|
|
99
103
|
if (ts.isCallExpression(parent) && parent.expression === node) {
|
|
@@ -113,6 +117,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
113
117
|
type: 'rpc',
|
|
114
118
|
stepName,
|
|
115
119
|
rpcName,
|
|
120
|
+
actor: extractActorFromOptions(optionsArg),
|
|
116
121
|
});
|
|
117
122
|
state.rpc.invokedFunctions.add(rpcName);
|
|
118
123
|
}
|
|
@@ -125,6 +130,21 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
125
130
|
});
|
|
126
131
|
}
|
|
127
132
|
}
|
|
133
|
+
else if (name.text === 'expectEventually' && args.length >= 4) {
|
|
134
|
+
// workflow.expectEventually(stepName, rpcName, data, predicate, options?)
|
|
135
|
+
const stepName = extractStepName(args[0], checker);
|
|
136
|
+
if (isStringLike(args[1], checker)) {
|
|
137
|
+
const rpcName = extractStringLiteral(args[1], checker);
|
|
138
|
+
steps.push({
|
|
139
|
+
type: 'rpc',
|
|
140
|
+
stepName,
|
|
141
|
+
rpcName,
|
|
142
|
+
expectEventually: true,
|
|
143
|
+
actor: extractActorFromOptions(args.length >= 5 ? args[4] : undefined),
|
|
144
|
+
});
|
|
145
|
+
state.rpc.invokedFunctions.add(rpcName);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
128
148
|
else if (name.text === 'sleep' && args.length >= 2) {
|
|
129
149
|
// workflow.sleep(stepName, duration)
|
|
130
150
|
const stepNameArg = args[0];
|
|
@@ -156,8 +176,8 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
156
176
|
});
|
|
157
177
|
}
|
|
158
178
|
/**
|
|
159
|
-
* Inspector for pikkuWorkflowFunc()
|
|
160
|
-
* Detects workflow registration and extracts metadata
|
|
179
|
+
* Inspector for pikkuWorkflowFunc(), pikkuWorkflowComplexFunc() and
|
|
180
|
+
* pikkuUserFlow() calls. Detects workflow registration and extracts metadata.
|
|
161
181
|
*/
|
|
162
182
|
export const addWorkflow = (logger, node, checker, state) => {
|
|
163
183
|
if (!ts.isCallExpression(node)) {
|
|
@@ -176,6 +196,11 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
176
196
|
else if (expression.text === 'pikkuWorkflowComplexFunc') {
|
|
177
197
|
wrapperType = 'complex';
|
|
178
198
|
}
|
|
199
|
+
else if (expression.text === 'pikkuUserFlow') {
|
|
200
|
+
// A user flow is a complex workflow whose steps run as actors over the
|
|
201
|
+
// real transport — same extraction rules as complex, distinct meta.
|
|
202
|
+
wrapperType = 'user-flow';
|
|
203
|
+
}
|
|
179
204
|
else {
|
|
180
205
|
return;
|
|
181
206
|
}
|
|
@@ -193,6 +218,7 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
193
218
|
const { funcNode, resolvedFunc } = extractFunctionNode(firstArg, checker);
|
|
194
219
|
// Extract metadata if using object form
|
|
195
220
|
let tags;
|
|
221
|
+
let title;
|
|
196
222
|
let summary;
|
|
197
223
|
let description;
|
|
198
224
|
let errors;
|
|
@@ -202,6 +228,7 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
202
228
|
if (metadata.disabled)
|
|
203
229
|
return;
|
|
204
230
|
tags = metadata.tags;
|
|
231
|
+
title = metadata.title;
|
|
205
232
|
summary = metadata.summary;
|
|
206
233
|
description = metadata.description;
|
|
207
234
|
errors = metadata.errors;
|
|
@@ -230,7 +257,7 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
230
257
|
// Try DSL workflow extraction first
|
|
231
258
|
// Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
|
|
232
259
|
const result = extractDSLWorkflow(node, checker, {
|
|
233
|
-
allowInline: wrapperType
|
|
260
|
+
allowInline: wrapperType !== 'dsl',
|
|
234
261
|
});
|
|
235
262
|
if (result.status === 'ok' && result.steps) {
|
|
236
263
|
// Extraction succeeded
|
|
@@ -275,24 +302,51 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
275
302
|
// For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
|
|
276
303
|
// patterns the DSL extractor doesn't handle (array+push, nested Promise.all
|
|
277
304
|
// with identifier args, etc.) are still registered as invoked functions.
|
|
278
|
-
|
|
279
|
-
|
|
305
|
+
// When DSL extraction already produced steps this pass is registration-only:
|
|
306
|
+
// appending to `steps` would duplicate nodes and clobber the graph.
|
|
307
|
+
if (wrapperType !== 'dsl') {
|
|
308
|
+
const sink = steps.length > 0 ? [] : steps;
|
|
309
|
+
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, sink);
|
|
280
310
|
}
|
|
311
|
+
// Actor names the flow's steps run as (user flows) — powers the console
|
|
312
|
+
// personas view and the User Flow graph badges.
|
|
313
|
+
const actorNames = [
|
|
314
|
+
...new Set(steps
|
|
315
|
+
.map((s) => ('actor' in s ? s.actor : undefined))
|
|
316
|
+
.filter((a) => typeof a === 'string')),
|
|
317
|
+
];
|
|
281
318
|
state.workflows.meta[workflowName] = {
|
|
282
319
|
pikkuFuncId,
|
|
283
320
|
name: workflowName,
|
|
284
321
|
steps,
|
|
285
322
|
context,
|
|
286
323
|
dsl,
|
|
324
|
+
title,
|
|
287
325
|
summary,
|
|
288
326
|
description,
|
|
289
327
|
errors,
|
|
290
328
|
tags,
|
|
291
329
|
expose,
|
|
330
|
+
userFlow: wrapperType === 'user-flow' ? true : undefined,
|
|
331
|
+
actors: actorNames.length > 0 ? actorNames : undefined,
|
|
292
332
|
};
|
|
333
|
+
// User flows are pure stories of remote RPCs (same rule as client-side CLI
|
|
334
|
+
// renderers): the func may only destructure logger/config — everything else
|
|
335
|
+
// must go through actor steps so the flow runs against the TARGET
|
|
336
|
+
// environment, never local services.
|
|
337
|
+
const funcMeta = state.functions.meta[pikkuFuncId];
|
|
338
|
+
if (wrapperType === 'user-flow' && funcMeta?.services) {
|
|
339
|
+
const disallowed = funcMeta.services.services.filter((svc) => svc !== 'logger' && svc !== 'config');
|
|
340
|
+
if (disallowed.length > 0) {
|
|
341
|
+
logger.critical(ErrorCode.USER_FLOW_HAS_SERVICES, `User flow '${workflowName}' destructures services: ${disallowed.join(', ')}. ` +
|
|
342
|
+
`User flows may only use 'logger'/'config' — drive everything else through ` +
|
|
343
|
+
`actor steps (workflow.do(step, rpc, data, { actor: actors.x })) so the flow ` +
|
|
344
|
+
`runs against the target environment.`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
293
348
|
// Workflow functions require platform services that aren't visible
|
|
294
349
|
// through parameter destructuring (they're accessed via workflow.do/sleep)
|
|
295
|
-
const funcMeta = state.functions.meta[pikkuFuncId];
|
|
296
350
|
if (funcMeta?.services) {
|
|
297
351
|
for (const svc of [
|
|
298
352
|
'workflowService',
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export declare enum ErrorCode {
|
|
|
19
19
|
MISSING_QUEUE_NAME = "PKU384",
|
|
20
20
|
MISSING_CHANNEL_NAME = "PKU400",
|
|
21
21
|
CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = "PKU672",
|
|
22
|
+
USER_FLOW_HAS_SERVICES = "PKU673",
|
|
22
23
|
DYNAMIC_STEP_NAME = "PKU529",
|
|
23
24
|
WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = "PKU600",
|
|
24
25
|
INVALID_DSL_WORKFLOW = "PKU641",
|
package/dist/error-codes.js
CHANGED
|
@@ -21,6 +21,7 @@ export var ErrorCode;
|
|
|
21
21
|
ErrorCode["MISSING_QUEUE_NAME"] = "PKU384";
|
|
22
22
|
ErrorCode["MISSING_CHANNEL_NAME"] = "PKU400";
|
|
23
23
|
ErrorCode["CLI_CLIENTSIDE_RENDERER_HAS_SERVICES"] = "PKU672";
|
|
24
|
+
ErrorCode["USER_FLOW_HAS_SERVICES"] = "PKU673";
|
|
24
25
|
ErrorCode["DYNAMIC_STEP_NAME"] = "PKU529";
|
|
25
26
|
ErrorCode["WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED"] = "PKU600";
|
|
26
27
|
ErrorCode["INVALID_DSL_WORKFLOW"] = "PKU641";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { isWorkflowDoCall, isWorkflowSleepCall, isWorkflowSuspendCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
|
|
2
|
+
import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, extractActorFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, 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
|
/**
|
|
@@ -386,9 +386,12 @@ function extractRpcStep(call, context, outputVar) {
|
|
|
386
386
|
const rpcName = extractStringLiteral(args[1], context.checker);
|
|
387
387
|
// Extract inputs from third argument
|
|
388
388
|
const inputs = args.length >= 3 ? extractInputSources(args[2], context) : undefined;
|
|
389
|
-
//
|
|
390
|
-
const
|
|
391
|
-
|
|
389
|
+
// do(step, rpc, data, options?) vs expectEventually(step, rpc, data, predicate, options?)
|
|
390
|
+
const expectEventually = isWorkflowExpectEventuallyCall(call);
|
|
391
|
+
const optionsIndex = expectEventually ? 4 : 3;
|
|
392
|
+
const optionsArg = args.length > optionsIndex ? args[optionsIndex] : undefined;
|
|
393
|
+
const options = optionsArg && ts.isObjectLiteralExpression(optionsArg)
|
|
394
|
+
? extractStepOptions(optionsArg, context)
|
|
392
395
|
: undefined;
|
|
393
396
|
return {
|
|
394
397
|
type: 'rpc',
|
|
@@ -397,6 +400,8 @@ function extractRpcStep(call, context, outputVar) {
|
|
|
397
400
|
outputVar,
|
|
398
401
|
inputs,
|
|
399
402
|
options,
|
|
403
|
+
actor: extractActorFromOptions(optionsArg),
|
|
404
|
+
expectEventually: expectEventually || undefined,
|
|
400
405
|
};
|
|
401
406
|
}
|
|
402
407
|
catch (error) {
|
|
@@ -3,9 +3,20 @@ import * as ts from 'typescript';
|
|
|
3
3
|
* Pattern detection helpers for DSL workflow extraction
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* Check if a call expression is workflow.do()
|
|
6
|
+
* Check if a call expression is workflow.do() or workflow.expectEventually()
|
|
7
|
+
* (both are RPC steps; expectEventually is the polling variant used by user flows)
|
|
7
8
|
*/
|
|
8
9
|
export declare function isWorkflowDoCall(node: ts.CallExpression, checker: ts.TypeChecker): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Check if a call expression is workflow.expectEventually()
|
|
12
|
+
*/
|
|
13
|
+
export declare function isWorkflowExpectEventuallyCall(node: ts.CallExpression): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Extract the actor NAME from a step options object literal:
|
|
16
|
+
* `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
|
|
17
|
+
* or not statically resolvable.
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractActorFromOptions(optionsArg: ts.Expression | undefined): string | undefined;
|
|
9
20
|
/**
|
|
10
21
|
* Check if a call expression is workflow.sleep()
|
|
11
22
|
*/
|
|
@@ -3,17 +3,52 @@ import * as ts from 'typescript';
|
|
|
3
3
|
* Pattern detection helpers for DSL workflow extraction
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* Check if a call expression is workflow.do()
|
|
6
|
+
* Check if a call expression is workflow.do() or workflow.expectEventually()
|
|
7
|
+
* (both are RPC steps; expectEventually is the polling variant used by user flows)
|
|
7
8
|
*/
|
|
8
9
|
export function isWorkflowDoCall(node, checker) {
|
|
9
10
|
if (!ts.isPropertyAccessExpression(node.expression)) {
|
|
10
11
|
return false;
|
|
11
12
|
}
|
|
12
13
|
const propAccess = node.expression;
|
|
13
|
-
return (propAccess.name.text === 'do'
|
|
14
|
+
return ((propAccess.name.text === 'do' ||
|
|
15
|
+
propAccess.name.text === 'expectEventually') &&
|
|
14
16
|
ts.isIdentifier(propAccess.expression) &&
|
|
15
17
|
propAccess.expression.text === 'workflow');
|
|
16
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if a call expression is workflow.expectEventually()
|
|
21
|
+
*/
|
|
22
|
+
export function isWorkflowExpectEventuallyCall(node) {
|
|
23
|
+
return (ts.isPropertyAccessExpression(node.expression) &&
|
|
24
|
+
node.expression.name.text === 'expectEventually' &&
|
|
25
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
26
|
+
node.expression.expression.text === 'workflow');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Extract the actor NAME from a step options object literal:
|
|
30
|
+
* `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
|
|
31
|
+
* or not statically resolvable.
|
|
32
|
+
*/
|
|
33
|
+
export function extractActorFromOptions(optionsArg) {
|
|
34
|
+
if (!optionsArg || !ts.isObjectLiteralExpression(optionsArg))
|
|
35
|
+
return;
|
|
36
|
+
for (const prop of optionsArg.properties) {
|
|
37
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
38
|
+
ts.isIdentifier(prop.name) &&
|
|
39
|
+
prop.name.text === 'actor') {
|
|
40
|
+
const init = prop.initializer;
|
|
41
|
+
if (ts.isPropertyAccessExpression(init)) {
|
|
42
|
+
return init.name.text;
|
|
43
|
+
}
|
|
44
|
+
if (ts.isElementAccessExpression(init) &&
|
|
45
|
+
ts.isStringLiteral(init.argumentExpression)) {
|
|
46
|
+
return init.argumentExpression.text;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
17
52
|
/**
|
|
18
53
|
* Check if a call expression is workflow.sleep()
|
|
19
54
|
*/
|
|
@@ -58,6 +58,12 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
58
58
|
rpcName: step.rpcName,
|
|
59
59
|
next: nextNodeId,
|
|
60
60
|
};
|
|
61
|
+
if (step.actor) {
|
|
62
|
+
node.actor = step.actor;
|
|
63
|
+
}
|
|
64
|
+
if (step.expectEventually) {
|
|
65
|
+
node.expectEventually = true;
|
|
66
|
+
}
|
|
61
67
|
if (step.inputs) {
|
|
62
68
|
if (step.inputs === 'passthrough') {
|
|
63
69
|
// Entire data is passed through - store as reference to trigger
|
|
@@ -291,16 +297,23 @@ export function convertDslToGraph(workflowName, meta) {
|
|
|
291
297
|
nodesRecord[node.nodeId] = node;
|
|
292
298
|
}
|
|
293
299
|
const entryNodeIds = nodes.length > 0 ? [nodes[0].nodeId] : [];
|
|
294
|
-
// Determine source type
|
|
300
|
+
// Determine source type:
|
|
301
|
+
// - userFlow: complex workflow whose steps run as actors (user flow)
|
|
295
302
|
// - dsl === true: pure DSL workflow, can be serialized
|
|
296
303
|
// - dsl === false: complex workflow with inline steps, not serializable
|
|
297
|
-
const source = meta.
|
|
304
|
+
const source = meta.userFlow
|
|
305
|
+
? 'user-flow'
|
|
306
|
+
: meta.dsl === false
|
|
307
|
+
? 'complex'
|
|
308
|
+
: 'dsl';
|
|
298
309
|
return {
|
|
299
310
|
name: workflowName,
|
|
300
311
|
pikkuFuncId: meta.pikkuFuncId,
|
|
301
312
|
source,
|
|
313
|
+
title: meta.title,
|
|
302
314
|
description: meta.description,
|
|
303
315
|
tags: meta.tags,
|
|
316
|
+
actors: meta.actors,
|
|
304
317
|
context: meta.context,
|
|
305
318
|
nodes: nodesRecord,
|
|
306
319
|
entryNodeIds,
|
|
@@ -96,6 +96,10 @@ export interface FunctionNode extends BaseNode {
|
|
|
96
96
|
outputVar?: string;
|
|
97
97
|
/** Hash of nodeId + RPC input/output schemas for version detection */
|
|
98
98
|
stepHash?: string;
|
|
99
|
+
/** User-flow actor name this step runs as (workflow.do {actor: actors.x}) */
|
|
100
|
+
actor?: string;
|
|
101
|
+
/** True for workflow.expectEventually polling steps (user flows) */
|
|
102
|
+
expectEventually?: boolean;
|
|
99
103
|
}
|
|
100
104
|
/**
|
|
101
105
|
* Flow node - control flow only, no RPC call
|
|
@@ -123,8 +127,9 @@ export declare const isFlowNode: (node: SerializedGraphNode) => node is FlowNode
|
|
|
123
127
|
* - 'dsl': Pure DSL workflow (pikkuWorkflowFunc) - can be round-tripped to code
|
|
124
128
|
* - 'complex': Complex workflow (pikkuWorkflowComplexFunc) - contains inline steps, not serializable
|
|
125
129
|
* - 'graph': Graph-based workflow (pikkuWorkflowGraph)
|
|
130
|
+
* - 'user-flow': User flow (pikkuUserFlow) - complex workflow whose steps run as actors
|
|
126
131
|
*/
|
|
127
|
-
export type WorkflowSourceType = 'dsl' | 'complex' | 'graph';
|
|
132
|
+
export type WorkflowSourceType = 'dsl' | 'complex' | 'graph' | 'user-flow';
|
|
128
133
|
/**
|
|
129
134
|
* Serialized workflow graph - the canonical JSON format
|
|
130
135
|
*/
|
|
@@ -135,10 +140,14 @@ export interface SerializedWorkflowGraph {
|
|
|
135
140
|
pikkuFuncId: string;
|
|
136
141
|
/** Source type: 'dsl' for pikkuWorkflowFunc, 'graph' for pikkuWorkflowGraph */
|
|
137
142
|
source: WorkflowSourceType;
|
|
143
|
+
/** Optional short display name */
|
|
144
|
+
title?: string;
|
|
138
145
|
/** Optional description */
|
|
139
146
|
description?: string;
|
|
140
147
|
/** Tags for organization */
|
|
141
148
|
tags?: string[];
|
|
149
|
+
/** Actor names a user flow's steps run as */
|
|
150
|
+
actors?: string[];
|
|
142
151
|
/** If true, workflow always executes inline without queues */
|
|
143
152
|
inline?: boolean;
|
|
144
153
|
/** Workflow context/state variables (from Zod schema) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.34",
|
|
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.
|
|
38
|
+
"@pikku/core": "^0.12.50",
|
|
39
39
|
"openapi-types": "^12.1.3",
|
|
40
40
|
"path-to-regexp": "^8.3.0",
|
|
41
41
|
"ts-json-schema-generator": "2.9.1-next.16",
|
package/src/add/add-functions.ts
CHANGED
|
@@ -357,9 +357,13 @@ export const addFunctions: AddWiring = (
|
|
|
357
357
|
return
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
// Match identifiers that contain both "pikku" and "func" (case insensitive)
|
|
360
|
+
// Match identifiers that contain both "pikku" and "func" (case insensitive),
|
|
361
|
+
// plus pikkuUserFlow (a workflow wrapper without "func" in its name)
|
|
361
362
|
const pikkuFuncPattern = /pikku.*func/i
|
|
362
|
-
if (
|
|
363
|
+
if (
|
|
364
|
+
!pikkuFuncPattern.test(expression.text) &&
|
|
365
|
+
expression.text !== 'pikkuUserFlow'
|
|
366
|
+
) {
|
|
363
367
|
return
|
|
364
368
|
}
|
|
365
369
|
|
package/src/add/add-workflow.ts
CHANGED
|
@@ -16,7 +16,10 @@ import {
|
|
|
16
16
|
getPropertyValue,
|
|
17
17
|
} from '../utils/get-property-value.js'
|
|
18
18
|
import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js'
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
getSourceText,
|
|
21
|
+
extractActorFromOptions,
|
|
22
|
+
} from '../utils/workflow/dsl/patterns.js'
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Extract a workflow step's display name without letting a non-static name
|
|
@@ -86,7 +89,8 @@ export function collectInvokedRPCs(
|
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
/**
|
|
89
|
-
* Scan for workflow.do(), workflow.
|
|
92
|
+
* Scan for workflow.do(), workflow.expectEventually(), workflow.sleep(), and
|
|
93
|
+
* workflow.cancel() calls to extract workflow steps
|
|
90
94
|
*/
|
|
91
95
|
function getWorkflowInvocations(
|
|
92
96
|
node: ts.Node,
|
|
@@ -100,7 +104,12 @@ function getWorkflowInvocations(
|
|
|
100
104
|
const { name } = node
|
|
101
105
|
|
|
102
106
|
// Check if this is accessing 'do' or 'sleep' property
|
|
103
|
-
if (
|
|
107
|
+
if (
|
|
108
|
+
name.text === 'do' ||
|
|
109
|
+
name.text === 'sleep' ||
|
|
110
|
+
name.text === 'cancel' ||
|
|
111
|
+
name.text === 'expectEventually'
|
|
112
|
+
) {
|
|
104
113
|
// Check if the parent is a call expression
|
|
105
114
|
const parent = node.parent
|
|
106
115
|
if (ts.isCallExpression(parent) && parent.expression === node) {
|
|
@@ -125,6 +134,7 @@ function getWorkflowInvocations(
|
|
|
125
134
|
type: 'rpc',
|
|
126
135
|
stepName,
|
|
127
136
|
rpcName,
|
|
137
|
+
actor: extractActorFromOptions(optionsArg),
|
|
128
138
|
})
|
|
129
139
|
state.rpc.invokedFunctions.add(rpcName)
|
|
130
140
|
} else if (isFunctionLike(secondArg)) {
|
|
@@ -135,6 +145,20 @@ function getWorkflowInvocations(
|
|
|
135
145
|
description: description || '<dynamic>',
|
|
136
146
|
})
|
|
137
147
|
}
|
|
148
|
+
} else if (name.text === 'expectEventually' && args.length >= 4) {
|
|
149
|
+
// workflow.expectEventually(stepName, rpcName, data, predicate, options?)
|
|
150
|
+
const stepName = extractStepName(args[0], checker)
|
|
151
|
+
if (isStringLike(args[1], checker)) {
|
|
152
|
+
const rpcName = extractStringLiteral(args[1], checker)
|
|
153
|
+
steps.push({
|
|
154
|
+
type: 'rpc',
|
|
155
|
+
stepName,
|
|
156
|
+
rpcName,
|
|
157
|
+
expectEventually: true,
|
|
158
|
+
actor: extractActorFromOptions(args.length >= 5 ? args[4] : undefined),
|
|
159
|
+
})
|
|
160
|
+
state.rpc.invokedFunctions.add(rpcName)
|
|
161
|
+
}
|
|
138
162
|
} else if (name.text === 'sleep' && args.length >= 2) {
|
|
139
163
|
// workflow.sleep(stepName, duration)
|
|
140
164
|
const stepNameArg = args[0]
|
|
@@ -169,8 +193,8 @@ function getWorkflowInvocations(
|
|
|
169
193
|
}
|
|
170
194
|
|
|
171
195
|
/**
|
|
172
|
-
* Inspector for pikkuWorkflowFunc()
|
|
173
|
-
* Detects workflow registration and extracts metadata
|
|
196
|
+
* Inspector for pikkuWorkflowFunc(), pikkuWorkflowComplexFunc() and
|
|
197
|
+
* pikkuUserFlow() calls. Detects workflow registration and extracts metadata.
|
|
174
198
|
*/
|
|
175
199
|
export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
176
200
|
if (!ts.isCallExpression(node)) {
|
|
@@ -185,11 +209,15 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
185
209
|
return
|
|
186
210
|
}
|
|
187
211
|
|
|
188
|
-
let wrapperType: 'dsl' | 'complex' | null = null
|
|
212
|
+
let wrapperType: 'dsl' | 'complex' | 'user-flow' | null = null
|
|
189
213
|
if (expression.text === 'pikkuWorkflowFunc') {
|
|
190
214
|
wrapperType = 'dsl'
|
|
191
215
|
} else if (expression.text === 'pikkuWorkflowComplexFunc') {
|
|
192
216
|
wrapperType = 'complex'
|
|
217
|
+
} else if (expression.text === 'pikkuUserFlow') {
|
|
218
|
+
// A user flow is a complex workflow whose steps run as actors over the
|
|
219
|
+
// real transport — same extraction rules as complex, distinct meta.
|
|
220
|
+
wrapperType = 'user-flow'
|
|
193
221
|
} else {
|
|
194
222
|
return
|
|
195
223
|
}
|
|
@@ -220,6 +248,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
220
248
|
|
|
221
249
|
// Extract metadata if using object form
|
|
222
250
|
let tags: string[] | undefined
|
|
251
|
+
let title: string | undefined
|
|
223
252
|
let summary: string | undefined
|
|
224
253
|
let description: string | undefined
|
|
225
254
|
let errors: string[] | undefined
|
|
@@ -235,6 +264,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
235
264
|
)
|
|
236
265
|
if (metadata.disabled) return
|
|
237
266
|
tags = metadata.tags
|
|
267
|
+
title = metadata.title
|
|
238
268
|
summary = metadata.summary
|
|
239
269
|
description = metadata.description
|
|
240
270
|
errors = metadata.errors
|
|
@@ -277,7 +307,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
277
307
|
// Try DSL workflow extraction first
|
|
278
308
|
// Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
|
|
279
309
|
const result = extractDSLWorkflow(node, checker, {
|
|
280
|
-
allowInline: wrapperType
|
|
310
|
+
allowInline: wrapperType !== 'dsl',
|
|
281
311
|
})
|
|
282
312
|
|
|
283
313
|
if (result.status === 'ok' && result.steps) {
|
|
@@ -331,26 +361,62 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
331
361
|
// For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
|
|
332
362
|
// patterns the DSL extractor doesn't handle (array+push, nested Promise.all
|
|
333
363
|
// with identifier args, etc.) are still registered as invoked functions.
|
|
334
|
-
|
|
335
|
-
|
|
364
|
+
// When DSL extraction already produced steps this pass is registration-only:
|
|
365
|
+
// appending to `steps` would duplicate nodes and clobber the graph.
|
|
366
|
+
if (wrapperType !== 'dsl') {
|
|
367
|
+
const sink: WorkflowStepMeta[] = steps.length > 0 ? [] : steps
|
|
368
|
+
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, sink)
|
|
336
369
|
}
|
|
337
370
|
|
|
371
|
+
// Actor names the flow's steps run as (user flows) — powers the console
|
|
372
|
+
// personas view and the User Flow graph badges.
|
|
373
|
+
const actorNames = [
|
|
374
|
+
...new Set(
|
|
375
|
+
steps
|
|
376
|
+
.map((s) => ('actor' in s ? s.actor : undefined))
|
|
377
|
+
.filter((a): a is string => typeof a === 'string')
|
|
378
|
+
),
|
|
379
|
+
]
|
|
380
|
+
|
|
338
381
|
state.workflows.meta[workflowName] = {
|
|
339
382
|
pikkuFuncId,
|
|
340
383
|
name: workflowName,
|
|
341
384
|
steps,
|
|
342
385
|
context,
|
|
343
386
|
dsl,
|
|
387
|
+
title,
|
|
344
388
|
summary,
|
|
345
389
|
description,
|
|
346
390
|
errors,
|
|
347
391
|
tags,
|
|
348
392
|
expose,
|
|
393
|
+
userFlow: wrapperType === 'user-flow' ? true : undefined,
|
|
394
|
+
actors: actorNames.length > 0 ? actorNames : undefined,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// User flows are pure stories of remote RPCs (same rule as client-side CLI
|
|
398
|
+
// renderers): the func may only destructure logger/config — everything else
|
|
399
|
+
// must go through actor steps so the flow runs against the TARGET
|
|
400
|
+
// environment, never local services.
|
|
401
|
+
const funcMeta = state.functions.meta[pikkuFuncId]
|
|
402
|
+
if (wrapperType === 'user-flow' && funcMeta?.services) {
|
|
403
|
+
const disallowed = funcMeta.services.services.filter(
|
|
404
|
+
(svc) => svc !== 'logger' && svc !== 'config'
|
|
405
|
+
)
|
|
406
|
+
if (disallowed.length > 0) {
|
|
407
|
+
logger.critical(
|
|
408
|
+
ErrorCode.USER_FLOW_HAS_SERVICES,
|
|
409
|
+
`User flow '${workflowName}' destructures services: ${disallowed.join(', ')}. ` +
|
|
410
|
+
`User flows may only use 'logger'/'config' — drive everything else through ` +
|
|
411
|
+
`actor steps (workflow.do(step, rpc, data, { actor: actors.x })) so the flow ` +
|
|
412
|
+
`runs against the target environment.`
|
|
413
|
+
)
|
|
414
|
+
return
|
|
415
|
+
}
|
|
349
416
|
}
|
|
350
417
|
|
|
351
418
|
// Workflow functions require platform services that aren't visible
|
|
352
419
|
// through parameter destructuring (they're accessed via workflow.do/sleep)
|
|
353
|
-
const funcMeta = state.functions.meta[pikkuFuncId]
|
|
354
420
|
if (funcMeta?.services) {
|
|
355
421
|
for (const svc of [
|
|
356
422
|
'workflowService',
|