@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
|
@@ -11,6 +11,7 @@ import { resolvePermissions } from '../utils/permissions.js';
|
|
|
11
11
|
import { extractWireNames } from '../utils/post-process.js';
|
|
12
12
|
import { ErrorCode } from '../error-codes.js';
|
|
13
13
|
import { findPiiPaths } from '../utils/check-pii-output.js';
|
|
14
|
+
import { isScenarioInstrumentationFunction } from './scenario-instrumentation.js';
|
|
14
15
|
const isValidVariableName = (name) => {
|
|
15
16
|
const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
16
17
|
return regex.test(name);
|
|
@@ -273,12 +274,22 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
273
274
|
return;
|
|
274
275
|
}
|
|
275
276
|
// Match identifiers that contain both "pikku" and "func" (case insensitive),
|
|
276
|
-
// plus pikkuScenario (
|
|
277
|
+
// plus pikkuScenario and pikkuScenarioStep (workflow wrappers without "func"
|
|
278
|
+
// in their names)
|
|
277
279
|
const pikkuFuncPattern = /pikku.*func/i;
|
|
278
280
|
if (!pikkuFuncPattern.test(expression.text) &&
|
|
279
|
-
expression.text !== 'pikkuScenario'
|
|
281
|
+
expression.text !== 'pikkuScenario' &&
|
|
282
|
+
expression.text !== 'pikkuScenarioStep') {
|
|
280
283
|
return;
|
|
281
284
|
}
|
|
285
|
+
// A scenario step is bundled like any invoked function, but is deliberately
|
|
286
|
+
// never RPC-registered: a step may drive a browser, so exposing it over the
|
|
287
|
+
// network would put a test harness on the public API surface.
|
|
288
|
+
const isScenarioStep = expression.text === 'pikkuScenarioStep';
|
|
289
|
+
// A scenario's own body. Marked so the codegen partitions and the deploy
|
|
290
|
+
// analyzer can tell it apart from an application workflow without having to
|
|
291
|
+
// reach into the workflow graph.
|
|
292
|
+
const isScenario = expression.text === 'pikkuScenario';
|
|
282
293
|
// only handle calls like pikkuFunc(...)
|
|
283
294
|
if (!ts.isIdentifier(expression) || !expression.text.startsWith('pikku')) {
|
|
284
295
|
return;
|
|
@@ -289,6 +300,14 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
289
300
|
if (!pikkuFuncId || pikkuFuncId.startsWith('__temp_')) {
|
|
290
301
|
return;
|
|
291
302
|
}
|
|
303
|
+
// pikku's own scenario instrumentation, which older projects still carry as a
|
|
304
|
+
// scaffolded file in their source. It is not application code and must not be
|
|
305
|
+
// registered, metadata'd or deployed as if it were — `pikku dev` registers the
|
|
306
|
+
// implementations itself, and that is the only server that should have them.
|
|
307
|
+
if (isScenarioInstrumentationFunction(pikkuFuncId) ||
|
|
308
|
+
isScenarioInstrumentationFunction(name)) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
292
311
|
let title;
|
|
293
312
|
let tags;
|
|
294
313
|
let summary;
|
|
@@ -304,6 +323,8 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
304
323
|
let workflowQueued;
|
|
305
324
|
let workflowRetries;
|
|
306
325
|
let workflowTimeout;
|
|
326
|
+
let scenarioStepBrowser;
|
|
327
|
+
let scenarioStepTemplate;
|
|
307
328
|
let scopes;
|
|
308
329
|
let version;
|
|
309
330
|
let objectNode;
|
|
@@ -375,6 +396,8 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
375
396
|
workflowQueued = getPropertyValue(firstArg, 'workflowQueued');
|
|
376
397
|
workflowRetries = getPropertyValue(firstArg, 'workflowRetries');
|
|
377
398
|
workflowTimeout = getPropertyValue(firstArg, 'workflowTimeout');
|
|
399
|
+
scenarioStepBrowser = getPropertyValue(firstArg, 'browser');
|
|
400
|
+
scenarioStepTemplate = getPropertyValue(firstArg, 'template');
|
|
378
401
|
scopes = getArrayPropertyValue(firstArg, 'scopes') ?? undefined;
|
|
379
402
|
// Extract approvalDescription identifier reference
|
|
380
403
|
for (const prop of firstArg.properties) {
|
|
@@ -773,6 +796,8 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
773
796
|
outputs: outputNames.filter((n) => n !== 'void') ?? null,
|
|
774
797
|
expose: expose || undefined,
|
|
775
798
|
remote: remote || undefined,
|
|
799
|
+
scenarioStep: isScenarioStep || undefined,
|
|
800
|
+
scenario: isScenario || undefined,
|
|
776
801
|
mcp: mcpEnabled || undefined,
|
|
777
802
|
readonly: readonly_ || undefined,
|
|
778
803
|
deploy: deploy || undefined,
|
|
@@ -781,6 +806,8 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
781
806
|
workflowQueued: workflowQueued === true ? true : undefined,
|
|
782
807
|
workflowRetries: workflowRetries ?? undefined,
|
|
783
808
|
workflowTimeout: workflowTimeout ?? undefined,
|
|
809
|
+
scenarioStepBrowser: scenarioStepBrowser === true ? true : undefined,
|
|
810
|
+
scenarioStepTemplate: scenarioStepTemplate || undefined,
|
|
784
811
|
scopes: scopes ?? undefined,
|
|
785
812
|
implementationHash,
|
|
786
813
|
version,
|
|
@@ -863,7 +890,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
863
890
|
logger.error(`• Function with explicit name '${name}' is not exported, this is not allowed.`);
|
|
864
891
|
return;
|
|
865
892
|
}
|
|
866
|
-
if (remote) {
|
|
893
|
+
if (remote && !isScenarioStep) {
|
|
867
894
|
state.rpc.invokedFunctions.add(pikkuFuncId);
|
|
868
895
|
// The consumer-facing surface a wireRemoteAddon imports (mirrors exposedMeta)
|
|
869
896
|
state.rpc.remoteMeta[name] = pikkuFuncId;
|
|
@@ -872,7 +899,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
872
899
|
exportedName,
|
|
873
900
|
});
|
|
874
901
|
}
|
|
875
|
-
if (expose) {
|
|
902
|
+
if (expose && !isScenarioStep) {
|
|
876
903
|
state.rpc.exposedMeta[name] = pikkuFuncId;
|
|
877
904
|
state.rpc.exposedFiles.set(name, {
|
|
878
905
|
path: node.getSourceFile().fileName,
|
|
@@ -881,11 +908,13 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
881
908
|
// Track exposed RPC function for service aggregation
|
|
882
909
|
state.serviceAggregation.usedFunctions.add(pikkuFuncId);
|
|
883
910
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
911
|
+
if (!isScenarioStep) {
|
|
912
|
+
// We add it to internal meta to allow autocomplete for everything
|
|
913
|
+
state.rpc.internalMeta[name] = pikkuFuncId;
|
|
914
|
+
if (version !== undefined) {
|
|
915
|
+
state.rpc.internalMeta[pikkuFuncId] = pikkuFuncId;
|
|
916
|
+
state.rpc.invokedFunctions.add(pikkuFuncId);
|
|
917
|
+
}
|
|
889
918
|
}
|
|
890
919
|
// But we only import the actual function if it's actually invoked to keep
|
|
891
920
|
// bundle size down
|
package/dist/add/add-workflow.js
CHANGED
|
@@ -86,6 +86,11 @@ export function collectInvokedRPCs(steps, rpcs) {
|
|
|
86
86
|
rpcs.add(step.options.onError);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
else if (step.type === 'scenarioStep' && step.stepFunc) {
|
|
90
|
+
// Steps are bundled like RPCs — the runner calls them through
|
|
91
|
+
// runPikkuFunc — but they are deliberately never RPC-registered.
|
|
92
|
+
rpcs.add(step.stepFunc);
|
|
93
|
+
}
|
|
89
94
|
else if (step.type === 'branch') {
|
|
90
95
|
for (const branch of step.branches) {
|
|
91
96
|
collectInvokedRPCs(branch.steps, rpcs);
|
|
@@ -247,6 +252,7 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
247
252
|
let description;
|
|
248
253
|
let errors;
|
|
249
254
|
let expose;
|
|
255
|
+
let skip;
|
|
250
256
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
251
257
|
const metadata = getCommonWireMetaData(firstArg, 'Workflow', workflowName, logger, checker);
|
|
252
258
|
if (metadata.disabled)
|
|
@@ -257,6 +263,7 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
257
263
|
description = metadata.description;
|
|
258
264
|
errors = metadata.errors;
|
|
259
265
|
expose = getPropertyValue(firstArg, 'expose');
|
|
266
|
+
skip = getPropertyValue(firstArg, 'skip') ?? undefined;
|
|
260
267
|
}
|
|
261
268
|
// Validate that we got a valid function
|
|
262
269
|
if (ts.isObjectLiteralExpression(firstArg) &&
|
|
@@ -324,6 +331,13 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
324
331
|
logger.critical(ErrorCode.INVALID_DSL_WORKFLOW, `Workflow '${workflowName}' does not conform to DSL workflow rules:\n${result.reason || 'Unknown error'}`);
|
|
325
332
|
return;
|
|
326
333
|
}
|
|
334
|
+
else if (wrapperType === 'scenario') {
|
|
335
|
+
// The fallback below understands do/sleep but not step/given/when/then,
|
|
336
|
+
// so a scenario that lands here registers with zero steps. Silence would
|
|
337
|
+
// leave the whole scenario passing vacuously.
|
|
338
|
+
logger.critical(ErrorCode.SCENARIO_NOT_EXTRACTABLE, `Scenario '${workflowName}' could not be extracted, so none of its steps were recorded and it would run as an empty scenario:\n${result.reason || 'Unknown error'}`);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
327
341
|
else {
|
|
328
342
|
// For pikkuWorkflowComplexFunc, fall back to basic extraction
|
|
329
343
|
logger.debug(`Workflow '${workflowName}' could not be extracted as DSL workflow: ${result.reason || 'Unknown error'}. Falling back to basic extraction.`);
|
|
@@ -358,36 +372,15 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
358
372
|
errors,
|
|
359
373
|
tags,
|
|
360
374
|
expose,
|
|
375
|
+
skip,
|
|
361
376
|
scenario: wrapperType === 'scenario' ? true : undefined,
|
|
362
377
|
actors: actorNames.length > 0 ? actorNames : undefined,
|
|
363
378
|
};
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (disallowed.length > 0) {
|
|
372
|
-
logger.critical(ErrorCode.SCENARIO_HAS_SERVICES, `Scenario '${workflowName}' destructures services: ${disallowed.join(', ')}. ` +
|
|
373
|
-
`Scenarios may only use 'logger'/'config' — drive everything else through ` +
|
|
374
|
-
`actor steps (workflow.do(step, rpc, data, { actor: actors.x })) so the flow ` +
|
|
375
|
-
`runs against the target environment.`);
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
// Workflow functions require platform services that aren't visible
|
|
380
|
-
// through parameter destructuring (they're accessed via workflow.do/sleep)
|
|
381
|
-
if (funcMeta?.services) {
|
|
382
|
-
for (const svc of [
|
|
383
|
-
'workflowService',
|
|
384
|
-
'workflowRunService',
|
|
385
|
-
'schedulerService',
|
|
386
|
-
'queueService',
|
|
387
|
-
]) {
|
|
388
|
-
if (!funcMeta.services.services.includes(svc)) {
|
|
389
|
-
funcMeta.services.services.push(svc);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
379
|
+
// The scenario "logger/config only" rule (PKU673) and the implicit
|
|
380
|
+
// workflow platform-service injection both used to live here, reading
|
|
381
|
+
// state.functions.meta[pikkuFuncId]. addWorkflow runs in the visitSetup
|
|
382
|
+
// pass, which completes before addFunctions populates that meta, so both
|
|
383
|
+
// were dead. The rule now lives in validateScenarioServices (post-process),
|
|
384
|
+
// and aggregateRequiredServices already adds the platform services globally
|
|
385
|
+
// whenever any workflow exists.
|
|
393
386
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The instrumentation RPCs `pikku scenario run` calls on the server it is
|
|
3
|
+
* testing: snapshot/reset live coverage, and reset/read recorded stub calls.
|
|
4
|
+
*
|
|
5
|
+
* They are pikku's own, not the application's. They used to be scaffolded into
|
|
6
|
+
* the project as ordinary `pikkuFunc`s, which made them indistinguishable from
|
|
7
|
+
* application code — so they were registered in the app bootstrap, listed in the
|
|
8
|
+
* app's function and RPC meta, and shipped, `expose: true`, in every deployed
|
|
9
|
+
* bundle. A deployment has no use for them: coverage and stub inspection are
|
|
10
|
+
* things you do to a development server.
|
|
11
|
+
*
|
|
12
|
+
* The inspector therefore ignores these names wherever it finds them, and
|
|
13
|
+
* `pikku dev` registers the implementations itself. Ignoring by name (rather
|
|
14
|
+
* than deleting the scaffold and requiring every project to regenerate) is what
|
|
15
|
+
* keeps an existing project's checked-in `scenarios.gen.ts` from deploying.
|
|
16
|
+
*/
|
|
17
|
+
export declare const SCENARIO_INSTRUMENTATION_FUNCTIONS: readonly ["pikkuScenarioTakeLiveCoverage", "pikkuScenarioResetLiveCoverage", "pikkuScenarioResetStubs", "pikkuScenarioGetStubCalls"];
|
|
18
|
+
export type ScenarioInstrumentationFunction = (typeof SCENARIO_INSTRUMENTATION_FUNCTIONS)[number];
|
|
19
|
+
/**
|
|
20
|
+
* True for a scenario instrumentation function, by name. Versioned ids
|
|
21
|
+
* (`name@v2`) are matched on their base so a project that bumps one is still
|
|
22
|
+
* covered.
|
|
23
|
+
*/
|
|
24
|
+
export declare const isScenarioInstrumentationFunction: (name: string | undefined) => boolean;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The instrumentation RPCs `pikku scenario run` calls on the server it is
|
|
3
|
+
* testing: snapshot/reset live coverage, and reset/read recorded stub calls.
|
|
4
|
+
*
|
|
5
|
+
* They are pikku's own, not the application's. They used to be scaffolded into
|
|
6
|
+
* the project as ordinary `pikkuFunc`s, which made them indistinguishable from
|
|
7
|
+
* application code — so they were registered in the app bootstrap, listed in the
|
|
8
|
+
* app's function and RPC meta, and shipped, `expose: true`, in every deployed
|
|
9
|
+
* bundle. A deployment has no use for them: coverage and stub inspection are
|
|
10
|
+
* things you do to a development server.
|
|
11
|
+
*
|
|
12
|
+
* The inspector therefore ignores these names wherever it finds them, and
|
|
13
|
+
* `pikku dev` registers the implementations itself. Ignoring by name (rather
|
|
14
|
+
* than deleting the scaffold and requiring every project to regenerate) is what
|
|
15
|
+
* keeps an existing project's checked-in `scenarios.gen.ts` from deploying.
|
|
16
|
+
*/
|
|
17
|
+
export const SCENARIO_INSTRUMENTATION_FUNCTIONS = [
|
|
18
|
+
'pikkuScenarioTakeLiveCoverage',
|
|
19
|
+
'pikkuScenarioResetLiveCoverage',
|
|
20
|
+
'pikkuScenarioResetStubs',
|
|
21
|
+
'pikkuScenarioGetStubCalls',
|
|
22
|
+
];
|
|
23
|
+
const names = new Set(SCENARIO_INSTRUMENTATION_FUNCTIONS);
|
|
24
|
+
/**
|
|
25
|
+
* True for a scenario instrumentation function, by name. Versioned ids
|
|
26
|
+
* (`name@v2`) are matched on their base so a project that bumps one is still
|
|
27
|
+
* covered.
|
|
28
|
+
*/
|
|
29
|
+
export const isScenarioInstrumentationFunction = (name) => (name ? names.has(name.split('@')[0]) : false);
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ export declare enum ErrorCode {
|
|
|
20
20
|
CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = "PKU672",
|
|
21
21
|
SCENARIO_HAS_SERVICES = "PKU673",
|
|
22
22
|
EXPECT_EVENTUALLY_SCENARIO_ONLY = "PKU675",
|
|
23
|
+
SCENARIO_BROWSER_STEP_NEEDS_ACTOR = "PKU677",
|
|
24
|
+
SCENARIO_STEP_TARGET_NOT_STATIC = "PKU678",
|
|
25
|
+
SCENARIO_NOT_EXTRACTABLE = "PKU679",
|
|
23
26
|
WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = "PKU600",
|
|
24
27
|
INVALID_DSL_WORKFLOW = "PKU641",
|
|
25
28
|
WORKFLOW_GRAPH_ADDON_NOT_WIRED = "PKU642",
|
package/dist/error-codes.js
CHANGED
|
@@ -22,6 +22,9 @@ export var ErrorCode;
|
|
|
22
22
|
ErrorCode["CLI_CLIENTSIDE_RENDERER_HAS_SERVICES"] = "PKU672";
|
|
23
23
|
ErrorCode["SCENARIO_HAS_SERVICES"] = "PKU673";
|
|
24
24
|
ErrorCode["EXPECT_EVENTUALLY_SCENARIO_ONLY"] = "PKU675";
|
|
25
|
+
ErrorCode["SCENARIO_BROWSER_STEP_NEEDS_ACTOR"] = "PKU677";
|
|
26
|
+
ErrorCode["SCENARIO_STEP_TARGET_NOT_STATIC"] = "PKU678";
|
|
27
|
+
ErrorCode["SCENARIO_NOT_EXTRACTABLE"] = "PKU679";
|
|
25
28
|
ErrorCode["WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED"] = "PKU600";
|
|
26
29
|
ErrorCode["INVALID_DSL_WORKFLOW"] = "PKU641";
|
|
27
30
|
ErrorCode["WORKFLOW_GRAPH_ADDON_NOT_WIRED"] = "PKU642";
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type { FilesAndMethodsErrors } from './utils/get-files-and-methods.js';
|
|
|
5
5
|
export { ErrorCode } from './error-codes.js';
|
|
6
6
|
export type { DiagnosticSeverity, CodedDiagnostic } from './error-codes.js';
|
|
7
7
|
export { AUTH_HANDLER_FUNC_ID } from './add/add-auth.js';
|
|
8
|
+
export { SCENARIO_INSTRUMENTATION_FUNCTIONS, isScenarioInstrumentationFunction, } from './add/scenario-instrumentation.js';
|
|
9
|
+
export type { ScenarioInstrumentationFunction } from './add/scenario-instrumentation.js';
|
|
8
10
|
export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
|
|
9
11
|
export type { SerializableInspectorState } from './utils/serialize-inspector-state.js';
|
|
10
12
|
export { filterInspectorState } from './utils/filter-inspector-state.js';
|
|
@@ -12,6 +14,7 @@ export { resolveDeployTarget } from './utils/resolve-deploy-target.js';
|
|
|
12
14
|
export { generateCustomTypes, sanitizeTypeName, } from './utils/custom-types-generator.js';
|
|
13
15
|
export { createEmptyManifest, serializeManifest, } from './utils/contract-hashes.js';
|
|
14
16
|
export type { ContractEntry, VersionHashEntry, VersionValidateError, VersionManifest, VersionManifestEntry, } from './utils/contract-hashes.js';
|
|
17
|
+
export type { InspectorFeature, InspectorFeatureEntry } from './types.js';
|
|
15
18
|
export { serializeMCPJson } from './utils/serialize-mcp-json.js';
|
|
16
19
|
export type { OpenAPISpecInfo } from './utils/serialize-openapi-json.js';
|
|
17
20
|
export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './utils/workflow/dsl/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { inspect, getInitialInspectorState } from './inspector.js';
|
|
2
2
|
export { ErrorCode } from './error-codes.js';
|
|
3
3
|
export { AUTH_HANDLER_FUNC_ID } from './add/add-auth.js';
|
|
4
|
+
export { SCENARIO_INSTRUMENTATION_FUNCTIONS, isScenarioInstrumentationFunction, } from './add/scenario-instrumentation.js';
|
|
4
5
|
export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
|
|
5
6
|
export { filterInspectorState } from './utils/filter-inspector-state.js';
|
|
6
7
|
export { resolveDeployTarget } from './utils/resolve-deploy-target.js';
|
package/dist/inspector.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { performance } from 'perf_hooks';
|
|
3
|
-
import { resolve } from 'path';
|
|
3
|
+
import { dirname, resolve } from 'path';
|
|
4
4
|
import { visitSetup, visitFunctions, visitRoutes } from './visit.js';
|
|
5
5
|
import { TypesMap } from './types-map.js';
|
|
6
6
|
import { getFilesAndMethods } from './utils/get-files-and-methods.js';
|
|
7
7
|
import { findCommonAncestor } from './utils/find-root-dir.js';
|
|
8
8
|
import { createNestedProjectFilter } from './utils/nested-project-filter.js';
|
|
9
|
-
import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, validateSchemaWiringSeparation, validateWorkflowGraphAddons, } from './utils/post-process.js';
|
|
9
|
+
import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, validateSchemaWiringSeparation, validateScenarioServices, validateScenarioSteps, validateWorkflowGraphAddons, } from './utils/post-process.js';
|
|
10
10
|
import { generateOpenAPISpec } from './utils/serialize-openapi-json.js';
|
|
11
11
|
import { pikkuState } from '@pikku/core/internal';
|
|
12
12
|
import { resolveLatestVersions } from './utils/resolve-versions.js';
|
|
@@ -86,6 +86,7 @@ export function getInitialInspectorState(rootDir) {
|
|
|
86
86
|
files: new Map(),
|
|
87
87
|
graphMeta: {},
|
|
88
88
|
graphFiles: new Map(),
|
|
89
|
+
featureFiles: new Map(),
|
|
89
90
|
invokedWorkflows: new Set(),
|
|
90
91
|
},
|
|
91
92
|
rpc: {
|
|
@@ -204,9 +205,43 @@ export function getInitialInspectorState(rootDir) {
|
|
|
204
205
|
program: null,
|
|
205
206
|
};
|
|
206
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* The path mappings the inspector's program has to share with the project.
|
|
210
|
+
*
|
|
211
|
+
* The inspector builds its own program rather than reusing the project's, which
|
|
212
|
+
* is what keeps a cold run affordable — but a project that reaches its generated
|
|
213
|
+
* code through a tsconfig `paths` entry resolves that import through those
|
|
214
|
+
* mappings and nothing else. Without them the factory an inspected function is
|
|
215
|
+
* passed to resolves to `any`, so a function whose input type comes from the
|
|
216
|
+
* factory's contextual type (rather than an explicit generic or a named schema)
|
|
217
|
+
* silently records no input at all.
|
|
218
|
+
*
|
|
219
|
+
* Only resolution keys are copied. Type-inference options such as `strict` stay
|
|
220
|
+
* the inspector's own, because changing those would change the recorded types.
|
|
221
|
+
*/
|
|
222
|
+
const readProjectPathMappings = (logger, tsconfig) => {
|
|
223
|
+
if (!tsconfig) {
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
226
|
+
const configPath = resolve(tsconfig);
|
|
227
|
+
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
228
|
+
if (configFile.error || !configFile.config) {
|
|
229
|
+
logger.debug(`Could not read ${configPath} for path mappings; imports that rely on them will not resolve`);
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
const { options: projectOptions } = ts.parseJsonConfigFileContent(configFile.config, ts.sys, dirname(configPath));
|
|
233
|
+
const { baseUrl, paths, rootDirs, pathsBasePath } = projectOptions;
|
|
234
|
+
return {
|
|
235
|
+
...(baseUrl !== undefined ? { baseUrl } : {}),
|
|
236
|
+
...(paths !== undefined ? { paths } : {}),
|
|
237
|
+
...(rootDirs !== undefined ? { rootDirs } : {}),
|
|
238
|
+
...(pathsBasePath !== undefined ? { pathsBasePath } : {}),
|
|
239
|
+
};
|
|
240
|
+
};
|
|
207
241
|
export const inspect = async (logger, routeFiles, options = {}) => {
|
|
208
242
|
const normalizedRouteFiles = routeFiles.map((file) => resolve(file));
|
|
209
243
|
const compilerOptions = {
|
|
244
|
+
...readProjectPathMappings(logger, options.tsconfig),
|
|
210
245
|
target: ts.ScriptTarget.ESNext,
|
|
211
246
|
module: ts.ModuleKind.Node16,
|
|
212
247
|
skipLibCheck: true,
|
|
@@ -310,6 +345,8 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
310
345
|
computeDiagnostics(state);
|
|
311
346
|
validateSchemaWiringSeparation(logger, state);
|
|
312
347
|
validateWorkflowGraphAddons(logger, state);
|
|
348
|
+
validateScenarioServices(logger, state);
|
|
349
|
+
validateScenarioSteps(logger, state);
|
|
313
350
|
if (options.openAPI) {
|
|
314
351
|
state.openAPISpec = await generateOpenAPISpec(logger, state.functions.meta, state.http.meta, state.schemas, options.openAPI.additionalInfo, pikkuState(null, 'misc', 'errors'));
|
|
315
352
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -205,6 +205,12 @@ export type InspectorOptions = Partial<{
|
|
|
205
205
|
rootDir: string;
|
|
206
206
|
isAddon: boolean;
|
|
207
207
|
sourceFile: ts.SourceFile;
|
|
208
|
+
/**
|
|
209
|
+
* The project's tsconfig, read for its path mappings only. The inspector's
|
|
210
|
+
* program needs them to resolve imports the project reaches through `paths`;
|
|
211
|
+
* unlike `schemaConfig.tsconfig` this is also needed on a setup-only run.
|
|
212
|
+
*/
|
|
213
|
+
tsconfig: string;
|
|
208
214
|
types: Partial<{
|
|
209
215
|
configFileType: string;
|
|
210
216
|
userSessionType: string;
|
|
@@ -346,6 +352,34 @@ export interface AuthDefinition {
|
|
|
346
352
|
*/
|
|
347
353
|
services: FunctionServicesMeta;
|
|
348
354
|
}
|
|
355
|
+
/** One statically resolved entry of a feature's `scenarios` array. */
|
|
356
|
+
export interface InspectorFeatureEntry {
|
|
357
|
+
/** The scenario's declared export name, with any import alias followed back. */
|
|
358
|
+
scenario: string;
|
|
359
|
+
/** A `{ scenario, data }` entry's literal data — gherkin's `Examples:`. */
|
|
360
|
+
data?: unknown;
|
|
361
|
+
}
|
|
362
|
+
/** A `pikkuFeature` export, as far as it can be read from the source. */
|
|
363
|
+
export interface InspectorFeature {
|
|
364
|
+
path: string;
|
|
365
|
+
exportedName: string;
|
|
366
|
+
name?: string;
|
|
367
|
+
description?: string;
|
|
368
|
+
tags?: string[];
|
|
369
|
+
entries: InspectorFeatureEntry[];
|
|
370
|
+
/**
|
|
371
|
+
* Entries that are not literal — a spread, a `.map()`, a call. Counted rather
|
|
372
|
+
* than dropped so the console can say the listing is partial instead of
|
|
373
|
+
* quietly showing a short feature.
|
|
374
|
+
*/
|
|
375
|
+
unresolvedEntries: number;
|
|
376
|
+
/**
|
|
377
|
+
* Feature hooks run once around the whole group, never per scenario, and are
|
|
378
|
+
* runtime-only — so only their presence is recorded.
|
|
379
|
+
*/
|
|
380
|
+
hasBefore: boolean;
|
|
381
|
+
hasAfter: boolean;
|
|
382
|
+
}
|
|
349
383
|
export interface InspectorState {
|
|
350
384
|
rootDir: string;
|
|
351
385
|
singletonServicesTypeImportMap: PathToNameAndType;
|
|
@@ -395,6 +429,15 @@ export interface InspectorState {
|
|
|
395
429
|
path: string;
|
|
396
430
|
exportedName: string;
|
|
397
431
|
}>;
|
|
432
|
+
/**
|
|
433
|
+
* `pikkuFeature` exports, keyed by export identifier.
|
|
434
|
+
*
|
|
435
|
+
* Everything the config states literally is extracted here, because a
|
|
436
|
+
* feature is the document structure the console renders. Entries built by
|
|
437
|
+
* an ordinary loop cannot be enumerated statically — those are counted in
|
|
438
|
+
* `unresolvedEntries` and resolved at runtime by object identity instead.
|
|
439
|
+
*/
|
|
440
|
+
featureFiles: Map<string, InspectorFeature>;
|
|
398
441
|
invokedWorkflows: Set<string>;
|
|
399
442
|
};
|
|
400
443
|
rpc: {
|
|
@@ -28,6 +28,32 @@ export function funcIdToTypeName(id) {
|
|
|
28
28
|
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
29
29
|
.join('');
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* `pikkuScenarioStep({ name })` is the one wrapper whose `name` is its identity:
|
|
33
|
+
* scenarios reference a step by that string, so it must become the pikkuFuncId
|
|
34
|
+
* (and hence the generated step map's key), not just a cosmetic label the way
|
|
35
|
+
* `pikkuMiddleware`/`pikkuAgent` use it.
|
|
36
|
+
*/
|
|
37
|
+
function extractScenarioStepName(callExpr, result) {
|
|
38
|
+
if (!ts.isCallExpression(callExpr) ||
|
|
39
|
+
!ts.isIdentifier(callExpr.expression) ||
|
|
40
|
+
callExpr.expression.text !== 'pikkuScenarioStep') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const firstArg = callExpr.arguments[0];
|
|
44
|
+
if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
for (const prop of firstArg.properties) {
|
|
48
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
49
|
+
ts.isIdentifier(prop.name) &&
|
|
50
|
+
prop.name.text === 'name' &&
|
|
51
|
+
ts.isStringLiteral(prop.initializer)) {
|
|
52
|
+
result.explicitName = prop.initializer.text;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
31
57
|
export function extractFunctionName(callExpr, checker, rootDir) {
|
|
32
58
|
const parent = callExpr.parent;
|
|
33
59
|
// Initialize the result
|
|
@@ -40,6 +66,7 @@ export function extractFunctionName(callExpr, checker, rootDir) {
|
|
|
40
66
|
isHelper: false,
|
|
41
67
|
version: null,
|
|
42
68
|
};
|
|
69
|
+
extractScenarioStepName(callExpr, result);
|
|
43
70
|
const workflowHelpers = new Set([
|
|
44
71
|
'workflow',
|
|
45
72
|
'workflowStart',
|
|
@@ -50,6 +50,32 @@ export declare function computeMiddlewareGroupsMeta(state: InspectorState): void
|
|
|
50
50
|
export declare function computePermissionsGroupsMeta(state: InspectorState): void;
|
|
51
51
|
export declare function computeRequiredSchemas(state: InspectorState, options: InspectorOptions): void;
|
|
52
52
|
export declare function validateAgentModels(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
|
|
53
|
+
/**
|
|
54
|
+
* Scenarios are pure stories of remote RPCs (same rule as client-side CLI
|
|
55
|
+
* renderers): the func may only destructure logger/config — everything else
|
|
56
|
+
* must go through actor steps so the flow runs against the TARGET
|
|
57
|
+
* environment, never local services.
|
|
58
|
+
*
|
|
59
|
+
* This runs in post-processing rather than in `addWorkflow` because
|
|
60
|
+
* `addWorkflow` runs in the `visitSetup` pass, which completes before
|
|
61
|
+
* `addFunctions` (`visitFunctions`) has populated `state.functions.meta` — so
|
|
62
|
+
* the check read `undefined` there and never fired.
|
|
63
|
+
*/
|
|
64
|
+
export declare function validateScenarioServices(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
|
|
65
|
+
/**
|
|
66
|
+
* Walk every scenario step, wherever it is nested, and validate the two things
|
|
67
|
+
* that can only be known once both the workflow meta and the function meta
|
|
68
|
+
* exist:
|
|
69
|
+
*
|
|
70
|
+
* - the step target must be a static string literal (PKU678) — otherwise it
|
|
71
|
+
* can't be bundled, typed or drawn;
|
|
72
|
+
* - a `browser: true` step must be given an actor (PKU677), since the browser
|
|
73
|
+
* context signs in as that persona.
|
|
74
|
+
*
|
|
75
|
+
* It also marks the step's function as used, so `scenario run` boots the
|
|
76
|
+
* singletons the step actually needs.
|
|
77
|
+
*/
|
|
78
|
+
export declare function validateScenarioSteps(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
|
|
53
79
|
/**
|
|
54
80
|
* A `pikkuWorkflowGraph` node that references the `graph:` namespace (e.g.
|
|
55
81
|
* `graph:editFields`) needs @pikku/addon-graph wired — otherwise the RPC never
|
|
@@ -4,6 +4,7 @@ import { extractTypeKeys } from './type-utils.js';
|
|
|
4
4
|
import { ErrorCode } from '../error-codes.js';
|
|
5
5
|
import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js';
|
|
6
6
|
import { flattenScopeDefinitions } from '@pikku/core/scope';
|
|
7
|
+
import { DYNAMIC_SCENARIO_STEP_TARGET } from './workflow/dsl/patterns.js';
|
|
7
8
|
/**
|
|
8
9
|
* Stamp the inspected authorize/callbacks service set onto the generated auth
|
|
9
10
|
* handler's function meta.
|
|
@@ -211,6 +212,13 @@ export function aggregateRequiredServices(state) {
|
|
|
211
212
|
for (const [, graph] of Object.entries(state.workflows.graphMeta)) {
|
|
212
213
|
if (!graph.nodes || !graph.name)
|
|
213
214
|
continue;
|
|
215
|
+
// A scenario is a workflow, but it only ever runs in-process under
|
|
216
|
+
// `pikku scenario run` — no step of one is dispatched through a queue. The
|
|
217
|
+
// synthetic entries were pure leakage: they put a
|
|
218
|
+
// `wf-orchestrator-<scenario>` worker into the app's queue meta, which every
|
|
219
|
+
// bundle imports and a provider then creates as a real production queue.
|
|
220
|
+
if (graph.source === 'scenario')
|
|
221
|
+
continue;
|
|
214
222
|
const toKebab = (s) => s
|
|
215
223
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
216
224
|
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
@@ -552,6 +560,90 @@ export function validateAgentModels(logger, state) {
|
|
|
552
560
|
}
|
|
553
561
|
}
|
|
554
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* Scenarios are pure stories of remote RPCs (same rule as client-side CLI
|
|
565
|
+
* renderers): the func may only destructure logger/config — everything else
|
|
566
|
+
* must go through actor steps so the flow runs against the TARGET
|
|
567
|
+
* environment, never local services.
|
|
568
|
+
*
|
|
569
|
+
* This runs in post-processing rather than in `addWorkflow` because
|
|
570
|
+
* `addWorkflow` runs in the `visitSetup` pass, which completes before
|
|
571
|
+
* `addFunctions` (`visitFunctions`) has populated `state.functions.meta` — so
|
|
572
|
+
* the check read `undefined` there and never fired.
|
|
573
|
+
*/
|
|
574
|
+
export function validateScenarioServices(logger, state) {
|
|
575
|
+
for (const [workflowName, meta] of Object.entries(state.workflows.meta)) {
|
|
576
|
+
if (!meta.scenario)
|
|
577
|
+
continue;
|
|
578
|
+
const funcMeta = state.functions.meta[meta.pikkuFuncId];
|
|
579
|
+
if (!funcMeta?.services)
|
|
580
|
+
continue;
|
|
581
|
+
const disallowed = funcMeta.services.services.filter((svc) => svc !== 'logger' && svc !== 'config');
|
|
582
|
+
if (disallowed.length > 0) {
|
|
583
|
+
logger.critical(ErrorCode.SCENARIO_HAS_SERVICES, `Scenario '${workflowName}' destructures services: ${disallowed.join(', ')}. ` +
|
|
584
|
+
`Scenarios may only use 'logger'/'config' — drive everything else through ` +
|
|
585
|
+
`actor steps (workflow.do(step, rpc, data, { actor: actors.x })) so the flow ` +
|
|
586
|
+
`runs against the target environment.`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Walk every scenario step, wherever it is nested, and validate the two things
|
|
592
|
+
* that can only be known once both the workflow meta and the function meta
|
|
593
|
+
* exist:
|
|
594
|
+
*
|
|
595
|
+
* - the step target must be a static string literal (PKU678) — otherwise it
|
|
596
|
+
* can't be bundled, typed or drawn;
|
|
597
|
+
* - a `browser: true` step must be given an actor (PKU677), since the browser
|
|
598
|
+
* context signs in as that persona.
|
|
599
|
+
*
|
|
600
|
+
* It also marks the step's function as used, so `scenario run` boots the
|
|
601
|
+
* singletons the step actually needs.
|
|
602
|
+
*/
|
|
603
|
+
export function validateScenarioSteps(logger, state) {
|
|
604
|
+
const visit = (workflowName, steps) => {
|
|
605
|
+
for (const step of steps) {
|
|
606
|
+
if (step.type === 'scenarioStep') {
|
|
607
|
+
if (step.stepFunc === DYNAMIC_SCENARIO_STEP_TARGET) {
|
|
608
|
+
logger.critical(ErrorCode.SCENARIO_STEP_TARGET_NOT_STATIC, `Scenario '${workflowName}' calls step '${step.stepName}' with a target that isn't a string literal. ` +
|
|
609
|
+
`A step target must be statically resolvable so it can be bundled, typed and drawn — ` +
|
|
610
|
+
`loop over data sets instead of computing the step name.`);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
state.serviceAggregation.usedFunctions.add(step.stepFunc);
|
|
614
|
+
const stepMeta = state.functions.meta[step.stepFunc];
|
|
615
|
+
if (stepMeta?.scenarioStepBrowser && !step.actor) {
|
|
616
|
+
logger.critical(ErrorCode.SCENARIO_BROWSER_STEP_NEEDS_ACTOR, `Scenario '${workflowName}' calls browser step '${step.stepFunc}' without an actor. ` +
|
|
617
|
+
`Pass { actor: actors.<name> } so the browser context signs in as that persona.`);
|
|
618
|
+
}
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
if (step.type === 'branch') {
|
|
622
|
+
for (const branch of step.branches)
|
|
623
|
+
visit(workflowName, branch.steps);
|
|
624
|
+
if (step.elseSteps)
|
|
625
|
+
visit(workflowName, step.elseSteps);
|
|
626
|
+
}
|
|
627
|
+
else if (step.type === 'switch') {
|
|
628
|
+
for (const c of step.cases ?? []) {
|
|
629
|
+
if (c.steps)
|
|
630
|
+
visit(workflowName, c.steps);
|
|
631
|
+
}
|
|
632
|
+
if (step.defaultSteps)
|
|
633
|
+
visit(workflowName, step.defaultSteps);
|
|
634
|
+
}
|
|
635
|
+
else if (step.type === 'fanout' && step.body) {
|
|
636
|
+
visit(workflowName, step.body);
|
|
637
|
+
}
|
|
638
|
+
else if (step.type === 'parallel' && step.children) {
|
|
639
|
+
visit(workflowName, step.children);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
for (const [workflowName, meta] of Object.entries(state.workflows.meta)) {
|
|
644
|
+
visit(workflowName, meta.steps ?? []);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
555
647
|
/**
|
|
556
648
|
* A `pikkuWorkflowGraph` node that references the `graph:` namespace (e.g.
|
|
557
649
|
* `graph:editFields`) needs @pikku/addon-graph wired — otherwise the RPC never
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JSONValue } from '@pikku/core';
|
|
2
|
-
import type { InspectorDiagnostic, InspectorState } from '../types.js';
|
|
2
|
+
import type { InspectorDiagnostic, InspectorFeature, InspectorState } from '../types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Serializable version of InspectorState that can be saved to JSON
|
|
5
5
|
* Omits typesLookup (contains non-serializable ts.Type objects) and converts Maps/Sets to arrays
|
|
@@ -160,6 +160,7 @@ export interface SerializableInspectorState {
|
|
|
160
160
|
path: string;
|
|
161
161
|
exportedName: string;
|
|
162
162
|
}]>;
|
|
163
|
+
featureFiles: Array<[string, InspectorFeature]>;
|
|
163
164
|
invokedWorkflows: string[];
|
|
164
165
|
};
|
|
165
166
|
rpc: {
|