@pikku/inspector 0.12.94 → 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,10 @@
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
+
1
8
  ## 0.12.94
2
9
 
3
10
  ### Patch Changes
@@ -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
@@ -126,7 +126,7 @@ export function aggregateRequiredServices(state) {
126
126
  extractAllServices(state);
127
127
  const { requiredServices, usedFunctions, usedMiddleware, usedPermissions } = state.serviceAggregation;
128
128
  // Internal services (always excluded from tree-shaking)
129
- const internalServices = new Set(['rpc', 'mcp', 'channel', 'userSession']);
129
+ const internalServices = new Set(["rpc", "mcp", "channel", "userSession"]);
130
130
  const addServices = (services) => {
131
131
  if (!services || !services.services)
132
132
  return;
@@ -172,15 +172,7 @@ export function aggregateRequiredServices(state) {
172
172
  });
173
173
  // 4. Services from middleware/permission groups used in wirings
174
174
  // We need to check all wirings and expand any tag/HTTP-pattern groups they use
175
- for (const method of [
176
- 'get',
177
- 'post',
178
- 'put',
179
- 'patch',
180
- 'delete',
181
- 'head',
182
- 'options',
183
- ]) {
175
+ for (const method of ["get", "post", "put", "patch", "delete", "head", "options"]) {
184
176
  for (const routeMeta of Object.values(state.http.meta[method])) {
185
177
  expandAndAddGroupServices(routeMeta.middleware, state, addServices);
186
178
  }
@@ -218,23 +210,22 @@ export function aggregateRequiredServices(state) {
218
210
  // that wrap workflow operations but don't destructure the services.
219
211
  const hasWorkflows = Object.keys(state.workflows.graphMeta).length > 0 ||
220
212
  Object.keys(state.workflows.meta).length > 0 ||
221
- Object.keys(state.functions.meta).some((id) => id.startsWith('workflowStart:') ||
222
- id.startsWith('workflowStatus:') ||
223
- id.startsWith('workflow:'));
213
+ Object.keys(state.functions.meta).some((id) => id.startsWith("workflowStart:") ||
214
+ id.startsWith("workflowStatus:") ||
215
+ id.startsWith("workflow:"));
224
216
  if (hasWorkflows) {
225
- requiredServices.add('workflowService');
226
- requiredServices.add('workflowRunService');
227
- requiredServices.add('schedulerService');
228
- requiredServices.add('queueService');
217
+ requiredServices.add("workflowService");
218
+ requiredServices.add("workflowRunService");
219
+ requiredServices.add("schedulerService");
220
+ requiredServices.add("queueService");
229
221
  }
230
222
  // Webhook source routes and their worker are generated and destructure
231
223
  // nothing, so they are found by id: the route accepts into the queue through
232
224
  // incomingWebhookService and the worker records attempts through it.
233
225
  const hasWebhookSources = Object.values(state.triggers?.webhookSourceMeta ?? {}).some(({ method, route }) => state.functions.meta[`http:${method}:${route}`] !== undefined);
234
- if (hasWebhookSources ||
235
- state.functions.meta['queue:pikku-incoming-webhooks'] !== undefined) {
236
- requiredServices.add('incomingWebhookService');
237
- requiredServices.add('queueService');
226
+ if (hasWebhookSources || state.functions.meta["queue:pikku-incoming-webhooks"] !== undefined) {
227
+ requiredServices.add("incomingWebhookService");
228
+ requiredServices.add("queueService");
238
229
  }
239
230
  // 6b. Inject synthetic queue workers for workflow graph steps.
240
231
  // Each workflow gets an orchestrator queue and per-step queues.
@@ -248,11 +239,11 @@ export function aggregateRequiredServices(state) {
248
239
  // synthetic entries were pure leakage: they put a
249
240
  // `wf-orchestrator-<scenario>` worker into the app's queue meta, which every
250
241
  // bundle imports and a provider then creates as a real production queue.
251
- if (graph.source === 'scenario')
242
+ if (graph.source === "scenario")
252
243
  continue;
253
244
  const toKebab = (s) => s
254
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
255
- .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")
256
247
  .toLowerCase();
257
248
  // Orchestrator queue
258
249
  const orchQueueName = `wf-orchestrator-${toKebab(graph.name)}`;
@@ -264,14 +255,11 @@ export function aggregateRequiredServices(state) {
264
255
  }
265
256
  // Per-step queues — only for steps explicitly marked workflowQueued: true
266
257
  for (const node of Object.values(graph.nodes)) {
267
- if (!('rpcName' in node) || !node.rpcName)
258
+ if (!("rpcName" in node) || !node.rpcName)
268
259
  continue;
269
260
  const rpcName = node.rpcName;
270
- const funcId = state.rpc?.internalMeta?.[rpcName] ??
271
- state.rpc?.exposedMeta?.[rpcName] ??
272
- rpcName;
273
- const funcMeta = (state.functions.meta[funcId] ??
274
- 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]);
275
263
  if (funcMeta?.workflowQueued !== true)
276
264
  continue;
277
265
  const stepQueueName = `wf-step-${toKebab(rpcName)}`;
@@ -285,20 +273,20 @@ export function aggregateRequiredServices(state) {
285
273
  }
286
274
  // AI agents need agentStorage + agentRunState + agentRunService + agentRunner
287
275
  if (Object.keys(state.agents.agentsMeta).length > 0) {
288
- requiredServices.add('agentStorage');
289
- requiredServices.add('agentRunState');
290
- requiredServices.add('agentRunService');
291
- requiredServices.add('agentRunner');
276
+ requiredServices.add("agentStorage");
277
+ requiredServices.add("agentRunState");
278
+ requiredServices.add("agentRunService");
279
+ requiredServices.add("agentRunner");
292
280
  }
293
281
  // Channels need eventHub for pub/sub
294
282
  if (Object.keys(state.channels.meta).length > 0) {
295
- requiredServices.add('eventHub');
283
+ requiredServices.add("eventHub");
296
284
  }
297
285
  // Declared scopes need scopeService to resolve a grant. Nothing in a project
298
286
  // destructures it — the generated auth layer reaches it — so the declaration
299
287
  // is the only signal there is.
300
288
  if ((state.scopes?.definitions?.length ?? 0) > 0) {
301
- requiredServices.add('scopeService');
289
+ requiredServices.add("scopeService");
302
290
  }
303
291
  // Declared feature flags need featureFlags for the same reason, from the
304
292
  // other side: `assertFeatureAvailable` reads the source out of the singleton
@@ -306,17 +294,17 @@ export function aggregateRequiredServices(state) {
306
294
  // ever says the project has flags. Without this the flag tables are left out
307
295
  // of the generated migration and every gate silently resolves open.
308
296
  if ((state.featureFlags?.definitions?.length ?? 0) > 0) {
309
- requiredServices.add('featureFlags');
297
+ requiredServices.add("featureFlags");
310
298
  }
311
299
  // A `defineAnalyticsEvents` declaration means events are emitted, and the
312
300
  // invocation buffer flushes into `analyticsService`. Nothing destructures
313
301
  // that either — functions are handed the request-scoped `analytics` — so the
314
302
  // declaration is again the only signal.
315
303
  if ((state.analytics?.length ?? 0) > 0) {
316
- requiredServices.add('analyticsService');
304
+ requiredServices.add("analyticsService");
317
305
  }
318
306
  if ((state.credentials?.definitions?.length ?? 0) > 0) {
319
- requiredServices.add('credentialService');
307
+ requiredServices.add("credentialService");
320
308
  }
321
309
  // 7. Services that consumed addons need from the parent project.
322
310
  const addonFnServices = new Map();
@@ -327,13 +315,7 @@ export function aggregateRequiredServices(state) {
327
315
  }
328
316
  const parentDeclared = state.addonRequiredParentServices ?? [];
329
317
  const parentDeclaredSet = new Set(parentDeclared);
330
- const defaultServices = new Set([
331
- 'config',
332
- 'logger',
333
- 'variables',
334
- 'schema',
335
- 'secrets',
336
- ]);
318
+ const defaultServices = new Set(["config", "logger", "variables", "schema", "secrets"]);
337
319
  let usesAddonFn = false;
338
320
  let addonFactoryNeeded = false;
339
321
  for (const funcId of usedFunctions) {
@@ -349,8 +331,7 @@ export function aggregateRequiredServices(state) {
349
331
  if (parentDeclaredSet.has(service)) {
350
332
  requiredServices.add(service);
351
333
  }
352
- else if (!internalServices.has(service) &&
353
- !defaultServices.has(service)) {
334
+ else if (!internalServices.has(service) && !defaultServices.has(service)) {
354
335
  addonFactoryNeeded = true;
355
336
  }
356
337
  }
@@ -398,7 +379,7 @@ export function validateSecretOverrides(logger, state) {
398
379
  for (const [logicalName, resolvedName] of Object.entries(addonDecl.secretOverrides ?? {})) {
399
380
  if (!secretIds.has(resolvedName)) {
400
381
  const availableSecrets = Array.from(secretIds);
401
- 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"}`);
402
383
  }
403
384
  }
404
385
  // A grant names the secret as the addon reads it, so it resolves through
@@ -407,7 +388,7 @@ export function validateSecretOverrides(logger, state) {
407
388
  const resolvedName = addonDecl.secretOverrides?.[logicalName] ?? logicalName;
408
389
  if (!secretIds.has(resolvedName)) {
409
390
  const availableSecrets = Array.from(secretIds);
410
- 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"}`);
411
392
  }
412
393
  }
413
394
  }
@@ -418,8 +399,8 @@ export function validateCredentialOverrides(logger, state) {
418
399
  return;
419
400
  const credentialNames = new Set(state.credentials?.definitions.map((d) => d.name) ?? []);
420
401
  /** Only a rename has a target to check; a mode-only override renames nothing. */
421
- const renameTarget = (override) => typeof override === 'string' ? override : override.name;
422
- 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;
423
404
  /**
424
405
  * Which declaration set each resolved name's mode. Credential generation
425
406
  * writes one shared metadata entry per name, so two declarations that map
@@ -436,10 +417,8 @@ export function validateCredentialOverrides(logger, state) {
436
417
  const resolvedName = renamed ?? logicalName;
437
418
  if (!credentialNames.has(resolvedName)) {
438
419
  const availableCredentials = Array.from(credentialNames);
439
- const target = renamed
440
- ? `'${logicalName}' -> '${resolvedName}'`
441
- : `'${logicalName}'`;
442
- 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"}`);
443
422
  }
444
423
  const mode = overrideMode(override);
445
424
  if (!mode)
@@ -455,7 +434,7 @@ export function validateCredentialOverrides(logger, state) {
455
434
  const resolvedName = (override ? renameTarget(override) : undefined) ?? logicalName;
456
435
  if (!credentialNames.has(resolvedName)) {
457
436
  const availableCredentials = Array.from(credentialNames);
458
- 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"}`);
459
438
  }
460
439
  }
461
440
  }
@@ -473,7 +452,7 @@ export function validateVariableOverrides(logger, state) {
473
452
  for (const [logicalName, resolvedName] of Object.entries(addonDecl.variableOverrides)) {
474
453
  if (!variableIds.has(resolvedName)) {
475
454
  const availableVariables = Array.from(variableIds);
476
- 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"}`);
477
456
  }
478
457
  }
479
458
  }
@@ -489,11 +468,11 @@ export function validateVariableOverrides(logger, state) {
489
468
  * attempt means it is genuinely absent.
490
469
  */
491
470
  const isPackageResolvable = (pkg, dirs) => {
492
- 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"]);
493
472
  for (const dir of dirs) {
494
473
  let req;
495
474
  try {
496
- req = createRequire(join(dir, 'package.json'));
475
+ req = createRequire(join(dir, "package.json"));
497
476
  }
498
477
  catch {
499
478
  return true; // cannot ask; do not accuse
@@ -547,7 +526,7 @@ export function validateRemoteAddonDependencies(logger, state) {
547
526
  const dirs = addonResolutionDirs(state.rootDir, decl.file);
548
527
  if (isPackageResolvable(decl.package, dirs))
549
528
  continue;
550
- 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.`);
551
530
  }
552
531
  // A remote addon belongs in the devDependencies of the package that wires
553
532
  // it, which in a workspace is not the root.
@@ -558,7 +537,7 @@ export function validateRemoteAddonDependencies(logger, state) {
558
537
  let manifest = null;
559
538
  if (existsSync(path)) {
560
539
  try {
561
- manifest = JSON.parse(readFileSync(path, 'utf-8'));
540
+ manifest = JSON.parse(readFileSync(path, "utf-8"));
562
541
  }
563
542
  catch (e) {
564
543
  logger.warn(`Could not read ${path} to verify remote addon dependencies: ${e?.message ?? e}`);
@@ -570,7 +549,7 @@ export function validateRemoteAddonDependencies(logger, state) {
570
549
  for (const [namespace, decl] of wireAddonDeclarations.entries()) {
571
550
  if (!decl.remote)
572
551
  continue;
573
- const pkgJsonPath = join(addonResolutionDirs(state.rootDir, decl.file)[0], 'package.json');
552
+ const pkgJsonPath = join(addonResolutionDirs(state.rootDir, decl.file)[0], "package.json");
574
553
  const pkgJson = readManifest(pkgJsonPath);
575
554
  if (!pkgJson)
576
555
  continue; // no manifest to check (e.g. some tests)
@@ -602,10 +581,10 @@ export function validateRemoteAddonAuth(logger, state) {
602
581
  if (!decl.remote)
603
582
  continue;
604
583
  if (decl.authCredentialId && !credentialNames.has(decl.authCredentialId)) {
605
- 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"}`);
606
585
  }
607
586
  if (decl.authSecretId && !secretNames.has(decl.authSecretId)) {
608
- 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"}`);
609
588
  }
610
589
  }
611
590
  }
@@ -614,7 +593,7 @@ export function computeResolvedIOTypes(state) {
614
593
  for (const [pikkuFuncId, meta] of Object.entries(functions.meta)) {
615
594
  const input = meta.inputs?.[0];
616
595
  const output = meta.outputs?.[0];
617
- let inputType = 'null';
596
+ let inputType = "null";
618
597
  if (input) {
619
598
  try {
620
599
  inputType = functions.typesMap.getTypeMeta(input).uniqueName;
@@ -623,7 +602,7 @@ export function computeResolvedIOTypes(state) {
623
602
  inputType = input;
624
603
  }
625
604
  }
626
- let outputType = 'null';
605
+ let outputType = "null";
627
606
  if (output) {
628
607
  try {
629
608
  outputType = functions.typesMap.getTypeMeta(output).uniqueName;
@@ -633,10 +612,10 @@ export function computeResolvedIOTypes(state) {
633
612
  }
634
613
  }
635
614
  state.resolvedIOTypes[pikkuFuncId] = { inputType, outputType };
636
- if (meta.inputSchemaName && inputType !== 'null') {
615
+ if (meta.inputSchemaName && inputType !== "null") {
637
616
  meta.inputSchemaName = inputType;
638
617
  }
639
- if (meta.outputSchemaName && outputType !== 'null') {
618
+ if (meta.outputSchemaName && outputType !== "null") {
640
619
  meta.outputSchemaName = outputType;
641
620
  }
642
621
  if (meta.inputs) {
@@ -698,15 +677,15 @@ export function computePermissionsGroupsMeta(state) {
698
677
  };
699
678
  }
700
679
  const PRIMITIVE_TYPES = new Set([
701
- 'boolean',
702
- 'string',
703
- 'number',
704
- 'null',
705
- 'undefined',
706
- 'void',
707
- 'any',
708
- 'unknown',
709
- 'never',
680
+ "boolean",
681
+ "string",
682
+ "number",
683
+ "null",
684
+ "undefined",
685
+ "void",
686
+ "any",
687
+ "unknown",
688
+ "never",
710
689
  ]);
711
690
  export function computeRequiredSchemas(state, options) {
712
691
  const { functions, schemaLookup } = state;
@@ -767,8 +746,8 @@ export function unresolvedSchemaReferences(state) {
767
746
  };
768
747
  for (const [funcName, meta] of Object.entries(functions.meta)) {
769
748
  for (const [role, declared] of [
770
- ['input', meta.inputs?.[0]],
771
- ['output', meta.outputs?.[0]],
749
+ ["input", meta.inputs?.[0]],
750
+ ["output", meta.outputs?.[0]],
772
751
  ]) {
773
752
  if (!declared || PRIMITIVE_TYPES.has(declared))
774
753
  continue;
@@ -785,12 +764,12 @@ export function validateSchemaReferences(logger, state) {
785
764
  if (unresolved.length === 0)
786
765
  return;
787
766
  logger.diagnostic({
788
- severity: 'error',
767
+ severity: "error",
789
768
  code: ErrorCode.SCHEMA_REFERENCE_UNRESOLVED,
790
- message: `${unresolved.length} function contract${unresolved.length === 1 ? '' : 's'} ` +
791
- `name${unresolved.length === 1 ? 's' : ''} a schema that was never generated, so ` +
792
- `calling ${unresolved.length === 1 ? 'it' : 'them'} fails at runtime with ` +
793
- `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(", ")}. ` +
794
773
  `The usual cause is a named type used as the function's input/output that ` +
795
774
  `is declared but not exported — export it, or inline the type at the call site.`,
796
775
  });
@@ -809,11 +788,11 @@ export function validateAgentModels(logger, state, modelAliases = {}) {
809
788
  // `hasOwn`, not a truthy lookup: every object literal inherits `toString`,
810
789
  // `constructor` and `__proto__`, so a bare model named after one of them
811
790
  // would read as a configured alias and pass a check nothing else repeats.
812
- if (!model.includes('/') && !Object.hasOwn(modelAliases, model)) {
791
+ if (!model.includes("/") && !Object.hasOwn(modelAliases, model)) {
813
792
  const known = Object.keys(modelAliases).sort();
814
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. ` +
815
794
  (known.length
816
- ? `Known aliases: ${known.join(', ')}.`
795
+ ? `Known aliases: ${known.join(", ")}.`
817
796
  : `No "models" aliases are configured.`));
818
797
  }
819
798
  }
@@ -836,9 +815,9 @@ export function validateScenarioServices(logger, state) {
836
815
  const funcMeta = state.functions.meta[meta.pikkuFuncId];
837
816
  if (!funcMeta?.services)
838
817
  continue;
839
- const disallowed = funcMeta.services.services.filter((svc) => svc !== 'logger' && svc !== 'config');
818
+ const disallowed = funcMeta.services.services.filter((svc) => svc !== "logger" && svc !== "config");
840
819
  if (disallowed.length > 0) {
841
- 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(", ")}. ` +
842
821
  `Scenarios may only use 'logger'/'config' — drive everything else through ` +
843
822
  `actor steps (workflow.do(step, rpc, data, { actor: actors.x })) so the flow ` +
844
823
  `runs against the target environment.`);
@@ -858,8 +837,8 @@ export const proseOpensWithActor = (prose, actor) => {
858
837
  if (!actor) {
859
838
  return false;
860
839
  }
861
- const escaped = actor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
862
- 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);
863
842
  };
864
843
  /**
865
844
  * The prose that will actually render for one step, in the runtime's own
@@ -875,11 +854,7 @@ const renderableProse = (step, stepMeta) => {
875
854
  if (description) {
876
855
  return [step.stepName, description];
877
856
  }
878
- return [
879
- step.stepName,
880
- stepMeta?.scenarioStepTemplate,
881
- stepMeta?.description,
882
- ].filter((prose) => typeof prose === 'string');
857
+ return [step.stepName, stepMeta?.scenarioStepTemplate, stepMeta?.description].filter((prose) => typeof prose === "string");
883
858
  };
884
859
  /**
885
860
  * A step's prose must not open by naming the actor it already runs as (PKU681).
@@ -927,7 +902,7 @@ export function validateScenarioSteps(logger, state) {
927
902
  const phasesSeen = new Set();
928
903
  const visit = (workflowName, steps) => {
929
904
  for (const step of steps) {
930
- if (step.type === 'scenarioStep') {
905
+ if (step.type === "scenarioStep") {
931
906
  phasesSeen.add(step.phase);
932
907
  if (step.stepFunc === DYNAMIC_SCENARIO_STEP_TARGET) {
933
908
  logger.critical(ErrorCode.SCENARIO_STEP_TARGET_NOT_STATIC, `Scenario '${workflowName}' calls step '${step.stepName}' with a target that isn't a string literal. ` +
@@ -939,21 +914,21 @@ export function validateScenarioSteps(logger, state) {
939
914
  const stepMeta = state.functions.meta[step.stepFunc];
940
915
  validateStepProse(logger, workflowName, step, stepMeta);
941
916
  if (stepMeta?.scenarioStepRequiresActor && !step.actor) {
942
- const why = stepMeta.scenarioStepSurfaces?.includes('browser')
943
- ? 'declares a browser binding'
944
- : 'is driven by a persona';
917
+ const why = stepMeta.scenarioStepSurfaces?.includes("browser")
918
+ ? "declares a browser binding"
919
+ : "is driven by a persona";
945
920
  logger.critical(ErrorCode.SCENARIO_STEP_NEEDS_ACTOR, `Scenario '${workflowName}' calls step '${step.stepFunc}', which ${why}, without an actor. ` +
946
921
  `Pass { actor: actors.<name> } so it runs as that persona.`);
947
922
  }
948
923
  continue;
949
924
  }
950
- if (step.type === 'branch') {
925
+ if (step.type === "branch") {
951
926
  for (const branch of step.branches)
952
927
  visit(workflowName, branch.steps);
953
928
  if (step.elseSteps)
954
929
  visit(workflowName, step.elseSteps);
955
930
  }
956
- else if (step.type === 'switch') {
931
+ else if (step.type === "switch") {
957
932
  for (const c of step.cases ?? []) {
958
933
  if (c.steps)
959
934
  visit(workflowName, c.steps);
@@ -961,10 +936,10 @@ export function validateScenarioSteps(logger, state) {
961
936
  if (step.defaultSteps)
962
937
  visit(workflowName, step.defaultSteps);
963
938
  }
964
- else if (step.type === 'fanout' && step.body) {
939
+ else if (step.type === "fanout" && step.body) {
965
940
  visit(workflowName, step.body);
966
941
  }
967
- else if (step.type === 'parallel' && step.children) {
942
+ else if (step.type === "parallel" && step.children) {
968
943
  visit(workflowName, step.children);
969
944
  }
970
945
  }
@@ -979,8 +954,8 @@ export function validateScenarioSteps(logger, state) {
979
954
  // `asserts` is the escape for a flow whose witness is an expectation helper
980
955
  // instead: those are inline steps and leave no phase behind, but a recorded
981
956
  // service call or a refusal is an assertion all the same.
982
- if (phasesSeen.size > 0 && !phasesSeen.has('then') && !meta.asserts) {
983
- 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. ` +
984
959
  `Add a scenario.then(...) naming what the actor should now see — a flow with no 'then' ` +
985
960
  `only proves nothing threw, and contributes nothing to witness coverage.`);
986
961
  }
@@ -1041,17 +1016,17 @@ export function validateScenarioFeatures(logger, state) {
1041
1016
  .map((feature) => feature.exportedName);
1042
1017
  if (unreadable.length > 0) {
1043
1018
  logger.diagnostic({
1044
- severity: 'warn',
1019
+ severity: "warn",
1045
1020
  code: ErrorCode.SCENARIO_HAS_NO_FEATURE,
1046
- message: `${unowned.map((name) => `'${name}'`).join(', ')} appear in no feature's \`scenarios\` array, ` +
1047
- `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. ` +
1048
1023
  `List those scenarios literally and this becomes a definite answer either way.`,
1049
1024
  });
1050
1025
  return;
1051
1026
  }
1052
1027
  for (const name of unowned) {
1053
1028
  logger.diagnostic({
1054
- severity: 'error',
1029
+ severity: "error",
1055
1030
  code: ErrorCode.SCENARIO_HAS_NO_FEATURE,
1056
1031
  message: `Scenario '${name}' is listed by no pikkuFeature, so nothing downstream can reach it — ` +
1057
1032
  `a run stamps no feature onto its results, the console groups it under none, and a guide cannot cite it. ` +
@@ -1066,16 +1041,16 @@ export function validateScenarioFeatures(logger, state) {
1066
1041
  * early with an actionable message pointing at `scaffold: { graph: true }`.
1067
1042
  */
1068
1043
  export function validateWorkflowGraphAddons(logger, state) {
1069
- 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");
1070
1045
  if (addonGraphWired) {
1071
1046
  return;
1072
1047
  }
1073
1048
  for (const [name, graph] of Object.entries(state.workflows.graphMeta)) {
1074
1049
  for (const node of Object.values(graph.nodes)) {
1075
- if (!('rpcName' in node) || typeof node.rpcName !== 'string') {
1050
+ if (!("rpcName" in node) || typeof node.rpcName !== "string") {
1076
1051
  continue;
1077
1052
  }
1078
- if (!node.rpcName.startsWith('graph:')) {
1053
+ if (!node.rpcName.startsWith("graph:")) {
1079
1054
  continue;
1080
1055
  }
1081
1056
  if (state.functions.meta[node.rpcName]) {
@@ -1125,7 +1100,7 @@ export function validateSchemaWiringSeparation(logger, state) {
1125
1100
  const schemas = Array.from(state.schemaLookup.entries())
1126
1101
  .filter(([, ref]) => ref.sourceFile === file)
1127
1102
  .map(([name]) => name);
1128
- 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.). ` +
1129
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. ` +
1130
1105
  `Move the route/wiring definitions to a dedicated wiring file.`);
1131
1106
  }
@@ -1140,7 +1115,7 @@ export function validateSchemaWiringSeparation(logger, state) {
1140
1115
  * not.
1141
1116
  */
1142
1117
  export function validateNoSecretAliasServices(logger, checker, state) {
1143
- if (!('typesLookup' in state)) {
1118
+ if (!("typesLookup" in state)) {
1144
1119
  return;
1145
1120
  }
1146
1121
  const singletonServicesType = resolveCoreType(state.typesLookup, state.singletonServicesTypeImportMap);
@@ -1156,14 +1131,14 @@ export function validateNoSecretAliasServices(logger, checker, state) {
1156
1131
  if (used.length === 0) {
1157
1132
  return;
1158
1133
  }
1159
- 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. ` +
1160
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.`);
1161
1136
  };
1162
1137
  for (const [id, meta] of Object.entries(state.functions.meta)) {
1163
- report('Function', id, meta.services?.services ?? []);
1138
+ report("Function", id, meta.services?.services ?? []);
1164
1139
  }
1165
1140
  for (const [id, def] of Object.entries(state.permissions.definitions)) {
1166
- report('Permission', id, def.services?.services ?? []);
1141
+ report("Permission", id, def.services?.services ?? []);
1167
1142
  }
1168
1143
  }
1169
1144
  /**
@@ -1192,22 +1167,22 @@ export function validateSecretUsage(logger, state) {
1192
1167
  // auth-secrets.gen.ts declares it, but that file is written after the first
1193
1168
  // inspection of a fresh project, so its defineSecret is not seen yet.
1194
1169
  if (state.auth?.definition)
1195
- declared.add('BETTER_AUTH_SECRET');
1170
+ declared.add("BETTER_AUTH_SECRET");
1196
1171
  for (const [file, usage] of state.secrets.usage) {
1197
1172
  const relativeFile = relative(state.rootDir, file);
1198
1173
  const undeclared = usage.keys.filter((key) => !declared.has(key));
1199
1174
  if (undeclared.length > 0) {
1200
1175
  logger.diagnostic({
1201
- severity: 'warn',
1176
+ severity: "warn",
1202
1177
  code: ErrorCode.SECRET_NOT_DECLARED,
1203
- 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.`,
1204
1179
  });
1205
1180
  }
1206
1181
  if (usage.dynamic.length > 0) {
1207
1182
  logger.diagnostic({
1208
- severity: 'warn',
1183
+ severity: "warn",
1209
1184
  code: ErrorCode.SECRET_KEY_NOT_STATIC,
1210
- 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.`,
1211
1186
  });
1212
1187
  }
1213
1188
  }
@@ -1220,7 +1195,10 @@ export function computeDiagnostics(state) {
1220
1195
  // bridges that reference addon functions (id `http:<method>:<route>`, no
1221
1196
  // source file). The user can't edit any of these, so a destructure lint
1222
1197
  // meant to nudge them about their own code must not fail the build over them.
1223
- 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") {
1224
1202
  continue;
1225
1203
  }
1226
1204
  if (meta.services && !meta.services.optimized) {
@@ -1300,14 +1278,14 @@ export function validateScopeReferences(logger, state) {
1300
1278
  continue;
1301
1279
  for (const scope of meta.scopes) {
1302
1280
  // A trailing wildcard grants a subtree; the node it hangs off must exist.
1303
- const declaredForm = scope.endsWith(':*') ? scope.slice(0, -2) : scope;
1304
- if (scope === '*') {
1281
+ const declaredForm = scope.endsWith(":*") ? scope.slice(0, -2) : scope;
1282
+ if (scope === "*") {
1305
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.`);
1306
1284
  continue;
1307
1285
  }
1308
1286
  if (!declared.has(declaredForm)) {
1309
1287
  const available = Array.from(declared);
1310
- 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"}`);
1311
1289
  }
1312
1290
  }
1313
1291
  }
@@ -1334,12 +1312,12 @@ export function validateAgentToolReferences(logger, state, options = {}) {
1334
1312
  return;
1335
1313
  const { wireAddonDeclarations } = state.rpc;
1336
1314
  for (const [agentKey, agent] of agents) {
1337
- const where = agent.sourceFile ? ` (${agent.sourceFile})` : '';
1315
+ const where = agent.sourceFile ? ` (${agent.sourceFile})` : "";
1338
1316
  for (const tool of agent.tools ?? []) {
1339
1317
  // A workflow reference is resolved against the workflow map, not here.
1340
- if (tool.startsWith('workflow:'))
1318
+ if (tool.startsWith("workflow:"))
1341
1319
  continue;
1342
- const separator = tool.indexOf(':');
1320
+ const separator = tool.indexOf(":");
1343
1321
  let meta;
1344
1322
  if (separator === -1) {
1345
1323
  const funcId = state.rpc.internalMeta[tool] ?? tool;
@@ -1356,7 +1334,7 @@ export function validateAgentToolReferences(logger, state, options = {}) {
1356
1334
  if (!addon) {
1357
1335
  const known = Array.from(wireAddonDeclarations?.keys() ?? []);
1358
1336
  logger.critical(ErrorCode.AGENT_TOOL_UNKNOWN_NAMESPACE, `AI agent '${agentKey}'${where} references tool '${tool}', but no addon is wired under the namespace '${namespace}'. ` +
1359
- `Wired namespaces: ${known.join(', ') || 'none'}.`);
1337
+ `Wired namespaces: ${known.join(", ") || "none"}.`);
1360
1338
  continue;
1361
1339
  }
1362
1340
  // An addon that has not been built yet contributed no metadata, which
@@ -1373,7 +1351,7 @@ export function validateAgentToolReferences(logger, state, options = {}) {
1373
1351
  if (options.strictMeta && !meta.description) {
1374
1352
  logger.critical(ErrorCode.AGENT_TOOL_MISSING_DESCRIPTION, `AI agent '${agentKey}'${where} uses tool '${tool}', which has no description. ` +
1375
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. ` +
1376
- `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)" : ""}.`);
1377
1355
  }
1378
1356
  }
1379
1357
  }
@@ -1388,12 +1366,12 @@ export function validateAgentToolReferences(logger, state, options = {}) {
1388
1366
  export function validateAgentScorerReferences(logger, state) {
1389
1367
  const known = Object.keys(state.scorers.scorersMeta);
1390
1368
  for (const [agentKey, agent] of Object.entries(state.agents.agentsMeta)) {
1391
- const where = agent.sourceFile ? ` (${agent.sourceFile})` : '';
1369
+ const where = agent.sourceFile ? ` (${agent.sourceFile})` : "";
1392
1370
  for (const scorer of agent.scorers ?? []) {
1393
1371
  if (known.includes(scorer))
1394
1372
  continue;
1395
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. ` +
1396
- `Declared scorers: ${known.join(', ') || 'none'}.`);
1374
+ `Declared scorers: ${known.join(", ") || "none"}.`);
1397
1375
  }
1398
1376
  }
1399
1377
  }
@@ -1416,14 +1394,14 @@ export function validateSystemRoleScopes(logger, state) {
1416
1394
  const declared = new Set(flattenScopeDefinitions(state.scopes.definitions).map((s) => s.id));
1417
1395
  for (const role of state.systemRoles.definitions) {
1418
1396
  for (const scope of role.scopes) {
1419
- if (scope === '*') {
1397
+ if (scope === "*") {
1420
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.`);
1421
1399
  continue;
1422
1400
  }
1423
- const declaredForm = scope.endsWith(':*') ? scope.slice(0, -2) : scope;
1401
+ const declaredForm = scope.endsWith(":*") ? scope.slice(0, -2) : scope;
1424
1402
  if (!declared.has(declaredForm)) {
1425
1403
  const available = Array.from(declared);
1426
- 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"}`);
1427
1405
  }
1428
1406
  }
1429
1407
  }
@@ -1450,7 +1428,7 @@ export function validatePersonaRoles(logger, state) {
1450
1428
  const available = Array.from(declared).sort();
1451
1429
  logger.critical(ErrorCode.INVALID_VALUE, `Persona '${persona.id}' holds role '${role}', which is not declared with defineSystemRole. ` +
1452
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. ` +
1453
- `Declared roles: ${available.join(', ') || 'none'}`);
1431
+ `Declared roles: ${available.join(", ") || "none"}`);
1454
1432
  }
1455
1433
  }
1456
1434
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.94",
3
+ "version": "0.12.95",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",