@pikku/inspector 0.12.92 → 0.12.95

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 CHANGED
@@ -1,3 +1,34 @@
1
+ ## 0.12.95
2
+
3
+ ### Patch Changes
4
+
5
+ - 6ecf80e: Stop the services-destructure lint (PKU410) firing on the stub a wiring registers for its function. Since inline functions carry a source file, a queue worker wired to a named function failed the build with a critical PKU410 even though that function destructures its services.
6
+ - 2612a67: The services-destructure lint also skips the `helper` wiring functions `ensureFunctionMetadata` synthesizes, alongside the `inline` ones it already skips, and a test pins that an inline wiring function that records a source file is not flagged.
7
+
8
+ ## 0.12.94
9
+
10
+ ### Patch Changes
11
+
12
+ - 4323bcf: Inline wiring functions now record their source file, so `pikku release` leaves scaffold-injected ones out of the API surface.
13
+
14
+ ## 0.12.93
15
+
16
+ ### Patch Changes
17
+
18
+ - 658f047: A function whose services are all singletons no longer builds wire services, so an addon's credential-bound wire factory does not run for, or fail, a webhook `receive` step that needs no connection. The inspector decides this from the `Services` type and records it as `singletonServicesOnly` on the function's runtime meta; the runner only reads the flag.
19
+ - 658f047: Webhook signing secrets live in the credential store. `WebhookSigningSecret.fromCredential(provider, credentialService, name)` reads the secret per delivery via `load()`, so a `setup` step (or a handshake) that stores a new one with `credentialService.set` takes effect without a deploy.
20
+
21
+ Breaking: `setup` no longer returns `secret`, `wireTriggerWebhookSource` no longer takes `secret`, lifecycle outcomes drop `secretName`/`secret`, and `pikku webhooks setup` drops `--secretsOut`.
22
+
23
+ - 658f047: A webhook source's `method` may be a list, mounting one route per method, for providers that verify the URL with a GET and deliver events with a POST (WhatsApp, Strava, Onfleet, Mailchimp), or a HEAD (Trello, Mandrill, SurveyMonkey).
24
+ - Updated dependencies [658f047]
25
+ - Updated dependencies [de63ab2]
26
+ - Updated dependencies [658f047]
27
+ - Updated dependencies [658f047]
28
+ - Updated dependencies [658f047]
29
+ - Updated dependencies [658f047]
30
+ - @pikku/core@0.12.128
31
+
1
32
  ## 0.12.92
2
33
 
3
34
  ### Patch Changes
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import { getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
2
+ import { getArrayPropertyValue, getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
3
3
  import { extractFunctionName, makeContextBasedId, } from '../utils/extract-function-name.js';
4
4
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
5
5
  import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js';
@@ -114,9 +114,10 @@ const addWireTriggerWebhookSource = (logger, node, checker, state, firstArg) =>
114
114
  logger.critical(ErrorCode.MISSING_NAME, `wireTriggerWebhookSource in ${node.getSourceFile().fileName} needs 'name' as a string literal.`);
115
115
  return;
116
116
  }
117
- const method = getPropertyValue(obj, 'method') ?? 'post';
117
+ const method = getArrayPropertyValue(obj, 'method') ??
118
+ getPropertyValue(obj, 'method') ??
119
+ 'post';
118
120
  const route = getPropertyValue(obj, 'route') ?? `/webhooks/${name}`;
119
- let secret = getPropertyValue(obj, 'secret');
120
121
  const eventsInitializer = getPropertyAssignmentInitializer(obj, 'events', false, checker);
121
122
  const events = eventsInitializer
122
123
  ? checker
@@ -151,23 +152,11 @@ const addWireTriggerWebhookSource = (logger, node, checker, state, firstArg) =>
151
152
  steps[step] = pikkuFuncId;
152
153
  state.serviceAggregation.usedFunctions.add(pikkuFuncId);
153
154
  }
154
- // An addon's receive reads the secret by its own name; the app may have
155
- // renamed it for this instance.
156
- const namespace = steps.receive?.includes(':')
157
- ? steps.receive.slice(0, steps.receive.indexOf(':'))
158
- : null;
159
- const addon = namespace
160
- ? state.rpc.wireAddonDeclarations.get(namespace)
161
- : undefined;
162
- if (secret && addon?.secretOverrides?.[secret]) {
163
- secret = addon.secretOverrides[secret];
164
- }
165
155
  const meta = {
166
156
  name,
167
157
  method: method,
168
158
  route,
169
159
  events,
170
- ...(secret ? { secret } : {}),
171
160
  ...steps,
172
161
  };
173
162
  state.triggers.webhookSourceMeta[name] = meta;
@@ -176,6 +176,9 @@ export function ensureFunctionMetadata(state, pikkuFuncId, fallbackName, funcIni
176
176
  };
177
177
  }
178
178
  const meta = state.functions.meta[pikkuFuncId];
