@pikku/inspector 0.12.60 → 0.12.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +301 -0
  2. package/OPTIMIZATION-PLAN.md +19 -19
  3. package/dist/add/add-auth.js +1 -1
  4. package/dist/add/add-channel.d.ts +1 -1
  5. package/dist/add/add-cli.js +5 -0
  6. package/dist/add/add-functions.js +10 -1
  7. package/dist/add/add-http-route.js +18 -18
  8. package/dist/add/add-keyed-wiring.d.ts +2 -0
  9. package/dist/add/add-keyed-wiring.js +3 -1
  10. package/dist/add/add-personas.js +4 -1
  11. package/dist/add/add-secret.js +1 -0
  12. package/dist/add/add-variable.js +1 -0
  13. package/dist/add/add-wire-remote-addon.js +2 -1
  14. package/dist/add/add-workflow-graph.js +63 -6
  15. package/dist/add/add-workflow.d.ts +1 -1
  16. package/dist/error-codes.d.ts +3 -1
  17. package/dist/error-codes.js +3 -1
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.js +1 -0
  20. package/dist/inspector.js +12 -2
  21. package/dist/types.d.ts +25 -21
  22. package/dist/utils/annotate-http-route-auth.js +2 -1
  23. package/dist/utils/check-secret-sinks.js +2 -1
  24. package/dist/utils/compute-required-schemas.d.ts +1 -1
  25. package/dist/utils/contract-hashes.d.ts +2 -2
  26. package/dist/utils/contract-hashes.js +1 -1
  27. package/dist/utils/extract-function-name.js +1 -1
  28. package/dist/utils/extract-services.d.ts +1 -1
  29. package/dist/utils/extract-surface-usage.d.ts +29 -0
  30. package/dist/utils/extract-surface-usage.js +97 -0
  31. package/dist/utils/filter-inspector-state.js +53 -16
  32. package/dist/utils/filter-utils.d.ts +1 -1
  33. package/dist/utils/get-property-value.js +42 -3
  34. package/dist/utils/load-addon-functions-meta.js +31 -0
  35. package/dist/utils/middleware.d.ts +1 -1
  36. package/dist/utils/permissions.d.ts +1 -1
  37. package/dist/utils/post-process.d.ts +27 -7
  38. package/dist/utils/post-process.js +79 -14
  39. package/dist/utils/resolve-deploy-target.d.ts +1 -1
  40. package/dist/utils/resolve-function-meta.d.ts +1 -1
  41. package/dist/utils/resolve-function-types.d.ts +1 -1
  42. package/dist/utils/resolve-versions.js +1 -1
  43. package/dist/utils/schema-generator.d.ts +1 -1
  44. package/dist/utils/serialize-inspector-state.d.ts +2 -1
  45. package/dist/utils/serialize-inspector-state.js +2 -0
  46. package/dist/utils/serialize-openapi-json.d.ts +2 -2
  47. package/dist/utils/validate-auth-sessionless.js +2 -1
  48. package/dist/utils/workflow/derive-workflow-plan.d.ts +1 -1
  49. package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +1 -1
  50. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +1 -1
  51. package/dist/utils/workflow/graph/finalize-workflows.js +1 -1
  52. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +6 -1
  53. package/dist/utils/workflow/graph/serialize-workflow-graph.js +26 -2
  54. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +11 -2
  55. package/package.json +10 -10
  56. package/run-tests.sh +0 -0
  57. package/src/add/add-agent.ts +2 -4
  58. package/src/add/add-auth.ts +2 -2
  59. package/src/add/add-channel.ts +1 -4
  60. package/src/add/add-cli.ts +7 -1
  61. package/src/add/add-credential.ts +3 -6
  62. package/src/add/add-file-with-factory.ts +2 -6
  63. package/src/add/add-functions.ts +24 -31
  64. package/src/add/add-gateway.ts +1 -1
  65. package/src/add/add-http-route-ref.test.ts +149 -0
  66. package/src/add/add-http-route.ts +26 -29
  67. package/src/add/add-keyed-wiring.ts +8 -7
  68. package/src/add/add-permission.ts +1 -1
  69. package/src/add/add-personas.test.ts +26 -2
  70. package/src/add/add-personas.ts +4 -3
  71. package/src/add/add-scenario-step.test.ts +37 -2
  72. package/src/add/add-scope.test.ts +15 -15
  73. package/src/add/add-scope.ts +5 -9
  74. package/src/add/add-secret.ts +1 -0
  75. package/src/add/add-system-role.test.ts +4 -4
  76. package/src/add/add-system-role.ts +2 -4
  77. package/src/add/add-variable.ts +1 -0
  78. package/src/add/add-wire-remote-addon.ts +4 -1
  79. package/src/add/add-workflow-graph-foreach.test.ts +174 -0
  80. package/src/add/add-workflow-graph-input.test.ts +1 -1
  81. package/src/add/add-workflow-graph.ts +78 -6
  82. package/src/add/add-workflow-sleep-expression.test.ts +3 -4
  83. package/src/add/add-workflow.ts +1 -4
  84. package/src/add/addon-contracts.test.ts +1 -1
  85. package/src/add/config-value-metadata.test.ts +94 -0
  86. package/src/add/secret-sink-check.test.ts +4 -1
  87. package/src/error-codes.ts +3 -1
  88. package/src/index.ts +3 -4
  89. package/src/inspector.ts +19 -2
  90. package/src/types.ts +25 -28
  91. package/src/utils/annotate-http-route-auth.test.ts +5 -7
  92. package/src/utils/annotate-http-route-auth.ts +2 -1
  93. package/src/utils/check-secret-sinks.ts +4 -1
  94. package/src/utils/compute-required-schemas.ts +1 -1
  95. package/src/utils/contract-hashes.test.ts +1 -1
  96. package/src/utils/contract-hashes.ts +3 -3
  97. package/src/utils/extract-function-name.ts +1 -1
  98. package/src/utils/extract-services.ts +1 -2
  99. package/src/utils/extract-surface-usage.test.ts +147 -0
  100. package/src/utils/extract-surface-usage.ts +121 -0
  101. package/src/utils/filter-inspector-state.test.ts +138 -12
  102. package/src/utils/filter-inspector-state.ts +82 -21
  103. package/src/utils/filter-utils.ts +1 -1
  104. package/src/utils/get-property-value.ts +43 -5
  105. package/src/utils/load-addon-functions-meta.ts +40 -0
  106. package/src/utils/middleware.ts +1 -1
  107. package/src/utils/permissions.ts +1 -1
  108. package/src/utils/post-process.test.ts +59 -2
  109. package/src/utils/post-process.ts +88 -25
  110. package/src/utils/resolve-deploy-target.ts +1 -1
  111. package/src/utils/resolve-function-meta.ts +1 -4
  112. package/src/utils/resolve-function-types.ts +1 -1
  113. package/src/utils/resolve-versions.ts +2 -5
  114. package/src/utils/schema-generator.ts +3 -3
  115. package/src/utils/serialize-inspector-state.ts +4 -1
  116. package/src/utils/serialize-mcp-json.ts +1 -1
  117. package/src/utils/serialize-openapi-json.ts +2 -2
  118. package/src/utils/validate-auth-sessionless.ts +2 -1
  119. package/src/utils/validate-tags-resolve-to-middleware.test.ts +4 -1
  120. package/src/utils/workflow/derive-workflow-plan.test.ts +1 -1
  121. package/src/utils/workflow/derive-workflow-plan.ts +1 -1
  122. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +1 -1
  123. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +2 -2
  124. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +1 -1
  125. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +1 -4
  126. package/src/utils/workflow/graph/finalize-workflow-wires.ts +1 -1
  127. package/src/utils/workflow/graph/finalize-workflows.ts +2 -2
  128. package/src/utils/workflow/graph/serialize-workflow-graph.ts +34 -3
  129. package/src/utils/workflow/graph/workflow-graph.types.ts +12 -2
  130. package/tsconfig.tsbuildinfo +1 -1
  131. package/dist/add/add-ai-agent.d.ts +0 -4
  132. package/dist/add/add-ai-agent.js +0 -442
  133. package/dist/add/add-ai-scorer.d.ts +0 -2
  134. package/dist/add/add-ai-scorer.js +0 -61
  135. package/dist/utils/secret-brokers.d.ts +0 -2
  136. package/dist/utils/secret-brokers.js +0 -9
