@pikku/inspector 0.12.103 → 0.12.107

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/add/add-auth.d.ts +1 -9
  2. package/dist/add/add-auth.js +64 -17
  3. package/dist/add/add-cli.js +25 -11
  4. package/dist/add/add-functions.js +35 -39
  5. package/dist/add/add-keyed-wiring.js +22 -2
  6. package/dist/add/add-middleware.js +17 -16
  7. package/dist/add/add-permission.d.ts +0 -3
  8. package/dist/add/add-permission.js +213 -22
  9. package/dist/add/add-scope.d.ts +1 -7
  10. package/dist/add/add-scope.js +1 -21
  11. package/dist/add/add-virtual-users.d.ts +13 -0
  12. package/dist/add/{add-personas.js → add-virtual-users.js} +33 -33
  13. package/dist/add/add-workflow.js +14 -10
  14. package/dist/add/check-indirect-registrations.d.ts +9 -0
  15. package/dist/add/check-indirect-registrations.js +114 -0
  16. package/dist/error-codes.d.ts +5 -3
  17. package/dist/error-codes.js +5 -3
  18. package/dist/finalize.d.ts +10 -0
  19. package/dist/finalize.js +102 -0
  20. package/dist/index.d.ts +8 -6
  21. package/dist/index.js +6 -5
  22. package/dist/initial-state.d.ts +7 -0
  23. package/dist/initial-state.js +216 -0
  24. package/dist/inspector.d.ts +6 -4
  25. package/dist/inspector.js +28 -300
  26. package/dist/lite.d.ts +26 -0
  27. package/dist/lite.js +25 -0
  28. package/dist/source-file-cache.d.ts +11 -0
  29. package/dist/source-file-cache.js +34 -10
  30. package/dist/types-map.js +2 -1
  31. package/dist/types.d.ts +35 -3
  32. package/dist/utils/apply-declared-scopes.d.ts +8 -0
  33. package/dist/utils/apply-declared-scopes.js +21 -0
  34. package/dist/utils/canonical-type-string.d.ts +14 -0
  35. package/dist/utils/canonical-type-string.js +162 -0
  36. package/dist/utils/context-based-id.d.ts +7 -0
  37. package/dist/utils/context-based-id.js +9 -0
  38. package/dist/utils/custom-types-generator.d.ts +15 -1
  39. package/dist/utils/custom-types-generator.js +17 -5
  40. package/dist/utils/ensure-function-metadata.d.ts +0 -14
  41. package/dist/utils/ensure-function-metadata.js +4 -1
  42. package/dist/utils/extract-function-name.d.ts +1 -1
  43. package/dist/utils/extract-function-name.js +2 -4
  44. package/dist/utils/filter-inspector-state.js +31 -1
  45. package/dist/utils/get-property-value.d.ts +5 -0
  46. package/dist/utils/get-property-value.js +15 -1
  47. package/dist/utils/inspector-constants.d.ts +16 -0
  48. package/dist/utils/inspector-constants.js +16 -0
  49. package/dist/utils/load-addon-functions-meta.js +1 -0
  50. package/dist/utils/post-process.d.ts +35 -36
  51. package/dist/utils/post-process.js +218 -193
  52. package/dist/utils/resolve-callee-name.d.ts +27 -0
  53. package/dist/utils/resolve-callee-name.js +147 -0
  54. package/dist/utils/resolve-service-types.d.ts +8 -0
  55. package/dist/utils/resolve-service-types.js +21 -0
  56. package/dist/utils/schema-generator.js +29 -2
  57. package/dist/utils/serialize-inspector-state.d.ts +9 -3
  58. package/dist/utils/serialize-inspector-state.js +22 -7
  59. package/dist/utils/single-declaration.d.ts +3 -3
  60. package/dist/utils/single-declaration.js +3 -3
  61. package/dist/utils/validate-single-step-workflows.d.ts +3 -0
  62. package/dist/utils/validate-single-step-workflows.js +32 -0
  63. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +3 -3
  64. package/dist/utils/workflow/dsl/patterns.d.ts +4 -11
  65. package/dist/utils/workflow/dsl/patterns.js +5 -11
  66. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +6 -6
  67. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +5 -5
  68. package/dist/visit.js +4 -2
  69. package/package.json +25 -9
  70. package/CHANGELOG.md +0 -4318
  71. package/OPTIMIZATION-PLAN.md +0 -201
  72. package/dist/add/add-personas.d.ts +0 -13
  73. package/run-tests.sh +0 -68
  74. package/src/utils/test-data/inspector-state.json +0 -1687