179
+ if (!meta.sourceFile && funcInitializer) {
180
+ meta.sourceFile = funcInitializer.getSourceFile().fileName;
181
+ }
179
182
  if (funcInitializer && checker) {
180
183
  let pikkuFuncCall = null;
181
184
  if (ts.isCallExpression(funcInitializer)) {
@@ -1,7 +1,7 @@
1
- import type { InspectorState, InspectorLogger, InspectorOptions } from '../types.js';
2
- import type { PermissionMetadata } from '@pikku/core/function';
3
- import type { MiddlewareMetadata } from '@pikku/core/middleware';
4
- import type ts from 'typescript';
1
+ import type { InspectorState, InspectorLogger, InspectorOptions } from "../types.js";
2
+ import type { PermissionMetadata } from "@pikku/core/function";
3
+ import type { MiddlewareMetadata } from "@pikku/core/middleware";
4
+ import type ts from "typescript";
5
5
  /**
6
6
  * Stamp the inspected authorize/callbacks service set onto the generated auth
7
7
  * handler's function meta.
@@ -15,7 +15,7 @@ import type ts from 'typescript';
15
15
  * onto the handler meta. Re-derived every inspect and ordered BEFORE
16
16
  * `aggregateRequiredServices` so it flows into `requiredServices`.
17
17
  */
18
- export declare function stampAuthHandlerServices(state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
18
+ export declare function stampAuthHandlerServices(state: InspectorState | Omit<InspectorState, "typesLookup">): void;
19
19
  /**
20
20
  * Helper to extract wire-level middleware/permission names from metadata.
21
21
  * Only extracts type:'wire' variants (individual middleware/permissions).
@@ -29,10 +29,10 @@ export declare function extractWireNames(list?: Array<MiddlewareMetadata | Permi
29
29
  * Note: usedFunctions, usedMiddleware, and usedPermissions are tracked directly
30
30
  * in the add-* methods during AST traversal for efficiency.
31
31
  */
32
- export declare function aggregateRequiredServices(state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
33
- export declare function validateSecretOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
34
- export declare function validateCredentialOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
35
- export declare function validateVariableOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
32
+ export declare function aggregateRequiredServices(state: InspectorState | Omit<InspectorState, "typesLookup">): void;
33
+ export declare function validateSecretOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
34
+ export declare function validateCredentialOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
35
+ export declare function validateVariableOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
36
36
  /**
37
37
  * Every wired addon must be installed, in the bucket matching how it is used.
38
38
  *
@@ -56,14 +56,14 @@ export declare function validateVariableOverrides(logger: InspectorLogger, state
56
56
  * up in `dependencies`. Asking the resolver is the only question that matches
57
57
  * what the loader will actually do.
58
58
  */
59
- export declare function validateRemoteAddonDependencies(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
59
+ export declare function validateRemoteAddonDependencies(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
60
60
  /**
61
61
  * The auth a `wireRemoteAddon` consumer binds must reference a slot the consumer
62
62
  * actually declares: a `credentialId` must be a wired credential, a `secretId`
63
63
  * a wired secret. A custom `resolve()` and a public (omitted) surface are not
64
64
  * statically checkable and are left to runtime.
65
65
  */
66
- export declare function validateRemoteAddonAuth(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
66
+ export declare function validateRemoteAddonAuth(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
67
67
  export declare function computeResolvedIOTypes(state: InspectorState): void;
68
68
  export declare function computeMiddlewareGroupsMeta(state: InspectorState): void;
69
69
  export declare function computePermissionsGroupsMeta(state: InspectorState): void;
@@ -88,7 +88,7 @@ export declare function validateSchemaReferences(logger: InspectorLogger, state:
88
88
  * A model is either concrete (`provider/model`) or an alias, which must appear
89
89
  * in the `models` table from pikku.config.json.
90
90
  */
91
- export declare function validateAgentModels(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>, modelAliases?: Record<string, string>): void;
91
+ export declare function validateAgentModels(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">, modelAliases?: Record<string, string>): void;
92
92
  /**
93
93
  * Scenarios are pure stories of remote RPCs (same rule as client-side CLI
94
94
  * renderers): the func may only destructure logger/config — everything else
@@ -100,7 +100,7 @@ export declare function validateAgentModels(logger: InspectorLogger, state: Insp
100
100
  * `addFunctions` (`visitFunctions`) has populated `state.functions.meta` — so
101
101
  * the check read `undefined` there and never fired.
102
102
  */
103
- export declare function validateScenarioServices(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
103
+ export declare function validateScenarioServices(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
104
104
  /**
105
105
  * Does this step's prose open by naming the actor who is already its subject?
106
106
  *
@@ -128,7 +128,7 @@ export declare const proseOpensWithActor: (prose: string, actor: string) => bool
128
128
  * It also marks the step's function as used, so `scenario run` boots the
129
129
  * singletons the step actually needs.
130
130
  */
131
- export declare function validateScenarioSteps(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
131
+ export declare function validateScenarioSteps(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
132
132
  /**
133
133
  * Reports scenarios that no `pikkuFeature` lists.
134
134
  *
@@ -153,7 +153,7 @@ export declare function validateScenarioSteps(logger: InspectorLogger, state: In
153
153
  * the check reports it then and downgrades to a warn naming both sides, rather
154
154
  * than accusing a scenario that may well be in that array.
155
155
  */
156
- export declare function validateScenarioFeatures(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
156
+ export declare function validateScenarioFeatures(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
157
157
  /**
158
158
  * A `pikkuWorkflowGraph` node that references the `graph:` namespace (e.g.
159
159
  * `graph:editFields`) needs @pikku/addon-graph wired — otherwise the RPC never
@@ -178,7 +178,7 @@ export declare function validateSchemaWiringSeparation(logger: InspectorLogger,
178
178
  * a name the omit does not cover. A name-based rule misses this; the type does
179
179
  * not.
180
180
  */
181
- export declare function validateNoSecretAliasServices(logger: InspectorLogger, checker: ts.TypeChecker, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
181
+ export declare function validateNoSecretAliasServices(logger: InspectorLogger, checker: ts.TypeChecker, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
182
182
  /**
183
183
  * Reports secret reads the catalogue does not account for.
184
184
  *
@@ -191,8 +191,8 @@ export declare function validateNoSecretAliasServices(logger: InspectorLogger, c
191
191
  * deployment is asked for the client id and secret every connect flow needs
192
192
  * without an author restating a shape the runtime already fixes.
193
193
  */
194
- export declare function registerDerivedOAuth2AppSecrets(state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
195
- export declare function validateSecretUsage(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
194
+ export declare function registerDerivedOAuth2AppSecrets(state: InspectorState | Omit<InspectorState, "typesLookup">): void;
195
+ export declare function validateSecretUsage(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
196
196
  export declare function computeDiagnostics(state: InspectorState): void;
197
197
  /**
198
198
  * Validates that every scope referenced by a function is declared via
@@ -201,7 +201,7 @@ export declare function computeDiagnostics(state: InspectorState): void;
201
201
  * A `*` suffix is a wildcard requirement: `admin:*` requires the `admin` scope
202
202
  * to be declared, and grants its whole subtree.
203
203
  */
204
- export declare function validateScopeReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
204
+ export declare function validateScopeReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
205
205
  /**
206
206
  * Resolves every `tools:` entry on every agent and reports the ones that do not
207
207
  * land on a real function, or that land on one with nothing to tell the model.
@@ -218,7 +218,7 @@ export declare function validateScopeReferences(logger: InspectorLogger, state:
218
218
  * quality loss rather than a failure. A title deliberately does not satisfy it —
219
219
  * a title labels a tool in a UI, it does not tell a model when to reach for it.
220
220
  */
221
- export declare function validateAgentToolReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>, options?: InspectorOptions): void;
221
+ export declare function validateAgentToolReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">, options?: InspectorOptions): void;
222
222
  /**
223
223
  * Validates that every scorer an agent asks to be graded by exists.
224
224
  *
@@ -226,7 +226,7 @@ export declare function validateAgentToolReferences(logger: InspectorLogger, sta
226
226
  * that has already answered the user — which means a typo would otherwise cost
227
227
  * nothing visible and simply grade nothing, forever.
228
228
  */
229
- export declare function validateAgentScorerReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
229
+ export declare function validateAgentScorerReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
230
230
  /**
231
231
  * Validates that every scope granted by a system role is declared via
232
232
  * `defineScope`. Runs after all visitors, so declaration order does not matter.
@@ -239,7 +239,7 @@ export declare function validateAgentScorerReferences(logger: InspectorLogger, s
239
239
  *
240
240
  * A `*` suffix grants a subtree; the node it hangs off must exist.
241
241
  */
242
- export declare function validateSystemRoleScopes(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
242
+ export declare function validateSystemRoleScopes(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
243
243
  /**
244
244
  * Validates that every role a persona names is declared with
245
245
  * `defineSystemRole`. Runs after all visitors, so declaration order does not
@@ -251,4 +251,4 @@ export declare function validateSystemRoleScopes(logger: InspectorLogger, state:
251
251
  * symptom is a scatter of 403s that read like authorization findings rather
252
252
  * than like a deleted role.
253
253
  */
254
- export declare function validatePersonaRoles(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
254
+ export declare function validatePersonaRoles(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
@@ -1,16 +1,16 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { createRequire } from 'node:module';
3
- import { join } from 'node:path';
4
- import { extractTypeKeys } from './type-utils.js';
5
- import { ErrorCode } from '../error-codes.js';
6
- import { findSecretAliasServices } from './secret-alias-services.js';
7
- import { deriveOAuth2AppSecrets } from '@pikku/core/secret';
8
- import { resolveCoreType } from './resolve-core-type.js';
9
- import { relative } from 'node:path';
10
- import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js';
11
- import { flattenScopeDefinitions } from '@pikku/core/scope';
12
- import { DYNAMIC_SCENARIO_STEP_TARGET } from './workflow/dsl/patterns.js';
13
- import { addonResolutionDirs } from './addon-resolution.js';
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { join } from "node:path";
4
+ import { extractTypeKeys } from "./type-utils.js";
5
+ import { ErrorCode } from "../error-codes.js";
6
+ import { findSecretAliasServices } from "./secret-alias-services.js";
7
+ import { deriveOAuth2AppSecrets } from "@pikku/core/secret";
8
+ import { resolveCoreType } from "./resolve-core-type.js";
9
+ import { relative } from "node:path";
10
+ import { AUTH_HANDLER_FUNC_ID } from "../add/add-auth.js";
11
+ import { flattenScopeDefinitions } from "@pikku/core/scope";
12
+ import { DYNAMIC_SCENARIO_STEP_TARGET } from "./workflow/dsl/patterns.js";
13
+ import { addonResolutionDirs } from "./addon-resolution.js";
14
14
  /**
15
15
  * Stamp the inspected authorize/callbacks service set onto the generated auth
16
16
  * handler's function meta.
@@ -45,7 +45,7 @@ export function extractWireNames(list) {
45
45
  if (!list)
46
46
  return [];
47
47
  return list
48
- .filter((item) => item.type === 'wire')
48
+ .filter((item) => item.type === "wire")
49
49
  .map((item) => item.name);
50
50
  }
51
51
  /**
@@ -57,13 +57,13 @@ function expandAndAddGroupServices(list, state, addServices) {
57
57
  if (!list)
58
58
  return;
59
59
  for (const item of list) {
60
- if (item.type === 'tag') {
60
+ if (item.type === "tag") {
61
61
  const groupMeta = state.middleware.tagMiddleware.get(item.tag);
62
62
  if (groupMeta?.services) {
63
63
  addServices(groupMeta.services);
64
64
  }
65
65
  }
66
- else if (item.type === 'http' && 'route' in item) {
66
+ else if (item.type === "http" && "route" in item) {
67
67
  const groupMeta = state.http.routeMiddleware.get(item.route);
68
68
  if (groupMeta?.services) {
69
69
  addServices(groupMeta.services);
@@ -78,7 +78,7 @@ function expandAndAddGroupServices(list, state, addServices) {
78
78
  */
79
79
  function extractAllServices(state) {
80
80
  // Skip if typesLookup is not available (e.g., deserialized state)
81
- if (!('typesLookup' in state)) {
81
+ if (!("typesLookup" in state)) {
82
82
  return;
83
83
  }
84
84
  // Extract all singleton services from the SingletonServices type
@@ -94,6 +94,24 @@ function extractAllServices(state) {
94
94
  state.serviceAggregation.allWireServices = extractTypeKeys(servicesType)
95
95
  .filter((name) => !singletonSet.has(name))
96
96
  .sort();
97
+ markSingletonServicesOnly(state, new Set(state.serviceAggregation.allWireServices));
98
+ }
99
+ }
100
+ /**
101
+ * Marks the functions that never read a wire service, so the runner can skip
102
+ * `createWireServices` for them. Only called once the `Services` type has
103
+ * resolved: an unresolved type yields no wire services, which would otherwise
104
+ * mark every function.
105
+ */
106
+ function markSingletonServicesOnly(state, wireServices) {
107
+ for (const meta of Object.values(state.functions.meta)) {
108
+ if (meta.services?.optimized &&
109
+ !meta.services.services.some((name) => wireServices.has(name))) {
110
+ meta.singletonServicesOnly = true;
111
+ }
112
+ else {
113
+ delete meta.singletonServicesOnly;
114
+ }
97
115
  }
98
116
  }
99
117
  /**
@@ -108,7 +126,7 @@ export function aggregateRequiredServices(state) {
108
126
  extractAllServices(state);
109
127
  const { requiredServices, usedFunctions, usedMiddleware, usedPermissions } = state.serviceAggregation;
110
128
  // Internal services (always excluded from tree-shaking)
111
- const internalServices = new Set(['rpc', 'mcp', 'channel', 'userSession']);
129
+ const internalServices = new Set(["rpc", "mcp", "channel", "userSession"]);
112
130
  const addServices = (services) => {
113
131
  if (!services || !services.services)
114
132
  return;
@@ -154,15 +172,7 @@ export function aggregateRequiredServices(state) {
154
172
  });
155
173
  // 4. Services from middleware/permission groups used in wirings
156
174
  // We need to check all wirings and expand any tag/HTTP-pattern groups they use
157
- for (const method of [
158
- 'get',
159
- 'post',
160
- 'put',
161
- 'patch',
162
- 'delete',
163
- 'head',
164
- 'options',
165
- ]) {
175
+ for (const method of ["get", "post", "put", "patch", "delete", "head", "options"]) {
166
176
  for (const routeMeta of Object.values(state.http.meta[method])) {
167
177
  expandAndAddGroupServices(routeMeta.middleware, state, addServices);
168
178
  }
@@ -200,23 +210,22 @@ export function aggregateRequiredServices(state) {
200
210
  // that wrap workflow operations but don't destructure the services.
201
211
  const hasWorkflows = Object.keys(state.workflows.graphMeta).length > 0 ||
202
212
  Object.keys(state.workflows.meta).length > 0 ||
203
- Object.keys(state.functions.meta).some((id) => id.startsWith('workflowStart:') ||
204
- id.startsWith('workflowStatus:') ||
205
- id.startsWith('workflow:'));
213
+ Object.keys(state.functions.meta).some((id) => id.startsWith("workflowStart:") ||
214
+ id.startsWith("workflowStatus:") ||
215
+ id.startsWith("workflow:"));
206
216
  if (hasWorkflows) {
207
- requiredServices.add('workflowService');
208
- requiredServices.add('workflowRunService');
209
- requiredServices.add('schedulerService');
210
- requiredServices.add('queueService');
217
+ requiredServices.add("workflowService");
218
+ requiredServices.add("workflowRunService");
219
+ requiredServices.add("schedulerService");
220
+ requiredServices.add("queueService");
211
221
  }
212
222
  // Webhook source routes and their worker are generated and destructure
213
223
  // nothing, so they are found by id: the route accepts into the queue through
214
224
  // incomingWebhookService and the worker records attempts through it.
215
225
  const hasWebhookSources = Object.values(state.triggers?.webhookSourceMeta ?? {}).some(({ method, route }) => state.functions.meta[`http:${method}:${route}`] !== undefined);
216
- if (hasWebhookSources ||
217
- state.functions.meta['queue:pikku-incoming-webhooks'] !== undefined) {
218
- requiredServices.add('incomingWebhookService');
219
- requiredServices.add('queueService');
226
+ if (hasWebhookSources || state.functions.meta["queue:pikku-incoming-webhooks"] !== undefined) {
227
+ requiredServices.add("incomingWebhookService");
228
+ requiredServices.add("queueService");
220
229
  }
221
230
  // 6b. Inject synthetic queue workers for workflow graph steps.
222
231
  // Each workflow gets an orchestrator queue and per-step queues.
@@ -230,11 +239,11 @@ export function aggregateRequiredServices(state) {
230
239
  // synthetic entries were pure leakage: they put a
231
240
  // `wf-orchestrator-<scenario>` worker into the app's queue meta, which every
232
241
  // bundle imports and a provider then creates as a real production queue.
233
- if (graph.source === 'scenario')
242
+ if (graph.source === "scenario")
234
243
  continue;
235
244
  const toKebab = (s) => s
236
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
237
- .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
245
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
246
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
238
247
  .toLowerCase();
239
248
  // Orchestrator queue
240
249
  const orchQueueName = `wf-orchestrator-${toKebab(graph.name)}`;
@@ -246,14 +255,11 @@ export function aggregateRequiredServices(state) {
246
255
  }
247
256
  // Per-step queues — only for steps explicitly marked workflowQueued: true
248
257
  for (const node of Object.values(graph.nodes)) {
249
- if (!('rpcName' in node) || !node.rpcName)
258
+ if (!("rpcName" in node) || !node.rpcName)
250
259
  continue;
251
260
  const rpcName = node.rpcName;
252
- const funcId = state.rpc?.internalMeta?.[rpcName] ??
253
- state.rpc?.exposedMeta?.[rpcName] ??
254
- rpcName;
255
- const funcMeta = (state.functions.meta[funcId] ??
256
- state.functions.meta[rpcName]);
261
+ const funcId = state.rpc?.internalMeta?.[rpcName] ?? state.rpc?.exposedMeta?.[rpcName] ?? rpcName;
262
+ const funcMeta = (state.functions.meta[funcId] ?? state.functions.meta[rpcName]);
257
263
  if (funcMeta?.workflowQueued !== true)
258
264
  continue;
259
265
  const stepQueueName = `wf-step-${toKebab(rpcName)}`;
@@ -267,20 +273,20 @@ export function aggregateRequiredServices(state) {
267
273
  }
268
274
  // AI agents need agentStorage + agentRunState + agentRunService + agentRunner
269
275
  if (Object.keys(state.agents.agentsMeta).length > 0) {
270
- requiredServices.add('agentStorage');
271
- requiredServices.add('agentRunState');
272
- requiredServices.add('agentRunService');
273
- requiredServices.add('agentRunner');
276
+ requiredServices.add("agentStorage");
277
+ requiredServices.add("agentRunState");
278
+ requiredServices.add("agentRunService");
279
+ requiredServices.add("agentRunner");
274
280
  }
275
281
  // Channels need eventHub for pub/sub
276
282
  if (Object.keys(state.channels.meta).length > 0) {
277
- requiredServices.add('eventHub');
283
+ requiredServices.add("eventHub");
278
284
  }
279
285
  // Declared scopes need scopeService to resolve a grant. Nothing in a project
280
286
  // destructures it — the generated auth layer reaches it — so the declaration
281
287
  // is the only signal there is.
282
288
  if ((state.scopes?.definitions?.length ?? 0) > 0) {
283
- requiredServices.add('scopeService');
289
+ requiredServices.add("scopeService");
284
290
  }
285
291
  // Declared feature flags need featureFlags for the same reason, from the
286
292
  // other side: `assertFeatureAvailable` reads the source out of the singleton
@@ -288,17 +294,17 @@ export function aggregateRequiredServices(state) {
288
294
  // ever says the project has flags. Without this the flag tables are left out
289
295
  // of the generated migration and every gate silently resolves open.
290
296
  if ((state.featureFlags?.definitions?.length ?? 0) > 0) {
291
- requiredServices.add('featureFlags');
297
+ requiredServices.add("featureFlags");
292
298
  }
293
299
  // A `defineAnalyticsEvents` declaration means events are emitted, and the
294
300
  // invocation buffer flushes into `analyticsService`. Nothing destructures
295
301
  // that either — functions are handed the request-scoped `analytics` — so the
296
302
  // declaration is again the only signal.
297
303
  if ((state.analytics?.length ?? 0) > 0) {
298
- requiredServices.add('analyticsService');
304
+ requiredServices.add("analyticsService");
299
305
  }
300
306
  if ((state.credentials?.definitions?.length ?? 0) > 0) {
301
- requiredServices.add('credentialService');
307
+ requiredServices.add("credentialService");
302
308
  }
303
309
  // 7. Services that consumed addons need from the parent project.
304
310
  const addonFnServices = new Map();
@@ -309,13 +315,7 @@ export function aggregateRequiredServices(state) {
309
315
  }
310
316
  const parentDeclared = state.addonRequiredParentServices ?? [];
311
317
  const parentDeclaredSet = new Set(parentDeclared);
312
- const defaultServices = new Set([
313
- 'config',
314
- 'logger',
315
- 'variables',
316
- 'schema',
317
- 'secrets',
318
- ]);
318
+ const defaultServices = new Set(["config", "logger", "variables", "schema", "secrets"]);
319
319
  let usesAddonFn = false;
320
320
  let addonFactoryNeeded = false;
321
321
  for (const funcId of usedFunctions) {
@@ -331,8 +331,7 @@ export function aggregateRequiredServices(state) {
331
331
  if (parentDeclaredSet.has(service)) {
332
332
  requiredServices.add(service);
333
333
  }
334
- else if (!internalServices.has(service) &&
335
- !defaultServices.has(service)) {
334
+ else if (!internalServices.has(service) && !defaultServices.has(service)) {
336
335
  addonFactoryNeeded = true;
337
336
  }
338
337
  }
@@ -380,7 +379,7 @@ export function validateSecretOverrides(logger, state) {
380
379
  for (const [logicalName, resolvedName] of Object.entries(addonDecl.secretOverrides ?? {})) {
381
380
  if (!secretIds.has(resolvedName)) {
382
381
  const availableSecrets = Array.from(secretIds);
383
- logger.critical(ErrorCode.INVALID_VALUE, `Secret override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(', ') || 'none'}`);
382
+ logger.critical(ErrorCode.INVALID_VALUE, `Secret override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(", ") || "none"}`);
384
383
  }
385
384
  }
386
385
  // A grant names the secret as the addon reads it, so it resolves through
@@ -389,7 +388,7 @@ export function validateSecretOverrides(logger, state) {
389
388
  const resolvedName = addonDecl.secretOverrides?.[logicalName] ?? logicalName;
390
389
  if (!secretIds.has(resolvedName)) {
391
390
  const availableSecrets = Array.from(secretIds);
392
- logger.critical(ErrorCode.INVALID_VALUE, `Secret grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(', ') || 'none'}`);
391
+ logger.critical(ErrorCode.INVALID_VALUE, `Secret grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(", ") || "none"}`);
393
392
  }
394
393
  }
395
394
  }
@@ -400,8 +399,8 @@ export function validateCredentialOverrides(logger, state) {
400
399
  return;
401
400
  const credentialNames = new Set(state.credentials?.definitions.map((d) => d.name) ?? []);
402
401
  /** Only a rename has a target to check; a mode-only override renames nothing. */
403
- const renameTarget = (override) => typeof override === 'string' ? override : override.name;
404
- const overrideMode = (override) => typeof override === 'string' ? undefined : override.mode;
402
+ const renameTarget = (override) => typeof override === "string" ? override : override.name;
403
+ const overrideMode = (override) => typeof override === "string" ? undefined : override.mode;
405
404
  /**
406
405
  * Which declaration set each resolved name's mode. Credential generation
407
406
  * writes one shared metadata entry per name, so two declarations that map
@@ -418,10 +417,8 @@ export function validateCredentialOverrides(logger, state) {
418
417
  const resolvedName = renamed ?? logicalName;
419
418
  if (!credentialNames.has(resolvedName)) {
420
419
  const availableCredentials = Array.from(credentialNames);
421
- const target = renamed
422
- ? `'${logicalName}' -> '${resolvedName}'`
423
- : `'${logicalName}'`;
424
- logger.critical(ErrorCode.INVALID_VALUE, `Credential override ${target} in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(', ') || 'none'}`);
420
+ const target = renamed ? `'${logicalName}' -> '${resolvedName}'` : `'${logicalName}'`;
421
+ logger.critical(ErrorCode.INVALID_VALUE, `Credential override ${target} in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(", ") || "none"}`);
425
422
  }
426
423
  const mode = overrideMode(override);
427
424
  if (!mode)
@@ -437,7 +434,7 @@ export function validateCredentialOverrides(logger, state) {
437
434
  const resolvedName = (override ? renameTarget(override) : undefined) ?? logicalName;
438
435
  if (!credentialNames.has(resolvedName)) {
439
436
  const availableCredentials = Array.from(credentialNames);
440
- logger.critical(ErrorCode.INVALID_VALUE, `Credential grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(', ') || 'none'}`);
437
+ logger.critical(ErrorCode.INVALID_VALUE, `Credential grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(", ") || "none"}`);
441
438
  }
442
439
  }
443
440
  }
@@ -455,7 +452,7 @@ export function validateVariableOverrides(logger, state) {
455
452
  for (const [logicalName, resolvedName] of Object.entries(addonDecl.variableOverrides)) {
456
453
  if (!variableIds.has(resolvedName)) {
457
454
  const availableVariables = Array.from(variableIds);
458
- logger.critical(ErrorCode.INVALID_VALUE, `Variable override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a variable that does not exist. Available variables: ${availableVariables.join(', ') || 'none'}`);
455
+ logger.critical(ErrorCode.INVALID_VALUE, `Variable override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a variable that does not exist. Available variables: ${availableVariables.join(", ") || "none"}`);
459
456
  }
460
457
  }
461
458
  }
@@ -471,11 +468,11 @@ export function validateVariableOverrides(logger, state) {
471
468
  * attempt means it is genuinely absent.
472
469
  */
473
470
  const isPackageResolvable = (pkg, dirs) => {
474
- const NOT_FOUND = new Set(['MODULE_NOT_FOUND', 'ERR_MODULE_NOT_FOUND']);
471
+ const NOT_FOUND = new Set(["MODULE_NOT_FOUND", "ERR_MODULE_NOT_FOUND"]);
475
472
  for (const dir of dirs) {
476
473
  let req;
477
474
  try {
478
- req = createRequire(join(dir, 'package.json'));
475
+ req = createRequire(join(dir, "package.json"));
479
476
  }
480
477
  catch {
481
478
  return true; // cannot ask; do not accuse
@@ -529,7 +526,7 @@ export function validateRemoteAddonDependencies(logger, state) {
529
526
  const dirs = addonResolutionDirs(state.rootDir, decl.file);
530
527
  if (isPackageResolvable(decl.package, dirs))
531
528
  continue;
532
- logger.critical(ErrorCode.ADDON_NOT_INSTALLED, `Addon '${namespace}' ('${decl.package}') is wired with wireAddon but cannot be resolved from ${dirs.join(' or ')} — every ref('${namespace}:…') will resolve to nothing and the surface will be dead at runtime. Add it to the dependencies of ${join(dirs[0], 'package.json')} and install, or remove the wireAddon call.`);
529
+ logger.critical(ErrorCode.ADDON_NOT_INSTALLED, `Addon '${namespace}' ('${decl.package}') is wired with wireAddon but cannot be resolved from ${dirs.join(" or ")} — every ref('${namespace}:…') will resolve to nothing and the surface will be dead at runtime. Add it to the dependencies of ${join(dirs[0], "package.json")} and install, or remove the wireAddon call.`);
533
530
  }
534
531
  // A remote addon belongs in the devDependencies of the package that wires
535
532
  // it, which in a workspace is not the root.
@@ -540,7 +537,7 @@ export function validateRemoteAddonDependencies(logger, state) {
540
537
  let manifest = null;
541
538
  if (existsSync(path)) {
542
539
  try {
543
- manifest = JSON.parse(readFileSync(path, 'utf-8'));
540
+ manifest = JSON.parse(readFileSync(path, "utf-8"));
544
541
  }
545
542
  catch (e) {
546
543
  logger.warn(`Could not read ${path} to verify remote addon dependencies: ${e?.message ?? e}`);
@@ -552,7 +549,7 @@ export function validateRemoteAddonDependencies(logger, state) {
552
549
  for (const [namespace, decl] of wireAddonDeclarations.entries()) {
553
550
  if (!decl.remote)
554
551
  continue;
555
- const pkgJsonPath = join(addonResolutionDirs(state.rootDir, decl.file)[0], 'package.json');
552
+ const pkgJsonPath = join(addonResolutionDirs(state.rootDir, decl.file)[0], "package.json");
556
553
  const pkgJson = readManifest(pkgJsonPath);
557
554
  if (!pkgJson)
558
555
  continue; // no manifest to check (e.g. some tests)
@@ -584,10 +581,10 @@ export function validateRemoteAddonAuth(logger, state) {
584
581
  if (!decl.remote)
585
582
  continue;
586
583
  if (decl.authCredentialId && !credentialNames.has(decl.authCredentialId)) {
587
- logger.critical(ErrorCode.REMOTE_ADDON_AUTH_UNRESOLVED, `Remote addon '${namespace}' binds auth.credentialId '${decl.authCredentialId}', but no such credential is wired. Available credentials: ${Array.from(credentialNames).join(', ') || 'none'}`);
584
+ logger.critical(ErrorCode.REMOTE_ADDON_AUTH_UNRESOLVED, `Remote addon '${namespace}' binds auth.credentialId '${decl.authCredentialId}', but no such credential is wired. Available credentials: ${Array.from(credentialNames).join(", ") || "none"}`);
588
585
  }
589
586
  if (decl.authSecretId && !secretNames.has(decl.authSecretId)) {
590
- logger.critical(ErrorCode.REMOTE_ADDON_AUTH_UNRESOLVED, `Remote addon '${namespace}' binds auth.secretId '${decl.authSecretId}', but no such secret is wired. Available secrets: ${Array.from(secretNames).join(', ') || 'none'}`);
587
+ logger.critical(ErrorCode.REMOTE_ADDON_AUTH_UNRESOLVED, `Remote addon '${namespace}' binds auth.secretId '${decl.authSecretId}', but no such secret is wired. Available secrets: ${Array.from(secretNames).join(", ") || "none"}`);
591
588
  }
592
589
  }
593
590
  }
@@ -596,7 +593,7 @@ export function computeResolvedIOTypes(state) {
596
593
  for (const [pikkuFuncId, meta] of Object.entries(functions.meta)) {
597
594
  const input = meta.inputs?.[0];
598
595
  const output = meta.outputs?.[0];
599
- let inputType = 'null';
596
+ let inputType = "null";
600
597
  if (input) {
601
598
  try {
602
599
  inputType = functions.typesMap.getTypeMeta(input).uniqueName;
@@ -605,7 +602,7 @@ export function computeResolvedIOTypes(state) {
605
602
  inputType = input;
606
603
  }
607
604
  }
608
- let outputType = 'null';
605
+ let outputType = "null";
609
606
  if (output) {
610
607
  try {
611
608
  outputType = functions.typesMap.getTypeMeta(output).uniqueName;
@@ -615,10 +612,10 @@ export function computeResolvedIOTypes(state) {
615
612
  }
616
613
  }
617
614
  state.resolvedIOTypes[pikkuFuncId] = { inputType, outputType };
618
- if (meta.inputSchemaName && inputType !== 'null') {
615
+ if (meta.inputSchemaName && inputType !== "null") {
619
616
  meta.inputSchemaName = inputType;
620
617
  }
621
- if (meta.outputSchemaName && outputType !== 'null') {
618
+ if (meta.outputSchemaName && outputType !== "null") {
622
619
  meta.outputSchemaName = outputType;
623
620
  }
624
621
  if (meta.inputs) {
@@ -680,15 +677,15 @@ export function computePermissionsGroupsMeta(state) {
680
677
  };
681
678
  }
682
679
  const PRIMITIVE_TYPES = new Set([
683
- 'boolean',
684
- 'string',
685
- 'number',
686
- 'null',
687
- 'undefined',
688
- 'void',
689
- 'any',
690
- 'unknown',
691
- 'never',
680
+ "boolean",
681
+ "string",
682
+ "number",
683
+ "null",
684
+ "undefined",
685
+ "void",
686
+ "any",
687
+ "unknown",
688
+ "never",
692
689
  ]);
693
690
  export function computeRequiredSchemas(state, options) {
694
691
  const { functions, schemaLookup } = state;
@@ -749,8 +746,8 @@ export function unresolvedSchemaReferences(state) {
749
746
  };
750
747
  for (const [funcName, meta] of Object.entries(functions.meta)) {
751
748
  for (const [role, declared] of [
752
- ['input', meta.inputs?.[0]],
753
- ['output', meta.outputs?.[0]],
749
+ ["input", meta.inputs?.[0]],
750
+ ["output", meta.outputs?.[0]],
754
751
  ]) {
755
752
  if (!declared || PRIMITIVE_TYPES.has(declared))
756
753
  continue;
@@ -767,12 +764,12 @@ export function validateSchemaReferences(logger, state) {
767
764
  if (unresolved.length === 0)
768
765
  return;
769
766
  logger.diagnostic({
770
- severity: 'error',
767
+ severity: "error",
771
768
  code: ErrorCode.SCHEMA_REFERENCE_UNRESOLVED,
772
- message: `${unresolved.length} function contract${unresolved.length === 1 ? '' : 's'} ` +
773
- `name${unresolved.length === 1 ? 's' : ''} a schema that was never generated, so ` +
774
- `calling ${unresolved.length === 1 ? 'it' : 'them'} fails at runtime with ` +
775
- `MissingSchemaError: ${unresolved.join(', ')}. ` +
769
+ message: `${unresolved.length} function contract${unresolved.length === 1 ? "" : "s"} ` +
770
+ `name${unresolved.length === 1 ? "s" : ""} a schema that was never generated, so ` +
771
+ `calling ${unresolved.length === 1 ? "it" : "them"} fails at runtime with ` +
772
+ `MissingSchemaError: ${unresolved.join(", ")}. ` +
776
773
  `The usual cause is a named type used as the function's input/output that ` +
777
774
  `is declared but not exported — export it, or inline the type at the call site.`,
778
775
  });
@@ -791,11 +788,11 @@ export function validateAgentModels(logger, state, modelAliases = {}) {
791
788
  // `hasOwn`, not a truthy lookup: every object literal inherits `toString`,
792
789
  // `constructor` and `__proto__`, so a bare model named after one of them
793
790
  // would read as a configured alias and pass a check nothing else repeats.
794
- if (!model.includes('/') && !Object.hasOwn(modelAliases, model)) {
791
+ if (!model.includes("/") && !Object.hasOwn(modelAliases, model)) {
795
792
  const known = Object.keys(modelAliases).sort();
796
793
  logger.critical(ErrorCode.INVALID_MODEL, `AI agent '${meta.name}' uses model '${model}', which is neither provider-qualified as '<provider>/<model>' (e.g. 'openai/gpt-4') nor an alias in the "models" table of pikku.config.json. ` +
797
794
  (known.length
798
- ? `Known aliases: ${known.join(', ')}.`
795
+ ? `Known aliases: ${known.join(", ")}.`
799
796
  : `No "models" aliases are configured.`));
800
797
  }
801
798
  }
@@ -818,9 +815,9 @@ export function validateScenarioServices(logger, state) {
818
815
  const funcMeta = state.functions.meta[meta.pikkuFuncId];
819
816
  if (!funcMeta?.services)
820
817
  continue;
821
- const disallowed = funcMeta.services.services.filter((svc) => svc !== 'logger' && svc !== 'config');
818
+ const disallowed = funcMeta.services.services.filter((svc) => svc !== "logger" && svc !== "config");
822
819
  if (disallowed.length > 0) {
823
- logger.critical(ErrorCode.SCENARIO_HAS_SERVICES, `Scenario '${workflowName}' destructures services: ${disallowed.join(', ')}. ` +
820
+ logger.critical(ErrorCode.SCENARIO_HAS_SERVICES, `Scenario '${workflowName}' destructures services: ${disallowed.join(", ")}. ` +
824
821
  `Scenarios may only use 'logger'/'config' — drive everything else through ` +
825
822
  `actor steps (workflow.do(step, rpc, data, { actor: actors.x })) so the flow ` +
826
823
  `runs against the target environment.`);
@@ -840,8 +837,8 @@ export const proseOpensWithActor = (prose, actor) => {
840
837
  if (!actor) {
841
838
  return false;
842
839
  }
843
- const escaped = actor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
844
- return new RegExp(`^["'“‘\\s]*${escaped}(?:['’]s)?\\b`, 'i').test(prose);
840
+ const escaped = actor.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
841
+ return new RegExp(`^["'“‘\\s]*${escaped}(?:['’]s)?\\b`, "i").test(prose);
845
842
  };
846
843
  /**
847
844
  * The prose that will actually render for one step, in the runtime's own
@@ -857,11 +854,7 @@ const renderableProse = (step, stepMeta) => {
857
854
  if (description) {
858
855
  return [step.stepName, description];
859
856
  }
860
- return [
861
- step.stepName,
862
- stepMeta?.scenarioStepTemplate,
863
- stepMeta?.description,
864
- ].filter((prose) => typeof prose === 'string');
857
+ return [step.stepName, stepMeta?.scenarioStepTemplate, stepMeta?.description].filter((prose) => typeof prose === "string");
865
858
  };
866
859
  /**
867
860
  * A step's prose must not open by naming the actor it already runs as (PKU681).
@@ -909,7 +902,7 @@ export function validateScenarioSteps(logger, state) {
909
902
  const phasesSeen = new Set();
910
903
  const visit = (workflowName, steps) => {
911
904
  for (const step of steps) {
912
- if (step.type === 'scenarioStep') {
905
+ if (step.type === "scenarioStep") {
913
906
  phasesSeen.add(step.phase);
914
907
  if (step.stepFunc === DYNAMIC_SCENARIO_STEP_TARGET) {
915
908
  logger.critical(ErrorCode.SCENARIO_STEP_TARGET_NOT_STATIC, `Scenario '${workflowName}' calls step '${step.stepName}' with a target that isn't a string literal. ` +
@@ -921,21 +914,21 @@ export function validateScenarioSteps(logger, state) {
921
914
  const stepMeta = state.functions.meta[step.stepFunc];
922
915
  validateStepProse(logger, workflowName, step, stepMeta);
923
916
  if (stepMeta?.scenarioStepRequiresActor && !step.actor) {
924
- const why = stepMeta.scenarioStepSurfaces?.includes('browser')
925
- ? 'declares a browser binding'
926
- : 'is driven by a persona';
917
+ const why = stepMeta.scenarioStepSurfaces?.includes("browser")
918
+ ? "declares a browser binding"
919
+ : "is driven by a persona";
927
920
  logger.critical(ErrorCode.SCENARIO_STEP_NEEDS_ACTOR, `Scenario '${workflowName}' calls step '${step.stepFunc}', which ${why}, without an actor. ` +
928
921
  `Pass { actor: actors.<name> } so it runs as that persona.`);
929
922
  }
930
923
  continue;
931
924
  }
932
- if (step.type === 'branch') {
925
+ if (step.type === "branch") {
933
926
  for (const branch of step.branches)
934
927
  visit(workflowName, branch.steps);
935
928
  if (step.elseSteps)
936
929
  visit(workflowName, step.elseSteps);
937
930
  }
938
- else if (step.type === 'switch') {
931
+ else if (step.type === "switch") {
939
932
  for (const c of step.cases ?? []) {
940
933
  if (c.steps)
941
934
  visit(workflowName, c.steps);
@@ -943,10 +936,10 @@ export function validateScenarioSteps(logger, state) {
943
936
  if (step.defaultSteps)
944
937
  visit(workflowName, step.defaultSteps);
945
938
  }
946
- else if (step.type === 'fanout' && step.body) {
939
+ else if (step.type === "fanout" && step.body) {
947
940
  visit(workflowName, step.body);
948
941
  }
949
- else if (step.type === 'parallel' && step.children) {
942
+ else if (step.type === "parallel" && step.children) {
950
943
  visit(workflowName, step.children);
951
944
  }
952
945
  }
@@ -961,8 +954,8 @@ export function validateScenarioSteps(logger, state) {
961
954
  // `asserts` is the escape for a flow whose witness is an expectation helper
962
955
  // instead: those are inline steps and leave no phase behind, but a recorded
963
956
  // service call or a refusal is an assertion all the same.
964
- if (phasesSeen.size > 0 && !phasesSeen.has('then') && !meta.asserts) {
965
- logger.critical(ErrorCode.SCENARIO_HAS_NO_ASSERTION, `Scenario '${workflowName}' has ${[...phasesSeen].join('/')} steps but never asserts. ` +
957
+ if (phasesSeen.size > 0 && !phasesSeen.has("then") && !meta.asserts) {
958
+ logger.critical(ErrorCode.SCENARIO_HAS_NO_ASSERTION, `Scenario '${workflowName}' has ${[...phasesSeen].join("/")} steps but never asserts. ` +
966
959
  `Add a scenario.then(...) naming what the actor should now see — a flow with no 'then' ` +
967
960
  `only proves nothing threw, and contributes nothing to witness coverage.`);
968
961
  }
@@ -1023,17 +1016,17 @@ export function validateScenarioFeatures(logger, state) {
1023
1016
  .map((feature) => feature.exportedName);
1024
1017
  if (unreadable.length > 0) {
1025
1018
  logger.diagnostic({
1026
- severity: 'warn',
1019
+ severity: "warn",
1027
1020
  code: ErrorCode.SCENARIO_HAS_NO_FEATURE,
1028
- message: `${unowned.map((name) => `'${name}'`).join(', ')} appear in no feature's \`scenarios\` array, ` +
1029
- `but ${unreadable.map((name) => `'${name}'`).join(', ')} build theirs with a spread or a call, so one of them may still be listed there. ` +
1021
+ message: `${unowned.map((name) => `'${name}'`).join(", ")} appear in no feature's \`scenarios\` array, ` +
1022
+ `but ${unreadable.map((name) => `'${name}'`).join(", ")} build theirs with a spread or a call, so one of them may still be listed there. ` +
1030
1023
  `List those scenarios literally and this becomes a definite answer either way.`,
1031
1024
  });
1032
1025
  return;
1033
1026
  }
1034
1027
  for (const name of unowned) {
1035
1028
  logger.diagnostic({
1036
- severity: 'error',
1029
+ severity: "error",
1037
1030
  code: ErrorCode.SCENARIO_HAS_NO_FEATURE,
1038
1031
  message: `Scenario '${name}' is listed by no pikkuFeature, so nothing downstream can reach it — ` +
1039
1032
  `a run stamps no feature onto its results, the console groups it under none, and a guide cannot cite it. ` +
@@ -1048,16 +1041,16 @@ export function validateScenarioFeatures(logger, state) {
1048
1041
  * early with an actionable message pointing at `scaffold: { graph: true }`.
1049
1042
  */
1050
1043
  export function validateWorkflowGraphAddons(logger, state) {
1051
- const addonGraphWired = Array.from(state.rpc.wireAddonDeclarations.values()).some((decl) => decl.package === '@pikku/addon-graph');
1044
+ const addonGraphWired = Array.from(state.rpc.wireAddonDeclarations.values()).some((decl) => decl.package === "@pikku/addon-graph");
1052
1045
  if (addonGraphWired) {
1053
1046
  return;
1054
1047
  }
1055
1048
  for (const [name, graph] of Object.entries(state.workflows.graphMeta)) {
1056
1049
  for (const node of Object.values(graph.nodes)) {
1057
- if (!('rpcName' in node) || typeof node.rpcName !== 'string') {
1050
+ if (!("rpcName" in node) || typeof node.rpcName !== "string") {
1058
1051
  continue;
1059
1052
  }
1060
- if (!node.rpcName.startsWith('graph:')) {
1053
+ if (!node.rpcName.startsWith("graph:")) {
1061
1054
  continue;
1062
1055
  }
1063
1056
  if (state.functions.meta[node.rpcName]) {
@@ -1107,7 +1100,7 @@ export function validateSchemaWiringSeparation(logger, state) {
1107
1100
  const schemas = Array.from(state.schemaLookup.entries())
1108
1101
  .filter(([, ref]) => ref.sourceFile === file)
1109
1102
  .map(([name]) => name);
1110
- logger.critical(ErrorCode.SCHEMA_AND_WIRING_COLOCATED, `File '${file}' contains both Zod schemas (${schemas.join(', ')}) and wiring calls (wireHTTPRoutes, addPermission, etc.). ` +
1103
+ logger.critical(ErrorCode.SCHEMA_AND_WIRING_COLOCATED, `File '${file}' contains both Zod schemas (${schemas.join(", ")}) and wiring calls (wireHTTPRoutes, addPermission, etc.). ` +
1111
1104
  `These must be in separate files because the CLI imports schema files at runtime, which triggers wiring side-effects that crash without server context. ` +
1112
1105
  `Move the route/wiring definitions to a dedicated wiring file.`);
1113
1106
  }
@@ -1122,7 +1115,7 @@ export function validateSchemaWiringSeparation(logger, state) {
1122
1115
  * not.
1123
1116
  */
1124
1117
  export function validateNoSecretAliasServices(logger, checker, state) {
1125
- if (!('typesLookup' in state)) {
1118
+ if (!("typesLookup" in state)) {
1126
1119
  return;
1127
1120
  }
1128
1121
  const singletonServicesType = resolveCoreType(state.typesLookup, state.singletonServicesTypeImportMap);
@@ -1138,14 +1131,14 @@ export function validateNoSecretAliasServices(logger, checker, state) {
1138
1131
  if (used.length === 0) {
1139
1132
  return;
1140
1133
  }
1141
- logger.critical(ErrorCode.SECRET_SERVICE_ALIASED, `${kind} '${id}' receives ${used.map((s) => `'${s}'`).join(', ')}, which ${used.length === 1 ? 'is' : 'are'} a SecretService under another name. ` +
1134
+ logger.critical(ErrorCode.SECRET_SERVICE_ALIASED, `${kind} '${id}' receives ${used.map((s) => `'${s}'`).join(", ")}, which ${used.length === 1 ? "is" : "are"} a SecretService under another name. ` +
1142
1135
  `SecretService is confined to pikkuServices, pikkuWireServices, addon service factories and middleware — give a service the secret value when you construct it and expose only what the function needs.`);
1143
1136
  };
1144
1137
  for (const [id, meta] of Object.entries(state.functions.meta)) {
1145
- report('Function', id, meta.services?.services ?? []);
1138
+ report("Function", id, meta.services?.services ?? []);
1146
1139
  }
1147
1140
  for (const [id, def] of Object.entries(state.permissions.definitions)) {
1148
- report('Permission', id, def.services?.services ?? []);
1141
+ report("Permission", id, def.services?.services ?? []);
1149
1142
  }
1150
1143
  }
1151
1144
  /**
@@ -1174,22 +1167,22 @@ export function validateSecretUsage(logger, state) {
1174
1167
  // auth-secrets.gen.ts declares it, but that file is written after the first
1175
1168
  // inspection of a fresh project, so its defineSecret is not seen yet.
1176
1169
  if (state.auth?.definition)
1177
- declared.add('BETTER_AUTH_SECRET');
1170
+ declared.add("BETTER_AUTH_SECRET");
1178
1171
  for (const [file, usage] of state.secrets.usage) {
1179
1172
  const relativeFile = relative(state.rootDir, file);
1180
1173
  const undeclared = usage.keys.filter((key) => !declared.has(key));
1181
1174
  if (undeclared.length > 0) {
1182
1175
  logger.diagnostic({
1183
- severity: 'warn',
1176
+ severity: "warn",
1184
1177
  code: ErrorCode.SECRET_NOT_DECLARED,
1185
- message: `${relativeFile} reads ${undeclared.map((k) => `'${k}'`).join(', ')}, which no defineSecret declares. Declare it so it is validated at deploy time and shown to whoever has to provision it.`,
1178
+ message: `${relativeFile} reads ${undeclared.map((k) => `'${k}'`).join(", ")}, which no defineSecret declares. Declare it so it is validated at deploy time and shown to whoever has to provision it.`,
1186
1179
  });
1187
1180
  }
1188
1181
  if (usage.dynamic.length > 0) {
1189
1182
  logger.diagnostic({
1190
- severity: 'warn',
1183
+ severity: "warn",
1191
1184
  code: ErrorCode.SECRET_KEY_NOT_STATIC,
1192
- message: `${relativeFile} reads a secret with a non-literal key (${usage.dynamic.join(', ')}). The deployment cannot narrow its secret scope around a key it cannot see.`,
1185
+ message: `${relativeFile} reads a secret with a non-literal key (${usage.dynamic.join(", ")}). The deployment cannot narrow its secret scope around a key it cannot see.`,
1193
1186
  });
1194
1187
  }
1195
1188
  }
@@ -1202,7 +1195,10 @@ export function computeDiagnostics(state) {
1202
1195
  // bridges that reference addon functions (id `http:<method>:<route>`, no
1203
1196
  // source file). The user can't edit any of these, so a destructure lint
1204
1197
  // meant to nudge them about their own code must not fail the build over them.
1205
- if (!meta.sourceFile || meta.sourceFile.endsWith('.gen.ts')) {
1198
+ if (!meta.sourceFile ||
1199
+ meta.sourceFile.endsWith(".gen.ts") ||
1200
+ meta.functionType === "inline" ||
1201
+ meta.functionType === "helper") {
1206
1202
  continue;
1207
1203
  }
1208
1204
  if (meta.services && !meta.services.optimized) {
@@ -1282,14 +1278,14 @@ export function validateScopeReferences(logger, state) {
1282
1278
  continue;
1283
1279
  for (const scope of meta.scopes) {
1284
1280
  // A trailing wildcard grants a subtree; the node it hangs off must exist.
1285
- const declaredForm = scope.endsWith(':*') ? scope.slice(0, -2) : scope;
1286
- if (scope === '*') {
1281
+ const declaredForm = scope.endsWith(":*") ? scope.slice(0, -2) : scope;
1282
+ if (scope === "*") {
1287
1283
  logger.critical(ErrorCode.INVALID_VALUE, `Function '${funcName}' requires the bare wildcard scope '*'. A function must require a specific scope — '*' is only meaningful as a grant.`);
1288
1284
  continue;
1289
1285
  }
1290
1286
  if (!declared.has(declaredForm)) {
1291
1287
  const available = Array.from(declared);
1292
- logger.critical(ErrorCode.INVALID_VALUE, `Function '${funcName}' requires scope '${scope}' which is not declared. Declare it with defineScope. Available scopes: ${available.join(', ') || 'none'}`);
1288
+ logger.critical(ErrorCode.INVALID_VALUE, `Function '${funcName}' requires scope '${scope}' which is not declared. Declare it with defineScope. Available scopes: ${available.join(", ") || "none"}`);
1293
1289
  }
1294
1290
  }
1295
1291
  }
@@ -1316,12 +1312,12 @@ export function validateAgentToolReferences(logger, state, options = {}) {
1316
1312
  return;
1317
1313
  const { wireAddonDeclarations } = state.rpc;
1318
1314
  for (const [agentKey, agent] of agents) {
1319
- const where = agent.sourceFile ? ` (${agent.sourceFile})` : '';
1315
+ const where = agent.sourceFile ? ` (${agent.sourceFile})` : "";
1320
1316
  for (const tool of agent.tools ?? []) {
1321
1317
  // A workflow reference is resolved against the workflow map, not here.
1322
- if (tool.startsWith('workflow:'))
1318
+ if (tool.startsWith("workflow:"))
1323
1319
  continue;
1324
- const separator = tool.indexOf(':');
1320
+ const separator = tool.indexOf(":");
1325
1321
  let meta;
1326
1322
  if (separator === -1) {
1327
1323
  const funcId = state.rpc.internalMeta[tool] ?? tool;
@@ -1338,7 +1334,7 @@ export function validateAgentToolReferences(logger, state, options = {}) {
1338
1334
  if (!addon) {
1339
1335
  const known = Array.from(wireAddonDeclarations?.keys() ?? []);
1340
1336
  logger.critical(ErrorCode.AGENT_TOOL_UNKNOWN_NAMESPACE, `AI agent '${agentKey}'${where} references tool '${tool}', but no addon is wired under the namespace '${namespace}'. ` +
1341
- `Wired namespaces: ${known.join(', ') || 'none'}.`);
1337
+ `Wired namespaces: ${known.join(", ") || "none"}.`);
1342
1338
  continue;
1343
1339
  }
1344
1340
  // An addon that has not been built yet contributed no metadata, which
@@ -1355,7 +1351,7 @@ export function validateAgentToolReferences(logger, state, options = {}) {
1355
1351
  if (options.strictMeta && !meta.description) {
1356
1352
  logger.critical(ErrorCode.AGENT_TOOL_MISSING_DESCRIPTION, `AI agent '${agentKey}'${where} uses tool '${tool}', which has no description. ` +
1357
1353
  `An agent tool's description is what the model is told it does; without one it is offered the tool under its own name. ` +
1358
- `Add a 'description' to '${tool}'${meta.title ? " (a 'title' does not count — it labels the tool, it does not explain it)" : ''}.`);
1354
+ `Add a 'description' to '${tool}'${meta.title ? " (a 'title' does not count — it labels the tool, it does not explain it)" : ""}.`);
1359
1355
  }
1360
1356
  }
1361
1357
  }
@@ -1370,12 +1366,12 @@ export function validateAgentToolReferences(logger, state, options = {}) {
1370
1366
  export function validateAgentScorerReferences(logger, state) {
1371
1367
  const known = Object.keys(state.scorers.scorersMeta);
1372
1368
  for (const [agentKey, agent] of Object.entries(state.agents.agentsMeta)) {
1373
- const where = agent.sourceFile ? ` (${agent.sourceFile})` : '';
1369
+ const where = agent.sourceFile ? ` (${agent.sourceFile})` : "";
1374
1370
  for (const scorer of agent.scorers ?? []) {
1375
1371
  if (known.includes(scorer))
1376
1372
  continue;
1377
1373
  logger.critical(ErrorCode.AGENT_SCORER_NOT_FOUND, `AI agent '${agentKey}'${where} asks to be graded by scorer '${scorer}', which is not declared in this project. ` +
1378
- `Declared scorers: ${known.join(', ') || 'none'}.`);
1374
+ `Declared scorers: ${known.join(", ") || "none"}.`);
1379
1375
  }
1380
1376
  }
1381
1377
  }
@@ -1398,14 +1394,14 @@ export function validateSystemRoleScopes(logger, state) {
1398
1394
  const declared = new Set(flattenScopeDefinitions(state.scopes.definitions).map((s) => s.id));
1399
1395
  for (const role of state.systemRoles.definitions) {
1400
1396
  for (const scope of role.scopes) {
1401
- if (scope === '*') {
1397
+ if (scope === "*") {
1402
1398
  logger.critical(ErrorCode.INVALID_VALUE, `System role '${role.name}' grants the bare wildcard scope '*'. Grant the roots it should cover instead — '*' hides what the role actually confers.`);
1403
1399
  continue;
1404
1400
  }
1405
- const declaredForm = scope.endsWith(':*') ? scope.slice(0, -2) : scope;
1401
+ const declaredForm = scope.endsWith(":*") ? scope.slice(0, -2) : scope;
1406
1402
  if (!declared.has(declaredForm)) {
1407
1403
  const available = Array.from(declared);
1408
- logger.critical(ErrorCode.INVALID_VALUE, `System role '${role.name}' grants scope '${scope}' which is not declared. Declare it with defineScope. Available scopes: ${available.join(', ') || 'none'}`);
1404
+ logger.critical(ErrorCode.INVALID_VALUE, `System role '${role.name}' grants scope '${scope}' which is not declared. Declare it with defineScope. Available scopes: ${available.join(", ") || "none"}`);
1409
1405
  }
1410
1406
  }
1411
1407
  }
@@ -1432,7 +1428,7 @@ export function validatePersonaRoles(logger, state) {
1432
1428
  const available = Array.from(declared).sort();
1433
1429
  logger.critical(ErrorCode.INVALID_VALUE, `Persona '${persona.id}' holds role '${role}', which is not declared with defineSystemRole. ` +
1434
1430
  `A persona may only name a system role — a role composed in the console can be deleted, and the persona would go on claiming to test it. ` +
1435
- `Declared roles: ${available.join(', ') || 'none'}`);
1431
+ `Declared roles: ${available.join(", ") || "none"}`);
1436
1432
  }
1437
1433
  }
1438
1434
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.92",
3
+ "version": "0.12.95",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -54,10 +54,10 @@
54
54
  "zod": "^4.4.3"
55
55
  },
56
56
  "peerDependencies": {
57
- "@pikku/core": "^0.12.127"
57
+ "@pikku/core": "^0.12.128"
58
58
  },
59
59
  "devDependencies": {
60
- "@pikku/core": "^0.12.127",
60
+ "@pikku/core": "^0.12.128",
61
61
  "@types/node": "^24.13.3"
62
62
  },
63
63
  "engines": {