@@ -1,6 +1,7 @@
1
- import { parseVersionedId } from '@pikku/core/ecosystem/types';
1
+ import { parseVersionedId } from '@pikku/core/utils';
2
+ import { makeContextBasedId } from './extract-function-name.js';
2
3
  import { aggregateRequiredServices } from './post-process.js';
3
- import { resolveDeployTarget } from './resolve-deploy-target.js';
4
+ import { IncompatibleDeployTargetError, resolveDeployTarget, } from './resolve-deploy-target.js';
4
5
  // Module-level Set to track warned groups across multiple filter calls
5
6
  const globalWarnedGroups = new Set();
6
7
  /**
@@ -67,8 +68,9 @@ function repopulateFileSetFromMeta(meta, fallbackFiles, hasEntries) {
67
68
  * Check if metadata matches the given filters
68
69
  */
69
70
  function matchesFilters(filters, meta, logger, warnedGroups,
70
- // Set of pikkuFuncIds to keep based on the deploy filter; null when
71
- // the deploy filter is inactive.
71
+ // Kept pikkuFuncIds under the deploy filter; null when the filter is
72
+ // inactive. Asked about one id at a time, which is what lets a conflict in an
73
+ // addon's own metadata surface only for a function this app actually wires.
72
74
  keptByDeploy) {
73
75
  // If no filters, allow everything
74
76
  if (Object.keys(filters).length === 0)
@@ -137,9 +139,12 @@ keptByDeploy) {
137
139
  return false;
138
140
  }
139
141
  }