@@ -0,0 +1,27 @@
1
+ import * as ts from 'typescript';
2
+ /** The helpers that define or register middleware. */
3
+ export declare const MIDDLEWARE_HELPERS: ReadonlySet<string>;
4
+ /** The helpers that define or register permissions. */
5
+ export declare const PERMISSION_HELPERS: ReadonlySet<string>;
6
+ type NameMatcher = ReadonlySet<string> | ((name: string) => boolean);
7
+ export declare const isNamespaceReceiver: (receiver: ts.Expression, checker: ts.TypeChecker) => boolean;
8
+ /**
9
+ * Every name a callee answers to, from its spelling at the call site to the
10
+ * declaration it resolves to: through import and re-export aliases, members
11
+ * of a namespace import, and `const` bindings of another name.
12
+ */
13
+ export declare const calleeNames: (expression: ts.Expression, checker: ts.TypeChecker) => readonly string[];
14
+ /**
15
+ * The pikku helper a call invokes, identified by what its callee resolves to
16
+ * rather than how it is spelled, so `import { addTagMiddleware as guard }` and
17
+ * `mw.addTagMiddleware(...)` are seen for what they are.
18
+ *
19
+ * The deepest known name wins, because it is the declaration's own: an alias
20
+ * that happens to look like another helper (`pikkuFunc as pikkuOpenFunc`) is
21
+ * still the helper it imports. A call inside the body of a helper's own
22
+ * declaration — the generated `#pikku` wrappers forwarding to `@pikku/core` —
23
+ * is the helper's implementation, not a use of it, and keeps the meaning its
24
+ * literal spelling gives it.
25
+ */
26
+ export declare const resolveCalleeName: (call: ts.CallExpression, checker: ts.TypeChecker, known: NameMatcher) => string | undefined;
27
+ export {};
@@ -0,0 +1,147 @@
1
+ import * as ts from 'typescript';
2
+ const MAX_DEPTH = 8;
3
+ /** The helpers that define or register middleware. */
4
+ export const MIDDLEWARE_HELPERS = new Set([
5
+ 'pikkuMiddleware',
6
+ 'pikkuMiddlewareFactory',
7
+ 'addTagMiddleware',
8
+ 'addGlobalMiddleware',
9
+ 'addHTTPMiddleware',
10
+ 'pikkuChannelMiddleware',
11
+ 'pikkuChannelMiddlewareFactory',
12
+ 'pikkuAgentMiddleware',
13
+ 'addChannelMiddleware',
14
+ ]);
15
+ /** The helpers that define or register permissions. */
16
+ export const PERMISSION_HELPERS = new Set([
17
+ 'pikkuPermission',
18
+ 'pikkuAuth',
19
+ 'pikkuPermissionFactory',
20
+ 'addGlobalPermission',
21
+ ]);
22
+ const matcher = (known) => typeof known === 'function' ? known : (name) => known.has(name);
23
+ const namesCache = new WeakMap();
24
+ const unwrap = (expression) => {
25
+ let current = expression;
26
+ while (ts.isParenthesizedExpression(current) ||
27
+ ts.isAsExpression(current) ||
28
+ ts.isSatisfiesExpression(current) ||
29
+ ts.isNonNullExpression(current) ||
30
+ ts.isTypeAssertionExpression(current)) {
31
+ current = current.expression;
32
+ }
33
+ return current;
34
+ };
35
+ export const isNamespaceReceiver = (receiver, checker) => {
36
+ if (!ts.isIdentifier(receiver))
37
+ return false;
38
+ const symbol = checker.getSymbolAtLocation(receiver);
39
+ if (!symbol)
40
+ return false;
41
+ if (symbol.declarations?.some(ts.isNamespaceImport))
42
+ return true;
43
+ const target = symbol.flags & ts.SymbolFlags.Alias
44
+ ? checker.getAliasedSymbol(symbol)
45
+ : symbol;
46
+ return (target.flags & ts.SymbolFlags.ValueModule) !== 0;
47
+ };
48
+ const followSymbol = (symbol, checker, names, depth) => {
49
+ if (!symbol || depth > MAX_DEPTH)
50
+ return;
51
+ let target = symbol;
52
+ if (symbol.flags & ts.SymbolFlags.Alias) {
53
+ const declaration = symbol.declarations?.[0];
54
+ if (declaration &&
55
+ (ts.isImportSpecifier(declaration) || ts.isExportSpecifier(declaration))) {
56
+ names.push((declaration.propertyName ?? declaration.name).text);
57
+ }
58
+ target = checker.getAliasedSymbol(symbol);
59
+ // An import from a module the program cannot resolve: the specifier's own
60
+ // exported name is the only identity there is.
61
+ if (!target.declarations?.length)
62
+ return;
63
+ names.push(target.getName());
64
+ }
65
+ const declaration = target.valueDeclaration;
66
+ if (declaration &&
67
+ ts.isVariableDeclaration(declaration) &&
68
+ declaration.initializer &&
69
+ ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) {
70
+ collectNames(declaration.initializer, checker, names, depth + 1);
71
+ }
72
+ };
73
+ const collectNames = (expression, checker, names, depth) => {
74
+ const callee = unwrap(expression);
75
+ if (ts.isIdentifier(callee)) {
76
+ names.push(callee.text);
77
+ followSymbol(checker.getSymbolAtLocation(callee), checker, names, depth);
78
+ }
79
+ else if (ts.isPropertyAccessExpression(callee) &&
80
+ isNamespaceReceiver(unwrap(callee.expression), checker)) {
81
+ names.push(callee.name.text);
82
+ followSymbol(checker.getSymbolAtLocation(callee.name), checker, names, depth);
83
+ }
84
+ };
85
+ /**
86
+ * Every name a callee answers to, from its spelling at the call site to the
87
+ * declaration it resolves to: through import and re-export aliases, members
88
+ * of a namespace import, and `const` bindings of another name.
89
+ */
90
+ export const calleeNames = (expression, checker) => {
91
+ let cache = namesCache.get(checker);
92
+ if (!cache) {
93
+ cache = new WeakMap();
94
+ namesCache.set(checker, cache);
95
+ }
96
+ const cached = cache.get(expression);
97
+ if (cached)
98
+ return cached;
99
+ const names = [];
100
+ collectNames(expression, checker, names, 0);
101
+ cache.set(expression, names);
102
+ return names;
103
+ };
104
+ const isInsideDeclarationOf = (node, isKnown) => {
105
+ for (let current = node.parent; current; current = current.parent) {
106
+ if ((ts.isVariableDeclaration(current) ||
107
+ ts.isFunctionDeclaration(current)) &&
108
+ current.name &&
109
+ ts.isIdentifier(current.name) &&
110
+ isKnown(current.name.text)) {
111
+ return true;
112
+ }
113
+ }
114
+ return false;
115
+ };
116
+ /**
117
+ * The pikku helper a call invokes, identified by what its callee resolves to
118
+ * rather than how it is spelled, so `import { addTagMiddleware as guard }` and
119
+ * `mw.addTagMiddleware(...)` are seen for what they are.
120
+ *
121
+ * The deepest known name wins, because it is the declaration's own: an alias
122
+ * that happens to look like another helper (`pikkuFunc as pikkuOpenFunc`) is
123
+ * still the helper it imports. A call inside the body of a helper's own
124
+ * declaration — the generated `#pikku` wrappers forwarding to `@pikku/core` —
125
+ * is the helper's implementation, not a use of it, and keeps the meaning its
126
+ * literal spelling gives it.
127
+ */
128
+ export const resolveCalleeName = (call, checker, known) => {
129
+ const isKnown = matcher(known);
130
+ const names = calleeNames(call.expression, checker);
131
+ let resolved;
132
+ for (let i = names.length - 1; i >= 0; i--) {
133
+ if (isKnown(names[i])) {
134
+ resolved = names[i];
135
+ break;
136
+ }
137
+ }
138
+ if (!resolved)
139
+ return undefined;
140
+ const spelled = ts.isIdentifier(call.expression)
141
+ ? call.expression.text
142
+ : undefined;
143
+ if (resolved !== spelled && isInsideDeclarationOf(call, isKnown)) {
144
+ return spelled !== undefined && isKnown(spelled) ? spelled : undefined;
145
+ }
146
+ return resolved;
147
+ };
@@ -0,0 +1,8 @@
1
+ import type ts from 'typescript';
2
+ import type { InspectorServiceTypes, InspectorState } from '../types.js';
3
+ /**
4
+ * Reads the checker's view of the SingletonServices and Services types into
5
+ * plain names, so the shared finalize phase can aggregate services and refuse
6
+ * secret aliases without a `ts.Type` in hand.
7
+ */
8
+ export declare const resolveServiceTypes: (state: Pick<InspectorState, "typesLookup" | "singletonServicesTypeImportMap" | "wireServicesTypeImportMap">, checker: ts.TypeChecker) => InspectorServiceTypes;
@@ -0,0 +1,21 @@
1
+ import { extractTypeKeys } from './type-utils.js';
2
+ import { resolveCoreType } from './resolve-core-type.js';
3
+ import { findSecretAliasServices } from './secret-alias-services.js';
4
+ /**
5
+ * Reads the checker's view of the SingletonServices and Services types into
6
+ * plain names, so the shared finalize phase can aggregate services and refuse
7
+ * secret aliases without a `ts.Type` in hand.
8
+ */
9
+ export const resolveServiceTypes = (state, checker) => {
10
+ const singletonServicesType = resolveCoreType(state.typesLookup, state.singletonServicesTypeImportMap);
11
+ const servicesType = resolveCoreType(state.typesLookup, state.wireServicesTypeImportMap);
12
+ return {
13
+ singletonServices: singletonServicesType
14
+ ? extractTypeKeys(singletonServicesType).sort()
15
+ : null,
16
+ services: servicesType ? extractTypeKeys(servicesType) : null,
17
+ secretAliases: singletonServicesType
18
+ ? findSecretAliasServices(singletonServicesType, checker)
19
+ : [],
20
+ };
21
+ };
@@ -1,13 +1,14 @@
1
1
  import * as ts from 'typescript';