140
- const { baseName } = parseVersionedId(meta.name);
141
- const matchesNamePattern = (pattern) => matchesWildcard(meta.name, pattern) ||
142
- (baseName !== meta.name && matchesWildcard(baseName, pattern));
142
+ const candidateNames = [meta.name, ...(meta.altNames ?? [])];
143
+ const matchesNamePattern = (pattern) => candidateNames.some((candidate) => {
144
+ const { baseName } = parseVersionedId(candidate);
145
+ return (matchesWildcard(candidate, pattern) ||
146
+ (baseName !== candidate && matchesWildcard(baseName, pattern)));
147
+ });
143
148
  // Check name include filter (match against both full ID and base name for versioned functions)
144
149
  if (filters.names && filters.names.length > 0) {
145
150
  const nameMatches = filters.names.some(matchesNamePattern);
@@ -257,14 +262,48 @@ export function filterInspectorState(state, filters, logger) {
257
262
  : null;
258
263
  const incompatible = new Set(filters.serverlessIncompatible ?? []);
259
264
  const defaultTarget = filters.defaultTarget ?? 'serverless';
260
- keptByDeploy = new Set();
261
- for (const [funcId, funcMeta] of Object.entries(state.functions.meta)) {
262
- const target = resolveDeployTarget(funcMeta, incompatible, funcId, defaultTarget);
265
+ const kept = new Set();
266
+ // An addon publishes metadata for everything it exports, so a conflict in a
267
+ // function this app never wires is not this app's build error. Holding it
268
+ // here and raising it from `has` reports it at the one place the app is
269
+ // known to depend on that function.
270
+ const conflicts = new Map();
271
+ keptByDeploy = {
272
+ has: (funcId) => {
273
+ const conflict = conflicts.get(funcId);
274
+ if (conflict)
275
+ throw conflict;
276
+ return kept.has(funcId);
277
+ },
278
+ };
279
+ const keepByTarget = (funcId, funcMeta, incompatibleServices) => {
280
+ const target = resolveDeployTarget(funcMeta, incompatibleServices, funcId, defaultTarget);
263
281
  if (allowed && !allowed.has(target))
264
- continue;
282
+ return;
265
283
  if (excluded && excluded.has(target))
266
- continue;
267
- keptByDeploy.add(funcId);
284
+ return;
285
+ kept.add(funcId);
286
+ };
287
+ for (const [funcId, funcMeta] of Object.entries(state.functions.meta)) {
288
+ keepByTarget(funcId, funcMeta, incompatible);
289
+ }
290
+ // A wiring onto an addon function records the addon's own id, so the
291
+ // addon's published metadata is the only place its deploy target can be
292
+ // read from. Its serverlessIncompatible services are namespace-scoped —
293
+ // a service name means whatever the addon that declared it means.
294
+ for (const [namespace, addonMeta] of Object.entries(state.addonFunctions ?? {})) {
295
+ const addonIncompatible = new Set(state.addonServerlessIncompatible?.get(namespace) ?? []);
296
+ for (const [funcName, funcMeta] of Object.entries(addonMeta)) {
297
+ const funcId = `${namespace}:${funcName}`;
298
+ try {
299
+ keepByTarget(funcId, funcMeta, addonIncompatible);
300
+ }
301
+ catch (e) {
302
+ if (!(e instanceof IncompatibleDeployTargetError))
303
+ throw e;
304
+ conflicts.set(funcId, e);
305
+ }
306
+ }
268
307
  }
269
308
  }
270
309
  // Snapshot the original workflow graph meta before filtering prunes it
@@ -363,6 +402,7 @@ export function filterInspectorState(state, filters, logger) {
363
402
  const matches = matchesFilters(filters, {
364
403
  type: 'http',
365
404
  name: routeMeta.pikkuFuncId, // Use function name, not route
405
+ altNames: [makeContextBasedId('http', method, routeMeta.route)],
366
406
  tags: routeMeta.tags,
367
407
  filePath,
368
408
  httpRoute: routeMeta.route,
@@ -376,9 +416,6 @@ export function filterInspectorState(state, filters, logger) {
376
416
  // Track used functions/middleware/permissions
377
417
  if (routeMeta.pikkuFuncId) {
378
418
  filteredState.serviceAggregation.usedFunctions.add(routeMeta.pikkuFuncId);
379
- if (routeMeta.refTarget) {
380
- filteredState.serviceAggregation.usedFunctions.add(routeMeta.refTarget);
381
- }
382
419
  // For workflow/agent routes, also add the base name
383
420
  // so the workflow/agent definition survives pruning
384
421
  const colonIdx = routeMeta.pikkuFuncId.indexOf(':');
@@ -1,5 +1,5 @@
1
1
  import type { InspectorFilters, InspectorLogger } from '../types.js';
2
- import type { PikkuWiringTypes } from '@pikku/core/ecosystem/types';
2
+ import type { PikkuWiringTypes } from '@pikku/core/types';
3
3
  /**
4
4
  * Match a value against a pattern with wildcard support
5
5
  * Supports "*" at the beginning, end, or both (e.g., "send*", "*Payment", "*process*")
@@ -94,6 +94,45 @@ export const assertStringLiteralProperty = (obj, propertyName, wiringType, logge
94
94
  console.error(errorMsg);
95
95
  }
96
96
  };
97
+ /**
98
+ * Fold a node that is a compile-time constant string down to its value.
99
+ *
100
+ * The fallback below this is `getText()`, which returns the node's *source*.
101
+ * For a lone `'…'` that is indistinguishable from the value, so nobody noticed
102
+ * that anything written as an expression comes through raw: a `description`
103
+ * split across lines as `'Fetch the thing ' + 'from upstream'` reaches
104
+ * `infra.json` with the quotes and the `+` still in it, and is rendered that
105
+ * way in the fabric console.
106
+ *
107
+ * Deliberately checker-free, unlike `extractStringLiteral`. Every value this
108
+ * resolves is a literal in the same expression, so no symbol resolution is
109
+ * needed, and `getPropertyValue` is called from a dozen places that have no
110
+ * checker to hand. Anything it cannot fold — an identifier, a template with a
111
+ * substitution — returns `undefined` and takes the old path, so this only ever
112
+ * turns a wrong answer into a right one.
113
+ */
114
+ const foldStaticString = (node) => {
115
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
116
+ return node.text;
117
+ }
118
+ if (ts.isParenthesizedExpression(node)) {
119
+ return foldStaticString(node.expression);
120
+ }
121
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
122
+ return foldStaticString(node.expression);
123
+ }
124
+ if (ts.isBinaryExpression(node) &&
125
+ node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
126
+ const left = foldStaticString(node.left);
127
+ if (left === undefined)
128
+ return undefined;
129
+ const right = foldStaticString(node.right);
130
+ if (right === undefined)
131
+ return undefined;
132
+ return left + right;
133
+ }
134
+ return undefined;
135
+ };
97
136
  export const getPropertyValue = (obj, propertyName) => {
98
137
  const property = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
99
138
  ts.isIdentifier(p.name) &&
@@ -126,9 +165,9 @@ export const getPropertyValue = (obj, propertyName) => {
126
165
  }
127
166
  return Number(initializer.text);
128
167
  }
129
- if (ts.isStringLiteral(initializer) ||
130
- ts.isNoSubstitutionTemplateLiteral(initializer)) {
131
- return initializer.text;
168
+ const staticString = foldStaticString(initializer);
169
+ if (staticString !== undefined) {
170
+ return staticString;
132
171
  }
133
172
  return initializer.getText();
134
173
  }
@@ -1,3 +1,4 @@
1
+ import { existsSync } from 'fs';
1
2
  import { readFile, readdir } from 'fs/promises';
2
3
  import { createRequire } from 'module';
3
4
  import { join, dirname } from 'path';
@@ -88,6 +89,35 @@ const resolveAddonMeta = (require, packageName, baseName) => {
88
89
  }
89
90
  return null;
90
91
  };
92
+ /**
93
+ * Give the addon's input/output type names a declaration file to be imported
94
+ * from, so a wiring that references an addon function can name its contract.
95
+ *
96
+ * Without this the only thing that knows where `GetScenarioArtifactInput` lives
97
+ * is the type checker, and it only knows once the consumer's *own* rpc map has
98
+ * already been generated with the addon's routes in it — which is to say on the
99
+ * second run. A first, cold run resolved `ref('console:getScenarioArtifact')`
100
+ * against a map that did not mention it yet and fell back to the whole
101
+ * `FlattenedRPCMap`, so the same source produced a different http-map depending
102
+ * on whether `.pikku` happened to be there. The addon publishes these types at a
103
+ * fixed subpath; reading them from the metadata makes the first run agree with
104
+ * the second.
105
+ */
106
+ const registerAddonTypes = (state, metaPath, meta) => {
107
+ // Derived from the metadata's own location rather than resolved as a package
108
+ // subpath: the addon exports that declaration under a `types`-only condition,
109
+ // which `require.resolve` will not follow.
110
+ const typesPath = join(dirname(metaPath), '..', 'rpc', 'pikku-rpc-wirings-map.internal.gen.d.ts');
111
+ if (!existsSync(typesPath))
112
+ return;
113
+ for (const funcMeta of Object.values(meta)) {
114
+ for (const name of [funcMeta.inputSchemaName, funcMeta.outputSchemaName]) {
115
+ if (!name || state.functions.typesMap.exists(name, typesPath))
116
+ continue;
117
+ state.functions.typesMap.addType(name, typesPath);
118
+ }
119
+ }
120
+ };
91
121
  /**
92
122
  * After the setup sweep discovers wireAddon() declarations, load each addon
93
123
  * package's function metadata so that wiring handlers (channels, HTTP routes,
@@ -116,6 +146,7 @@ export async function loadAddonFunctionsMeta(logger, state) {
116
146
  const meta = JSON.parse(raw);
117
147
  state.addonFunctions[namespace] = meta;
118
148
  logger.debug(`Loaded ${Object.keys(meta).length} addon functions for '${namespace}' from ${decl.package}`);
149
+ registerAddonTypes(state, metaPath, meta);
119
150
  // If wireAddon has mcp: true, expose addon functions with mcp: true as MCP tools
120
151
  if (decl.mcp) {
121
152
  for (const [funcName, funcMeta] of Object.entries(meta)) {
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import type { MiddlewareMetadata } from '@pikku/core/ecosystem/types';
2
+ import type { MiddlewareMetadata } from '@pikku/core/middleware';
3
3
  import type { InspectorState } from '../types.js';
4
4
  export interface MiddlewareRef {
5
5
  definitionId: string;
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import type { PermissionMetadata } from '@pikku/core/ecosystem/types';
2
+ import type { PermissionMetadata } from '@pikku/core/function';
3
3
  import type { InspectorState } from '../types.js';
4
4
  /**
5
5
  * Extract permission pikkuFuncIds from an expression (array or object literal)
@@ -1,5 +1,6 @@
1
1
  import type { InspectorState, InspectorLogger, InspectorOptions } from '../types.js';
2
- import type { MiddlewareMetadata, PermissionMetadata } from '@pikku/core/ecosystem/types';
2
+ import type { PermissionMetadata } from '@pikku/core/function';
3
+ import type { MiddlewareMetadata } from '@pikku/core/middleware';
3
4
  import type ts from 'typescript';
4
5
  /**
5
6
  * Stamp the inspected authorize/callbacks service set onto the generated auth
@@ -33,10 +34,27 @@ export declare function validateSecretOverrides(logger: InspectorLogger, state:
33
34
  export declare function validateCredentialOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
34
35
  export declare function validateVariableOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
35
36
  /**
37
+ * Every wired addon must be installed, in the bucket matching how it is used.
38
+ *
36
39
  * A `wireRemoteAddon` package ships types only — its handlers run on the host —
37
40
  * so it MUST be a devDependency, not a production dependency (a prod dep would
38
- * drag in the runtime deps remote consumption exists to avoid). This is the
39
- * mirror image of `wireAddon`, which requires a production dependency.
41
+ * drag in the runtime deps remote consumption exists to avoid).
42
+ *
43
+ * A plain `wireAddon` only has to be *resolvable*. The failure it allows is
44
+ * quiet in a way the remote one is not — an absent package makes the metadata
45
+ * load fail, which is caught and downgraded to a warning, so every
46
+ * `ref('<namespace>:…')` resolves against nothing and the surface is dead at
47
+ * runtime. The generated console is the common case: `scaffold.console` emits
48
+ * `wireAddon({ package: '@pikku/addon-console' })` and refs into it, and a
49
+ * project that never installed the package gets a console that 404s with
50
+ * nothing in the build output saying why.
51
+ *
52
+ * Deliberately a resolution check and not a `package.json` one. An addon is
53
+ * discovered because it is *wired*, not because it is a declared dependency —
54
+ * addons arrive by workspace link, by a local `addons/` directory recorded in
55
+ * `pikku-addons.json`, and by plain install, and only the last of those shows
56
+ * up in `dependencies`. Asking the resolver is the only question that matches
57
+ * what the loader will actually do.
40
58
  */
41
59
  export declare function validateRemoteAddonDependencies(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
42
60
  /**
@@ -64,6 +82,7 @@ export declare function computeRequiredSchemas(state: InspectorState, options: I
64
82
  * the functions that are actually invoked, so this blocks CI through
65
83
  * `--fail-on-error` without failing a dev server over a function nobody calls.
66
84
  */
85
+ export declare function unresolvedSchemaReferences(state: InspectorState): string[];
67
86
  export declare function validateSchemaReferences(logger: InspectorLogger, state: InspectorState): void;
68
87
  /**
69
88
  * A model is either concrete (`provider/model`) or an alias, which must appear
@@ -89,10 +108,11 @@ export declare function validateScenarioServices(logger: InspectorLogger, state:
89
108
  *
90
109
  * - the step target must be a static string literal (PKU678) — otherwise it
91
110
  * can't be bundled, typed or drawn;
92
- * - a step declaring a `browser` binding must be given an actor (PKU677), since
93
- * the browser context signs in as that persona. It is checked statically even
94
- * though the binding only runs on a browser run: a step that can never be
95
- * driven by a human is a latent failure, not a passing default run.
111
+ * - a step that runs as a persona must be given one (PKU677) — declared by
112
+ * `actor: true`, or implied by a `browser` binding, since the browser context
113
+ * signs in as that persona. It is checked statically even though a browser
114
+ * binding only runs on a browser run: a step that can never be driven by a
115
+ * human is a latent failure, not a passing default run.
96
116
  * - a scenario written as a step ladder must assert something (PKU680).
97
117
  *
98
118
  * It also marks the step's function as used, so `scenario run` boots the
@@ -1,11 +1,12 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
2
3
  import { join } from 'node:path';
3
4
  import { extractTypeKeys } from './type-utils.js';
4
5
  import { ErrorCode } from '../error-codes.js';
5
6
  import { findSecretAliasServices } from './secret-alias-services.js';
6
7
  import { relative } from 'node:path';
7
8
  import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js';
8
- import { flattenScopeDefinitions } from '@pikku/core/ecosystem/scope';
9
+ import { flattenScopeDefinitions } from '@pikku/core/scope';
9
10
  import { DYNAMIC_SCENARIO_STEP_TARGET } from './workflow/dsl/patterns.js';
10
11
  /**
11
12
  * Stamp the inspected authorize/callbacks service set onto the generated auth
@@ -374,17 +375,74 @@ export function validateVariableOverrides(logger, state) {
374
375
  }
375
376
  }
376
377
  /**
378
+ * Can `pkg` be resolved from `rootDir`, by any means the loader would use?
379
+ *
380
+ * Resolution is attempted against `<pkg>/package.json` first and the bare
381
+ * specifier second, because neither alone is conclusive. A package with a
382
+ * restrictive `exports` map refuses the first with
383
+ * `ERR_PACKAGE_PATH_NOT_EXPORTED` — which is not a failure at all: reaching the
384
+ * exports map means the package was found. Only a module-not-found from both
385
+ * attempts means it is genuinely absent.
386
+ */
387
+ const isPackageResolvable = (pkg, rootDir) => {
388
+ const NOT_FOUND = new Set(['MODULE_NOT_FOUND', 'ERR_MODULE_NOT_FOUND']);
389
+ let req;
390
+ try {
391
+ req = createRequire(join(rootDir, 'package.json'));
392
+ }
393
+ catch {
394
+ return true; // cannot ask; do not accuse
395
+ }
396
+ for (const specifier of [`${pkg}/package.json`, pkg]) {
397
+ try {
398
+ req.resolve(specifier);
399
+ return true;
400
+ }
401
+ catch (e) {
402
+ if (!NOT_FOUND.has(e?.code))
403
+ return true;
404
+ }
405
+ }
406
+ return false;
407
+ };
408
+ /**
409
+ * Every wired addon must be installed, in the bucket matching how it is used.
410
+ *
377
411
  * A `wireRemoteAddon` package ships types only — its handlers run on the host —
378
412
  * so it MUST be a devDependency, not a production dependency (a prod dep would
379
- * drag in the runtime deps remote consumption exists to avoid). This is the
380
- * mirror image of `wireAddon`, which requires a production dependency.
413
+ * drag in the runtime deps remote consumption exists to avoid).
414
+ *
415
+ * A plain `wireAddon` only has to be *resolvable*. The failure it allows is
416
+ * quiet in a way the remote one is not — an absent package makes the metadata
417
+ * load fail, which is caught and downgraded to a warning, so every
418
+ * `ref('<namespace>:…')` resolves against nothing and the surface is dead at
419
+ * runtime. The generated console is the common case: `scaffold.console` emits
420
+ * `wireAddon({ package: '@pikku/addon-console' })` and refs into it, and a
421
+ * project that never installed the package gets a console that 404s with
422
+ * nothing in the build output saying why.
423
+ *
424
+ * Deliberately a resolution check and not a `package.json` one. An addon is
425
+ * discovered because it is *wired*, not because it is a declared dependency —
426
+ * addons arrive by workspace link, by a local `addons/` directory recorded in
427
+ * `pikku-addons.json`, and by plain install, and only the last of those shows
428
+ * up in `dependencies`. Asking the resolver is the only question that matches
429
+ * what the loader will actually do.
381
430
  */
382
431
  export function validateRemoteAddonDependencies(logger, state) {
383
432
  const { wireAddonDeclarations } = state.rpc;
384
433
  if (!wireAddonDeclarations || wireAddonDeclarations.size === 0)
385
434
  return;
386
- const hasRemote = Array.from(wireAddonDeclarations.values()).some((d) => d.remote);
387
- if (!hasRemote)
435
+ // Local addons first, and independently of the manifest below: their check
436
+ // asks the resolver, not `package.json`, so a project without a manifest (as
437
+ // in some tests) must not skip it.
438
+ for (const [namespace, decl] of wireAddonDeclarations.entries()) {
439
+ if (decl.remote)
440
+ continue;
441
+ if (isPackageResolvable(decl.package, state.rootDir))
442
+ continue;
443
+ logger.critical(ErrorCode.ADDON_NOT_INSTALLED, `Addon '${namespace}' ('${decl.package}') is wired with wireAddon but cannot be resolved — every ref('${namespace}:…') will resolve to nothing and the surface will be dead at runtime. Install it, or remove the wireAddon call.`);
444
+ }
445
+ if (!Array.from(wireAddonDeclarations.values()).some((d) => d.remote))
388
446
  return;
389
447
  const pkgJsonPath = join(state.rootDir, 'package.json');
390
448
  if (!existsSync(pkgJsonPath))
@@ -579,7 +637,7 @@ export function computeRequiredSchemas(state, options) {
579
637
  * the functions that are actually invoked, so this blocks CI through
580
638
  * `--fail-on-error` without failing a dev server over a function nobody calls.
581
639
  */
582
- export function validateSchemaReferences(logger, state) {
640
+ export function unresolvedSchemaReferences(state) {
583
641
  const { functions, schemas } = state;
584
642
  const available = new Set(Object.keys(schemas ?? {}));
585
643
  const unresolved = [];
@@ -604,6 +662,10 @@ export function validateSchemaReferences(logger, state) {
604
662
  unresolved.push(`${funcName}.${role} → '${resolved}'`);
605
663
  }
606
664
  }
665
+ return unresolved;
666
+ }
667
+ export function validateSchemaReferences(logger, state) {
668
+ const unresolved = unresolvedSchemaReferences(state);
607
669
  if (unresolved.length === 0)
608
670
  return;
609
671
  logger.diagnostic({
@@ -674,10 +736,11 @@ export function validateScenarioServices(logger, state) {
674
736
  *
675
737
  * - the step target must be a static string literal (PKU678) — otherwise it
676
738
  * can't be bundled, typed or drawn;
677
- * - a step declaring a `browser` binding must be given an actor (PKU677), since
678
- * the browser context signs in as that persona. It is checked statically even
679
- * though the binding only runs on a browser run: a step that can never be
680
- * driven by a human is a latent failure, not a passing default run.
739
+ * - a step that runs as a persona must be given one (PKU677) — declared by
740
+ * `actor: true`, or implied by a `browser` binding, since the browser context
741
+ * signs in as that persona. It is checked statically even though a browser
742
+ * binding only runs on a browser run: a step that can never be driven by a
743
+ * human is a latent failure, not a passing default run.
681
744
  * - a scenario written as a step ladder must assert something (PKU680).
682
745
  *
683
746
  * It also marks the step's function as used, so `scenario run` boots the
@@ -697,10 +760,12 @@ export function validateScenarioSteps(logger, state) {
697
760
  }
698
761
  state.serviceAggregation.usedFunctions.add(step.stepFunc);
699
762
  const stepMeta = state.functions.meta[step.stepFunc];
700
- if (stepMeta?.scenarioStepSurfaces?.includes('browser') &&
701
- !step.actor) {
702
- logger.critical(ErrorCode.SCENARIO_BROWSER_STEP_NEEDS_ACTOR, `Scenario '${workflowName}' calls step '${step.stepFunc}', which declares a browser binding, without an actor. ` +
703
- `Pass { actor: actors.<name> } so the browser context signs in as that persona.`);
763
+ if (stepMeta?.scenarioStepRequiresActor && !step.actor) {
764
+ const why = stepMeta.scenarioStepSurfaces?.includes('browser')
765
+ ? 'declares a browser binding'
766
+ : 'is driven by a persona';
767
+ logger.critical(ErrorCode.SCENARIO_STEP_NEEDS_ACTOR, `Scenario '${workflowName}' calls step '${step.stepFunc}', which ${why}, without an actor. ` +
768
+ `Pass { actor: actors.<name> } so it runs as that persona.`);
704
769
  }
705
770
  continue;
706
771
  }
@@ -1,4 +1,4 @@
1
- import type { FunctionMeta } from '@pikku/core/ecosystem/services';
1
+ import type { FunctionMeta } from '@pikku/core/services';
2
2
  /**
3
3
  * Thrown when a function's explicit `deploy: 'serverless'` conflicts
4
4
  * with one of its services being declared `serverlessIncompatible`.
@@ -1,4 +1,4 @@
1
- import type { FunctionMeta, FunctionsMeta } from '@pikku/core/ecosystem/services';
1
+ import type { FunctionMeta, FunctionsMeta } from '@pikku/core/services';
2
2
  /**
3
3
  * Look up function metadata by pikkuFuncId, checking both local functions
4
4
  * and addon functions. Addon functions use namespaced IDs like 'namespace:funcName'.
@@ -1,5 +1,5 @@
1
1
  import type { TypesMap } from '../types-map.js';
2
- import type { FunctionsMeta } from '@pikku/core/ecosystem/services';
2
+ import type { FunctionsMeta } from '@pikku/core/services';
3
3
  export declare function resolveFunctionIOTypes(pikkuFuncId: string, functionsMeta: FunctionsMeta, typesMap: TypesMap, requiredTypes: Set<string>): {
4
4
  inputType: string;
5
5
  outputType: string;
@@ -1,4 +1,4 @@
1
- import { parseVersionedId, formatVersionedId, } from '@pikku/core/ecosystem/types';
1
+ import { parseVersionedId, formatVersionedId } from '@pikku/core/utils';
2
2
  import { ErrorCode } from '../error-codes.js';
3
3
  export function resolveLatestVersions(state, logger) {
4
4
  const functionsMeta = state.functions.meta;
@@ -1,4 +1,4 @@
1
- import type { JSONValue } from '@pikku/core/ecosystem/types';
1
+ import type { JSONValue } from '@pikku/core/utils';
2
2
  import type { InspectorLogger, InspectorState } from '../types.js';
3
3
  /** One file the generated schemas were derived from, as of when they were generated. */
4
4
  export type SchemaDep = {
@@ -1,5 +1,5 @@
1
1
  import type { SecretUsage } from './extract-secret-usage.js';
2
- import type { JSONValue } from '@pikku/core/ecosystem/types';
2
+ import type { JSONValue } from '@pikku/core/utils';
3
3
  import type { InspectorDiagnostic, InspectorFeature, InspectorState } from '../types.js';
4
4
  /**
5
5
  * Serializable version of InspectorState that can be saved to JSON
@@ -242,6 +242,7 @@ export interface SerializableInspectorState {
242
242
  files: string[];
243
243
  usage: Record<string, SecretUsage>;
244
244
  };
245
+ surfaceUsage: InspectorState['surfaceUsage'];
245
246
  credentials: {
246
247
  definitions: InspectorState['credentials']['definitions'];
247
248
  files: string[];
@@ -118,6 +118,7 @@ export function serializeInspectorState(state) {
118
118
  files: Array.from(state.secrets.files),
119
119
  usage: Object.fromEntries(state.secrets.usage),
120
120
  },
121
+ surfaceUsage: state.surfaceUsage,
121
122
  credentials: {
122
123
  definitions: state.credentials.definitions,
123
124
  files: Array.from(state.credentials.files),
@@ -295,6 +296,7 @@ export function deserializeInspectorState(data) {
295
296
  files: new Set(data.secrets?.files || []),
296
297
  usage: new Map(Object.entries(data.secrets?.usage || {})),
297
298
  },
299
+ surfaceUsage: data.surfaceUsage || {},
298
300
  credentials: {
299
301
  definitions: data.credentials?.definitions || [],
300
302
  files: new Set(data.credentials?.files || []),
@@ -1,5 +1,5 @@
1
- import type { FunctionsMeta } from '@pikku/core/ecosystem/services';
2
- import type { HTTPWiringsMeta } from '@pikku/core/ecosystem/http';
1
+ import type { FunctionsMeta } from '@pikku/core/services';
2
+ import type { HTTPWiringsMeta } from '@pikku/core/http';
3
3
  import type { InspectorLogger } from '../types.js';
4
4
  interface ErrorDetails {
5
5
  status: number;
@@ -1,7 +1,8 @@
1
1
  import { getPropertyValue } from './get-property-value.js';
2
2
  import { ErrorCode } from '../error-codes.js';
3
+ import { resolveFunctionMeta } from './resolve-function-meta.js';
3
4
  export function validateAuthSessionless(logger, obj, state, funcName, wireDescription, inheritedAuth) {
4
- const fnMeta = state.functions.meta[funcName];
5
+ const fnMeta = resolveFunctionMeta(state, funcName);
5
6
  if (!fnMeta)
6
7
  return true;
7
8
  const routeAuth = getPropertyValue(obj, 'auth');
@@ -1,4 +1,4 @@
1
- import type { WorkflowStepMeta, WorkflowPlannedStep } from '@pikku/core/ecosystem/workflow';
1
+ import type { WorkflowStepMeta, WorkflowPlannedStep } from '@pikku/core/workflow';
2
2
  /**
3
3
  * Derive the static UI plan for a DSL workflow from its extracted step tree.
4
4
  *
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import type { WorkflowStepMeta, WorkflowContext } from '@pikku/core/ecosystem/workflow';
2
+ import type { WorkflowStepMeta, WorkflowContext } from '@pikku/core/workflow';
3
3
  /**
4
4
  * Result of DSL workflow extraction
5
5
  */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Converts DSL (Domain Specific Language) step-based format to graph node format
3
3
  */
4
- import type { WorkflowsMeta } from '@pikku/core/ecosystem/workflow';
4
+ import type { WorkflowsMeta } from '@pikku/core/workflow';
5
5
  import type { SerializedWorkflowGraph } from './workflow-graph.types.js';
6
6
  /**
7
7
  * Convert a DSL workflow to graph format
@@ -1,4 +1,4 @@
1
- import { isVersionedId, formatVersionedId, parseVersionedId, } from '@pikku/core/ecosystem/types';
1
+ import { isVersionedId, formatVersionedId, parseVersionedId, } from '@pikku/core/utils';
2
2
  import { canonicalJSON, hashString } from '../../hash.js';
3
3
  import { convertDslToGraph } from './convert-dsl-to-graph.js';
4
4
  import { deriveWorkflowPlan } from '../derive-workflow-plan.js';
@@ -11,7 +11,12 @@ export declare function serializeWorkflowGraph(definition: {
11
11
  func: {
12
12
  name?: string;
13
13
  };
14
- input?: (ref: any) => Record<string, unknown>;
14
+ input?: (ref: any, template?: any, $item?: any) => Record<string, unknown>;
15
+ forEach?: string | {
16
+ nodeId: string;
17
+ path?: string;
18
+ } | ((ref: any) => unknown);
19
+ mode?: 'parallel' | 'sequential';
15
20
  next?: string | string[] | Record<string, string | string[]>;
16
21
  onError?: string | string[];
17
22
  notes?: string;