2
2
  import { createHash } from 'crypto';
3
3
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from 'fs';
4
- import { dirname, join, resolve } from 'path';
4
+ import { basename, dirname, join, resolve } from 'path';
5
5
  import { createFormatter, createParser, DEFAULT_CONFIG, RootlessError, SchemaGenerator, } from 'ts-json-schema-generator';
6
6
  import { register } from 'tsx/esm/api';
7
7
  import * as z from 'zod';
8
8
  import { findZodTransform } from './find-zod-transform.js';
9
9
  import { zodToTypeText } from './zod-to-type-text.js';
10
10
  import { ErrorCode } from '../error-codes.js';
11
+ import { getCachedSourceFile } from '../source-file-cache.js';
11
12
  import { generateCustomTypes } from './custom-types-generator.js';
12
13
  const PRIMITIVE_TYPES = new Set([
13
14
  'boolean',
@@ -212,6 +213,21 @@ const createIndexedGenerator = (program, config) => {
212
213
  const completedConfig = { ...DEFAULT_CONFIG, ...config };
213
214
  return new IndexedSchemaGenerator(program, createParser(program, completedConfig), createFormatter(completedConfig), completedConfig);
214
215
  };
216
+ /**
217
+ * Parse at the inspector's target so the schema program reuses the inspector's
218
+ * SourceFiles instead of re-parsing the project; `target` is part of the
219
+ * parse cache key. The lib the project's own target implies is pinned, so type
220
+ * resolution is unchanged.
221
+ */
222
+ const parseAtInspectorTarget = (options) => {
223
+ if (options.target === ts.ScriptTarget.ESNext)
224
+ return options;
225
+ return {
226
+ ...options,
227
+ target: ts.ScriptTarget.ESNext,
228
+ lib: options.lib ?? [basename(ts.getDefaultLibFilePath(options))],
229
+ };
230
+ };
215
231
  function createProgramWithVirtualFile(logger, tsconfig, virtualFilePath, virtualFileContent) {
216
232
  const configPath = resolve(tsconfig);
217
233
  // Cache the parsed tsconfig — it doesn't change between runs
@@ -219,6 +235,7 @@ function createProgramWithVirtualFile(logger, tsconfig, virtualFilePath, virtual
219
235
  const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
220
236
  const basePath = dirname(configPath);
221
237
  cachedParsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, basePath);
238
+ cachedParsedConfig.options = parseAtInspectorTarget(cachedParsedConfig.options);
222
239
  cachedTsconfigPath = configPath;
223
240
  cachedSchemaProgram = undefined;
224
241
  }
@@ -235,7 +252,17 @@ function createProgramWithVirtualFile(logger, tsconfig, virtualFilePath, virtual
235
252
  if (resolve(fileName) === resolvedVirtualPath) {
236
253
  return ts.createSourceFile(fileName, virtualFileContent, languageVersionOrOptions);
237
254
  }
238
- return defaultHost.getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
255
+ if (shouldCreateNewSourceFile) {
256
+ return defaultHost.getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
257
+ }
258
+ // Shares the inspector's parsed files rather than parsing the project a
259
+ // second time.
260
+ const text = defaultHost.readFile(fileName);
261
+ if (text === undefined)
262
+ return undefined;
263
+ return getCachedSourceFile(fileName, text, languageVersionOrOptions, {
264
+ populate: false,
265
+ }).file;
239
266
  },
240
267
  fileExists(fileName) {
241
268
  if (resolve(fileName) === resolvedVirtualPath)
@@ -60,6 +60,8 @@ export interface SerializableInspectorState {
60
60
  addonRequiredParentServices: string[];
61
61
  addonCreatedServices: string[];
62
62
  addonServicesFactorySeen: boolean;
63
+ analytics?: InspectorState['analytics'];
64
+ outgoingWebhooks?: InspectorState['outgoingWebhooks'];
63
65
  addonServerlessIncompatible: Array<[string, string[]]>;
64
66
  configFactories: Array<[
65
67
  string,
@@ -115,6 +117,7 @@ export interface SerializableInspectorState {
115
117
  exportedName: string;
116
118
  }]>;
117
119
  approvalDescriptions: InspectorState['functions']['approvalDescriptions'];
120
+ dynamicImportIds?: string[];
118
121
  };
119
122
  http: {
120
123
  metaInputTypes: Array<[
@@ -260,6 +263,7 @@ export interface SerializableInspectorState {
260
263
  scopes: {
261
264
  definitions: InspectorState['scopes']['definitions'];
262
265
  files: string[];
266
+ declared?: Array<[string, string]>;
263
267
  };
264
268
  systemRoles: {
265
269
  definitions: InspectorState['systemRoles']['definitions'];
@@ -269,8 +273,8 @@ export interface SerializableInspectorState {
269
273
  definitions: InspectorState['featureFlags']['definitions'];
270
274
  files: string[];
271
275
  };
272
- personas: {
273
- definitions: InspectorState['personas']['definitions'];
276
+ virtualUsers: {
277
+ definitions: InspectorState['virtualUsers']['definitions'];
274
278
  files: string[];
275
279
  };
276
280
  variables: {
@@ -301,6 +305,7 @@ export interface SerializableInspectorState {
301
305
  permissions: {
302
306
  definitions: InspectorState['permissions']['definitions'];
303
307
  instances: InspectorState['permissions']['instances'];
308
+ global?: InspectorState['permissions']['global'];
304
309
  };
305
310
  serviceAggregation: {
306
311
  requiredServices: string[];
@@ -310,6 +315,7 @@ export interface SerializableInspectorState {
310
315
  allSingletonServices: string[];
311
316
  allWireServices: string[];
312
317
  };
318
+ serviceTypes?: InspectorState['serviceTypes'];
313
319
  resolvedIOTypes: Record<string, {
314
320
  inputType: string;
315
321
  outputType: string;
@@ -328,7 +334,7 @@ export interface SerializableInspectorState {
328
334
  * Serializes InspectorState to a JSON-compatible format
329
335
  * Omits typesLookup (not needed for filtering/generation) and converts Maps/Sets to arrays
330
336
  */
331
- export declare function serializeInspectorState(state: InspectorState): SerializableInspectorState;
337
+ export declare function serializeInspectorState(state: Omit<InspectorState, 'typesLookup'>): SerializableInspectorState;
332
338
  /**
333
339
  * Deserializes JSON data back to InspectorState
334
340
  * Creates a partial state suitable for filtering/generation (without typesLookup)
@@ -26,6 +26,8 @@ export function serializeInspectorState(state) {
26
26
  addonRequiredParentServices: state.addonRequiredParentServices,
27
27
  addonCreatedServices: state.addonCreatedServices,
28
28
  addonServicesFactorySeen: state.addonServicesFactorySeen,
29
+ analytics: state.analytics,
30
+ outgoingWebhooks: state.outgoingWebhooks,
29
31
  addonServerlessIncompatible: Array.from(state.addonServerlessIncompatible.entries()),
30
32
  configFactories: Array.from(state.configFactories.entries()),
31
33
  serverLifecycleFactories: Array.from(state.serverLifecycleFactories.entries()),
@@ -38,6 +40,7 @@ export function serializeInspectorState(state) {
38
40
  meta: state.functions.meta,
39
41
  files: Array.from(state.functions.files.entries()),
40
42
  approvalDescriptions: state.functions.approvalDescriptions,
43
+ dynamicImportIds: Array.from(state.functions.dynamicImportIds ?? []),
41
44
  },
42
45
  http: {
43
46
  metaInputTypes: Array.from(state.http.metaInputTypes.entries()),
@@ -131,6 +134,9 @@ export function serializeInspectorState(state) {
131
134
  scopes: {
132
135
  definitions: state.scopes.definitions,
133
136
  files: Array.from(state.scopes.files),
137
+ declared: state.scopes.declared
138
+ ? Array.from(state.scopes.declared.entries())
139
+ : undefined,
134
140
  },
135
141
  systemRoles: {
136
142
  definitions: state.systemRoles.definitions,
@@ -140,9 +146,9 @@ export function serializeInspectorState(state) {
140
146
  definitions: state.featureFlags.definitions,
141
147
  files: Array.from(state.featureFlags.files),
142
148
  },
143
- personas: {
144
- definitions: state.personas.definitions,
145
- files: Array.from(state.personas.files),
149
+ virtualUsers: {
150
+ definitions: state.virtualUsers.definitions,
151
+ files: Array.from(state.virtualUsers.files),
146
152
  },
147
153
  variables: {
148
154
  definitions: state.variables.definitions,
@@ -166,6 +172,7 @@ export function serializeInspectorState(state) {
166
172
  permissions: {
167
173
  definitions: state.permissions.definitions,
168
174
  instances: state.permissions.instances,
175
+ global: state.permissions.global,
169
176
  },
170
177
  serviceAggregation: {
171
178
  requiredServices: Array.from(state.serviceAggregation.requiredServices),
@@ -175,6 +182,7 @@ export function serializeInspectorState(state) {
175
182
  allSingletonServices: state.serviceAggregation.allSingletonServices,
176
183
  allWireServices: state.serviceAggregation.allWireServices,
177
184
  },
185
+ serviceTypes: state.serviceTypes,
178
186
  resolvedIOTypes: state.resolvedIOTypes,
179
187
  middlewareGroupsMeta: state.middlewareGroupsMeta,
180
188
  permissionsGroupsMeta: state.permissionsGroupsMeta,
@@ -212,6 +220,8 @@ export function deserializeInspectorState(data) {
212
220
  addonRequiredParentServices: data.addonRequiredParentServices || [],
213
221
  addonCreatedServices: data.addonCreatedServices || [],
214
222
  addonServicesFactorySeen: data.addonServicesFactorySeen ?? false,
223
+ analytics: data.analytics,
224
+ outgoingWebhooks: data.outgoingWebhooks,
215
225
  addonServerlessIncompatible: new Map(data.addonServerlessIncompatible || []),
216
226
  configFactories: new Map(data.configFactories),
217
227
  serverLifecycleFactories: new Map(data.serverLifecycleFactories),
@@ -227,7 +237,7 @@ export function deserializeInspectorState(data) {
227
237
  meta: data.functions.meta,
228
238
  files: new Map(data.functions.files),
229
239
  approvalDescriptions: data.functions.approvalDescriptions || {},
230
- dynamicImportIds: new Set(),
240
+ dynamicImportIds: new Set(data.functions.dynamicImportIds ?? []),
231
241
  },
232
242
  http: {
233
243
  metaInputTypes: new Map(data.http.metaInputTypes),
@@ -321,6 +331,9 @@ export function deserializeInspectorState(data) {
321
331
  scopes: {
322
332
  definitions: data.scopes?.definitions || [],
323
333
  files: new Set(data.scopes?.files || []),
334
+ declared: data.scopes?.declared
335
+ ? new Map(data.scopes.declared)
336
+ : undefined,
324
337
  },
325
338
  systemRoles: {
326
339
  definitions: data.systemRoles?.definitions || [],
@@ -330,9 +343,9 @@ export function deserializeInspectorState(data) {
330
343
  definitions: data.featureFlags?.definitions || [],
331
344
  files: new Set(data.featureFlags?.files || []),
332
345
  },
333
- personas: {
334
- definitions: data.personas?.definitions || [],
335
- files: new Set(data.personas?.files || []),
346
+ virtualUsers: {
347
+ definitions: data.virtualUsers?.definitions || [],
348
+ files: new Set(data.virtualUsers?.files || []),
336
349
  },
337
350
  variables: {
338
351
  definitions: data.variables?.definitions || [],
@@ -356,6 +369,7 @@ export function deserializeInspectorState(data) {
356
369
  permissions: {
357
370
  definitions: data.permissions.definitions,
358
371
  instances: data.permissions.instances || {},
372
+ global: data.permissions.global || [],
359
373
  },
360
374
  serviceAggregation: {
361
375
  requiredServices: new Set(data.serviceAggregation.requiredServices),
@@ -365,6 +379,7 @@ export function deserializeInspectorState(data) {
365
379
  allSingletonServices: data.serviceAggregation.allSingletonServices,
366
380
  allWireServices: data.serviceAggregation.allWireServices,
367
381
  },
382
+ serviceTypes: data.serviceTypes,
368
383
  resolvedIOTypes: data.resolvedIOTypes || {},
369
384
  middlewareGroupsMeta: data.middlewareGroupsMeta || {
370
385
  definitions: {},
@@ -3,18 +3,18 @@ import type { InspectorLogger } from '../types.js';
3
3
  /**
4
4
  * Claims the one call site a single-declaration construct is allowed.
5
5
  *
6
- * `definePersonas` takes a keyed object, so one call already declares as many
6
+ * `defineVirtualUsers` takes a keyed object, so one call already declares as many
7
7
  * entries as you like. Letting the call itself repeat buys nothing and costs
8
8
  * the thing that matters: a single place to read the declared set from, and a
9
9
  * single place to add to it. The same rule `pikkuBetterAuth` has had all along.
10
10
  *
11
11
  * A second call in the SAME file is refused too — "the file" is not an answer
12
- * to "where do I add a persona?" when the file holds two calls, so allowing it
12
+ * to "where do I add a virtual user?" when the file holds two calls, so allowing it
13
13
  * would keep the ambiguity the rule exists to remove.
14
14
  *
15
15
  * A generated file is exempt, and never claims the slot either. The rule exists
16
16
  * to name the one place a person adds to, and nobody adds to a file the CLI
17
- * rewrites on the next run — `pikku-personas.gen.ts` would otherwise make every
17
+ * rewrites on the next run — `pikku-virtual-users.gen.ts` would otherwise make every
18
18
  * project that scaffolds one unbuildable, and take the app's own declaration
19
19
  * down with it.
20
20
  *
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * Claims the one call site a single-declaration construct is allowed.
3
3
  *
4
- * `definePersonas` takes a keyed object, so one call already declares as many
4
+ * `defineVirtualUsers` takes a keyed object, so one call already declares as many
5
5
  * entries as you like. Letting the call itself repeat buys nothing and costs
6
6
  * the thing that matters: a single place to read the declared set from, and a
7
7
  * single place to add to it. The same rule `pikkuBetterAuth` has had all along.
8
8
  *
9
9
  * A second call in the SAME file is refused too — "the file" is not an answer
10
- * to "where do I add a persona?" when the file holds two calls, so allowing it
10
+ * to "where do I add a virtual user?" when the file holds two calls, so allowing it
11
11
  * would keep the ambiguity the rule exists to remove.
12
12
  *
13
13
  * A generated file is exempt, and never claims the slot either. The rule exists
14
14
  * to name the one place a person adds to, and nobody adds to a file the CLI
15
- * rewrites on the next run — `pikku-personas.gen.ts` would otherwise make every
15
+ * rewrites on the next run — `pikku-virtual-users.gen.ts` would otherwise make every
16
16
  * project that scaffolds one unbuildable, and take the app's own declaration
17
17
  * down with it.
18
18
  *
@@ -0,0 +1,3 @@
1
+ import type { InspectorLogger, InspectorState } from '../types.js';
2
+ /** Warns about a workflow with at most one RPC step and nothing to wait on or fan out: it is a function or a queue job wearing a workflow's cost. */
3
+ export declare function validateSingleStepWorkflows(logger: InspectorLogger, state: InspectorState): void;
@@ -0,0 +1,32 @@
1
+ import { ErrorCode } from '../error-codes.js';
2
+ const DURABLE_FLOWS = new Set([
3
+ 'sleep',
4
+ 'suspend',
5
+ 'approval',
6
+ 'parallel',
7
+ 'fanout',
8
+ ]);
9
+ /** Warns about a workflow with at most one RPC step and nothing to wait on or fan out: it is a function or a queue job wearing a workflow's cost. */
10
+ export function validateSingleStepWorkflows(logger, state) {
11
+ for (const [name, graph] of Object.entries(state.workflows.graphMeta)) {
12
+ if (graph.source === 'scenario')
13
+ continue;
14
+ const file = state.workflows.files.get(graph.pikkuFuncId)?.path ??
15
+ state.workflows.graphFiles.get(name)?.path;
16
+ if (!file)
17
+ continue;
18
+ const nodes = Object.values(graph.nodes);
19
+ const rpcSteps = nodes.filter((n) => 'rpcName' in n && n.rpcName);
20
+ if (rpcSteps.length > 1)
21
+ continue;
22
+ if (nodes.some((n) => 'flow' in n && DURABLE_FLOWS.has(n.flow)))
23
+ continue;
24
+ logger.diagnostic({
25
+ severity: 'warn',
26
+ code: ErrorCode.SINGLE_STEP_WORKFLOW,
27
+ message: `Workflow '${name}' (${file}) runs ${rpcSteps.length === 0 ? 'no RPC step' : 'a single RPC step'} ` +
28
+ `and never sleeps, suspends or fans out, so it buys nothing a workflow is for. ` +
29
+ `Call the function directly, or enqueue it on a queue worker if it must survive a restart.`,
30
+ });
31
+ }
32
+ }
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, isScenarioStepCall, isScenarioExpectationCall, getScenarioStepPhase, DYNAMIC_SCENARIO_STEP_TARGET, extractActorFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, isWorkflowMilestoneCall, isWorkflowApprovalCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
2
+ import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, isScenarioStepCall, isScenarioExpectationCall, getScenarioStepPhase, DYNAMIC_SCENARIO_STEP_TARGET, extractVirtualUserFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, isWorkflowMilestoneCall, isWorkflowApprovalCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
3
3
  import { validateNoDisallowedPatterns, validateAwaitedCalls, formatValidationErrors, } from './validation.js';
4
4
  import { extractStringLiteral, extractNumberLiteral, } from '../../extract-node-value.js';
5
5
  /**
@@ -571,7 +571,7 @@ function extractRpcStep(call, context, outputVar) {
571
571
  outputVar,
572
572
  inputs,
573
573
  options,
574
- actor: extractActorFromOptions(optionsArg),
574
+ virtualUser: extractVirtualUserFromOptions(optionsArg),
575
575
  expectEventually: expectEventually || undefined,
576
576
  };
577
577
  }
@@ -623,7 +623,7 @@ function extractScenarioStep(call, context, outputVar) {
623
623
  outputVar,
624
624
  inputs,
625
625
  options,
626
- actor: extractActorFromOptions(optionsArg),
626
+ virtualUser: extractVirtualUserFromOptions(optionsArg),
627
627
  };
628
628
  }
629
629
  catch (error) {
@@ -13,13 +13,7 @@ export declare function isWorkflowWireIdentifier(expr: ts.Expression): boolean;
13
13
  * (both are RPC steps; expectEventually is the polling variant used by scenarios)
14
14
  */
15
15
  export declare function isWorkflowDoCall(node: ts.CallExpression, checker: ts.TypeChecker): boolean;
16
- /**
17
- * Placeholder recorded when a scenario step's target isn't a static string
18
- * literal. The step is kept rather than dropped so post-processing can raise
19
- * PKU678 against it — a dropped step is the silent-failure mode this repo has
20
- * hit repeatedly on workflow graph round-trips.
21
- */
22
- export declare const DYNAMIC_SCENARIO_STEP_TARGET = "<dynamic>";
16
+ export { DYNAMIC_SCENARIO_STEP_TARGET } from '../../inspector-constants.js';
23
17
  declare const SCENARIO_STEP_PHASES: readonly ["given", "when", "then"];
24
18
  export type ScenarioStepPhaseName = (typeof SCENARIO_STEP_PHASES)[number];
25
19
  /**
@@ -44,11 +38,11 @@ export declare function isScenarioExpectationCall(node: ts.CallExpression): bool
44
38
  */
45
39
  export declare function isWorkflowExpectEventuallyCall(node: ts.CallExpression): boolean;
46
40
  /**
47
- * Extract the actor NAME from a step options object literal:
48
- * `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
41
+ * Extract the virtual user NAME from a step options object literal:
42
+ * `{ virtualUser: virtualUsers.x }` → 'x' (also `virtualUsers['x']`). Undefined when absent
49
43
  * or not statically resolvable.
50
44
  */
51
- export declare function extractActorFromOptions(optionsArg: ts.Expression | undefined): string | undefined;
45
+ export declare function extractVirtualUserFromOptions(optionsArg: ts.Expression | undefined): string | undefined;
52
46
  /**
53
47
  * Check if a call expression is workflow.sleep()
54
48
  */
@@ -117,4 +111,3 @@ export declare function isArrayType(type: ts.Type, checker: ts.TypeChecker): boo
117
111
  * Get the source text of a node (for condition expressions)
118
112
  */
119
113
  export declare function getSourceText(node: ts.Node): string;
120
- export {};
@@ -24,13 +24,7 @@ export function isWorkflowDoCall(node, checker) {
24
24
  propAccess.name.text === 'expectEventually') &&
25
25
  isWorkflowWireIdentifier(propAccess.expression));
26
26
  }
27
- /**
28
- * Placeholder recorded when a scenario step's target isn't a static string
29
- * literal. The step is kept rather than dropped so post-processing can raise
30
- * PKU678 against it — a dropped step is the silent-failure mode this repo has
31
- * hit repeatedly on workflow graph round-trips.
32
- */
33
- export const DYNAMIC_SCENARIO_STEP_TARGET = '<dynamic>';
27
+ export { DYNAMIC_SCENARIO_STEP_TARGET } from '../../inspector-constants.js';
34
28
  const SCENARIO_STEP_PHASES = ['given', 'when', 'then'];
35
29
  /**
36
30
  * Check if a call expression is scenario.given()/when()/then() — a declared
@@ -88,17 +82,17 @@ export function isWorkflowExpectEventuallyCall(node) {
88
82
  isWorkflowWireIdentifier(node.expression.expression));
89
83
  }
90
84
  /**
91
- * Extract the actor NAME from a step options object literal:
92
- * `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
85
+ * Extract the virtual user NAME from a step options object literal:
86
+ * `{ virtualUser: virtualUsers.x }` → 'x' (also `virtualUsers['x']`). Undefined when absent
93
87
  * or not statically resolvable.
94
88
  */
95
- export function extractActorFromOptions(optionsArg) {
89
+ export function extractVirtualUserFromOptions(optionsArg) {
96
90
  if (!optionsArg || !ts.isObjectLiteralExpression(optionsArg))
97
91
  return;
98
92
  for (const prop of optionsArg.properties) {
99
93
  if (ts.isPropertyAssignment(prop) &&
100
94
  ts.isIdentifier(prop.name) &&
101
- prop.name.text === 'actor') {
95
+ prop.name.text === 'virtualUser') {
102
96
  const init = prop.initializer;
103
97
  if (ts.isPropertyAccessExpression(init)) {
104
98
  return init.name.text;