@pikku/inspector 0.12.59 → 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 (132) hide show
  1. package/CHANGELOG.md +373 -0
  2. package/OPTIMIZATION-PLAN.md +19 -19
  3. package/dist/add/add-agent-scorer.d.ts +2 -0
  4. package/dist/add/{add-ai-scorer.js → add-agent-scorer.js} +4 -4
  5. package/dist/add/{add-ai-agent.d.ts → add-agent.d.ts} +1 -1
  6. package/dist/add/{add-ai-agent.js → add-agent.js} +11 -11
  7. package/dist/add/add-auth.js +1 -1
  8. package/dist/add/add-cli.js +5 -0
  9. package/dist/add/add-functions.js +10 -1
  10. package/dist/add/add-http-route.js +18 -18
  11. package/dist/add/add-keyed-wiring.d.ts +2 -0
  12. package/dist/add/add-keyed-wiring.js +3 -1
  13. package/dist/add/add-middleware.js +5 -5
  14. package/dist/add/add-personas.js +3 -0
  15. package/dist/add/add-secret.js +1 -0
  16. package/dist/add/add-variable.js +1 -0
  17. package/dist/add/add-wire-remote-addon.js +2 -1
  18. package/dist/add/add-workflow-graph.js +63 -6
  19. package/dist/error-codes.d.ts +3 -1
  20. package/dist/error-codes.js +3 -1
  21. package/dist/index.d.ts +3 -1
  22. package/dist/index.js +1 -0
  23. package/dist/inspector.js +13 -3
  24. package/dist/types.d.ts +10 -5
  25. package/dist/utils/annotate-http-route-auth.js +2 -1
  26. package/dist/utils/check-secret-sinks.js +2 -1
  27. package/dist/utils/compute-required-schemas.d.ts +1 -1
  28. package/dist/utils/contract-hashes.d.ts +2 -1
  29. package/dist/utils/contract-hashes.js +1 -1
  30. package/dist/utils/extract-function-name.js +1 -1
  31. package/dist/utils/extract-services.d.ts +1 -1
  32. package/dist/utils/extract-surface-usage.d.ts +29 -0
  33. package/dist/utils/extract-surface-usage.js +97 -0
  34. package/dist/utils/filter-inspector-state.js +53 -16
  35. package/dist/utils/filter-utils.d.ts +1 -1
  36. package/dist/utils/get-property-value.js +42 -3
  37. package/dist/utils/load-addon-functions-meta.js +31 -0
  38. package/dist/utils/middleware.d.ts +2 -2
  39. package/dist/utils/middleware.js +5 -5
  40. package/dist/utils/permissions.d.ts +1 -1
  41. package/dist/utils/post-process.d.ts +28 -8
  42. package/dist/utils/post-process.js +83 -18
  43. package/dist/utils/resolve-deploy-target.d.ts +1 -1
  44. package/dist/utils/resolve-function-meta.d.ts +1 -1
  45. package/dist/utils/resolve-function-types.d.ts +1 -1
  46. package/dist/utils/resolve-versions.js +1 -1
  47. package/dist/utils/schema-generator.d.ts +1 -1
  48. package/dist/utils/serialize-inspector-state.d.ts +4 -3
  49. package/dist/utils/serialize-inspector-state.js +6 -4
  50. package/dist/utils/serialize-openapi-json.d.ts +1 -1
  51. package/dist/utils/validate-auth-sessionless.js +2 -1
  52. package/dist/utils/workflow/graph/finalize-workflows.js +1 -1
  53. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +6 -1
  54. package/dist/utils/workflow/graph/serialize-workflow-graph.js +26 -2
  55. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +9 -0
  56. package/dist/visit.js +4 -4
  57. package/package.json +10 -10
  58. package/run-tests.sh +0 -0
  59. package/src/add/{add-ai-scorer.test.ts → add-agent-scorer.test.ts} +11 -11
  60. package/src/add/{add-ai-scorer.ts → add-agent-scorer.ts} +4 -4
  61. package/src/add/{add-ai-agent.test.ts → add-agent.test.ts} +1 -1
  62. package/src/add/{add-ai-agent.ts → add-agent.ts} +13 -21
  63. package/src/add/add-auth.ts +2 -2
  64. package/src/add/add-cli.ts +6 -0
  65. package/src/add/add-credential.ts +3 -6
  66. package/src/add/add-file-with-factory.ts +2 -6
  67. package/src/add/add-functions.ts +23 -25
  68. package/src/add/add-http-route-ref.test.ts +149 -0
  69. package/src/add/add-http-route.ts +25 -28
  70. package/src/add/add-keyed-wiring.ts +8 -7
  71. package/src/add/add-middleware.ts +7 -5
  72. package/src/add/add-permission.ts +1 -1
  73. package/src/add/add-personas.test.ts +24 -0
  74. package/src/add/add-personas.ts +3 -1
  75. package/src/add/add-scenario-step.test.ts +35 -0
  76. package/src/add/add-scope.test.ts +4 -4
  77. package/src/add/add-scope.ts +4 -8
  78. package/src/add/add-secret.ts +1 -0
  79. package/src/add/add-system-role.test.ts +8 -8
  80. package/src/add/add-system-role.ts +2 -4
  81. package/src/add/add-variable.ts +1 -0
  82. package/src/add/add-wire-remote-addon.ts +4 -1
  83. package/src/add/add-workflow-graph-foreach.test.ts +174 -0
  84. package/src/add/add-workflow-graph.ts +78 -6
  85. package/src/add/add-workflow-sleep-expression.test.ts +3 -4
  86. package/src/add/addon-contracts.test.ts +1 -1
  87. package/src/add/config-value-metadata.test.ts +94 -0
  88. package/src/add/secret-sink-check.test.ts +4 -1
  89. package/src/error-codes.ts +3 -1
  90. package/src/index.ts +3 -4
  91. package/src/inspector.ts +20 -3
  92. package/src/types.ts +10 -7
  93. package/src/utils/annotate-http-route-auth.test.ts +5 -7
  94. package/src/utils/annotate-http-route-auth.ts +2 -1
  95. package/src/utils/check-secret-sinks.ts +4 -1
  96. package/src/utils/compute-required-schemas.ts +1 -1
  97. package/src/utils/contract-hashes.test.ts +1 -1
  98. package/src/utils/contract-hashes.ts +3 -2
  99. package/src/utils/extract-function-name.ts +1 -1
  100. package/src/utils/extract-services.ts +4 -2
  101. package/src/utils/extract-surface-usage.test.ts +147 -0
  102. package/src/utils/extract-surface-usage.ts +121 -0
  103. package/src/utils/filter-inspector-state.test.ts +138 -12
  104. package/src/utils/filter-inspector-state.ts +82 -21
  105. package/src/utils/filter-utils.ts +1 -1
  106. package/src/utils/get-property-value.ts +43 -5
  107. package/src/utils/load-addon-functions-meta.ts +40 -0
  108. package/src/utils/middleware.test.ts +23 -23
  109. package/src/utils/middleware.ts +6 -6
  110. package/src/utils/permissions.ts +1 -1
  111. package/src/utils/post-process.test.ts +68 -11
  112. package/src/utils/post-process.ts +91 -28
  113. package/src/utils/resolve-deploy-target.ts +1 -1
  114. package/src/utils/resolve-function-meta.ts +1 -1
  115. package/src/utils/resolve-function-types.ts +1 -1
  116. package/src/utils/resolve-versions.ts +1 -1
  117. package/src/utils/schema-generator.ts +2 -1
  118. package/src/utils/serialize-inspector-state.ts +10 -7
  119. package/src/utils/serialize-mcp-json.ts +1 -1
  120. package/src/utils/serialize-openapi-json.ts +1 -1
  121. package/src/utils/validate-auth-sessionless.ts +2 -1
  122. package/src/utils/validate-tags-resolve-to-middleware.test.ts +4 -1
  123. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +4 -1
  124. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +1 -1
  125. package/src/utils/workflow/graph/finalize-workflows.ts +6 -2
  126. package/src/utils/workflow/graph/serialize-workflow-graph.ts +34 -3
  127. package/src/utils/workflow/graph/workflow-graph.types.ts +10 -0
  128. package/src/visit.ts +4 -4
  129. package/tsconfig.tsbuildinfo +1 -1
  130. package/dist/add/add-ai-scorer.d.ts +0 -2
  131. package/dist/utils/secret-brokers.d.ts +0 -2
  132. package/dist/utils/secret-brokers.js +0 -9
@@ -128,22 +128,27 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
128
128
  const packageName = ts.isIdentifier(funcInitializer)
129
129
  ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
130
130
  : null;
131
+ // A ref()-wired route points straight at the addon function, the same way the
132
+ // CLI and channel adders record one. The addon's own metadata is the contract
133
+ // — and it is authoritative where the checker is not: `ref('ns:fn')` only
134
+ // types precisely once the consumer's own rpc map already lists the addon's
135
+ // functions, so a cold run inferred the whole `FlattenedRPCMap` and a warm one
136
+ // inferred the real input. The addon's metadata says which it is either way.
137
+ //
138
+ // The route stays the consuming app's all the same, so it records no
139
+ // packageName: that names the package a wire *runs in*, and running this one
140
+ // inside the addon would hand it the addon's middleware and services instead
141
+ // of the app's. `ref` reaches the addon over RPC, which enters it properly.
131
142
  if (refAddonTarget) {
132
143
  const targetMeta = resolveFunctionMeta(state, refAddonTarget);
133
144
  if (!targetMeta) {
134
145
  logger.warn(`Skipping route '${fullRoute}': addon function metadata for '${refAddonTarget}' is not available yet.`);
135
146
  return;
136
147
  }
148
+ funcName = refAddonTarget;
137
149
  }
138
- ensureFunctionMetadata(state, funcName, fullRoute, funcInitializer, checker, extracted.isHelper);
139
- // Propagate sessionless from the addon target so the runtime can correctly
140
- // determine whether a session is required for ref()-based routes.
141
- if (refAddonTarget) {
142
- const targetMeta = resolveFunctionMeta(state, refAddonTarget);
143
- const inlineMeta = state.functions.meta[funcName];
144
- if (targetMeta && inlineMeta && targetMeta.sessionless !== undefined) {
145
- inlineMeta.sessionless = targetMeta.sessionless;
146
- }
150
+ else {
151
+ ensureFunctionMetadata(state, funcName, fullRoute, funcInitializer, checker, extracted.isHelper);
147
152
  }
148
153
  // The route's own `auth`, falling back to the group's. Recorded on the meta
149
154
  // below so an audit can answer "is this route open?" without re-reading the
@@ -175,12 +180,11 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
175
180
  }
176
181
  const input = fnMeta.inputs?.[0] || null;
177
182
  const getRouteInputKeys = () => {
178
- const targetFuncName = refAddonTarget ?? funcName;
179
- const inputTypes = state.typesLookup.get(targetFuncName);
183
+ const inputTypes = state.typesLookup.get(funcName);
180
184
  if (inputTypes && inputTypes.length > 0) {
181
185
  return extractTypeKeys(inputTypes[0]);
182
186
  }
183
- const targetMeta = resolveFunctionMeta(state, targetFuncName);
187
+ const targetMeta = resolveFunctionMeta(state, funcName);
184
188
  if (targetMeta?.inputSchemaName) {
185
189
  const schema = state.schemas[targetMeta.inputSchemaName];
186
190
  const properties = schema?.properties;
@@ -203,7 +207,7 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
203
207
  const missingParams = params.filter((p) => !inputKeys.includes(p));
204
208
  if (missingParams.length > 0) {
205
209
  logger.critical(ErrorCode.ROUTE_PARAM_MISMATCH, `Route '${fullRoute}' has path parameter(s) [${missingParams.join(', ')}] ` +
206
- `not found in function '${refAddonTarget ?? funcName}' input type. ` +
210
+ `not found in function '${funcName}' input type. ` +
207
211
  `Input type has: [${inputKeys.join(', ')}]`);
208
212
  return;
209
213
  }
@@ -213,7 +217,7 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
213
217
  const missingQuery = query.filter((q) => !inputKeys.includes(q));
214
218
  if (missingQuery.length > 0) {
215
219
  logger.critical(ErrorCode.ROUTE_QUERY_MISMATCH, `Route '${fullRoute}' has query parameter(s) [${missingQuery.join(', ')}] ` +
216
- `not found in function '${refAddonTarget ?? funcName}' input type. ` +
220
+ `not found in function '${funcName}' input type. ` +
217
221
  `Input type has: [${inputKeys.join(', ')}]`);
218
222
  return;
219
223
  }
@@ -227,9 +231,6 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
227
231
  // Resolve permissions
228
232
  const permissions = resolvePermissions(state, obj, tags, checker);
229
233
  state.serviceAggregation.usedFunctions.add(funcName);
230
- if (refAddonTarget) {
231
- state.serviceAggregation.usedFunctions.add(refAddonTarget);
232
- }
233
234
  extractWireNames(middleware).forEach((name) => state.serviceAggregation.usedMiddleware.add(name));
234
235
  extractWireNames(permissions).forEach((name) => state.serviceAggregation.usedPermissions.add(name));
235
236
  // Check for SSE
@@ -240,7 +241,6 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
240
241
  state.http.files.add(sourceFile.fileName);
241
242
  state.http.meta[method][fullRoute] = {
242
243
  pikkuFuncId: funcName,
243
- ...(refAddonTarget && { refTarget: refAddonTarget }),
244
244
  ...(packageName && { packageName }),
245
245
  route: fullRoute,
246
246
  sourceFile: sourceFile.fileName,
@@ -4,6 +4,8 @@ export interface KeyedWiringConfig {
4
4
  idField: string;
5
5
  label: string;
6
6
  schemaPrefix: string;
7
+ /** Boolean property carrying this wiring kind's deploy-gate opt-in/opt-out. */
8
+ flagField: string;
7
9
  getState: (state: InspectorState) => {
8
10
  definitions: any[];
9
11
  files: Set<string>;
@@ -2,7 +2,7 @@ import * as ts from 'typescript';
2
2
  import { getPropertyValue, getArrayPropertyValue, assertStringLiteralProperty, } from '../utils/get-property-value.js';
3
3
  import { ErrorCode } from '../error-codes.js';
4
4
  import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
5
- import { parseDurationString } from '@pikku/core';
5
+ import { parseDurationString } from '@pikku/core/utils';
6
6
  export const createAddKeyedWiring = (config) => {
7
7
  return (logger, node, checker, state, _options) => {
8
8
  if (!ts.isCallExpression(node)) {
@@ -29,6 +29,7 @@ export const createAddKeyedWiring = (config) => {
29
29
  const docsUrlValue = getPropertyValue(obj, 'docsUrl');
30
30
  const allowedHostsValue = getArrayPropertyValue(obj, 'allowedHosts');
31
31
  const idValue = getPropertyValue(obj, config.idField);
32
+ const flagValue = getPropertyValue(obj, config.flagField) === true;
32
33
  let schemaVariableName = null;
33
34
  let schemaSourceFile = null;
34
35
  let schemaIdentifier = null;
@@ -107,6 +108,7 @@ export const createAddKeyedWiring = (config) => {
107
108
  docsUrl: docsUrlValue || undefined,
108
109
  allowedHosts: allowedHostsValue || undefined,
109
110
  [config.idField]: idValue,
111
+ [config.flagField]: flagValue || undefined,
110
112
  schema: schemaLookupName,
111
113
  sourceFile,
112
114
  });
@@ -508,12 +508,12 @@ export const addMiddleware = (logger, node, checker, state) => {
508
508
  logger.debug(`• Found channel middleware factory with services: ${services.services.join(', ')}`);
509
509
  return;
510
510
  }
511
- if (expression.text === 'pikkuAIMiddleware') {
511
+ if (expression.text === 'pikkuAgentMiddleware') {
512
512
  const arg = args[0];
513
513
  if (!arg)
514
514
  return;
515
515
  if (!ts.isObjectLiteralExpression(arg)) {
516
- logger.error(`• pikkuAIMiddleware() requires an object literal argument.`);
516
+ logger.error(`• pikkuAgentMiddleware() requires an object literal argument.`);
517
517
  return;
518
518
  }
519
519
  const allServices = new Set();
@@ -542,12 +542,12 @@ export const addMiddleware = (logger, node, checker, state) => {
542
542
  pikkuFuncId = node.parent.name.text;
543
543
  }
544
544
  else {
545
- logger.error(`• pikkuAIMiddleware() must be assigned to a variable or object property. ` +
546
- `Extract it to a const: const myMiddleware = pikkuAIMiddleware(...)`);
545
+ logger.error(`• pikkuAgentMiddleware() must be assigned to a variable or object property. ` +
546
+ `Extract it to a const: const myMiddleware = pikkuAgentMiddleware(...)`);
547
547
  return;
548
548
  }
549
549
  }
550
- state.aiMiddleware.definitions[pikkuFuncId] = {
550
+ state.agentMiddleware.definitions[pikkuFuncId] = {
551
551
  services,
552
552
  sourceFile: node.getSourceFile().fileName,
553
553
  position: node.getStart(),
@@ -192,6 +192,9 @@ export const addPersonas = (logger, node, _checker, state) => {
192
192
  const personality = stringProperty(config, 'personality');
193
193
  if (personality !== undefined)
194
194
  persona.personality = personality;
195
+ const app = stringProperty(config, 'app');
196
+ if (app !== undefined)
197
+ persona.app = app;
195
198
  if (disposition !== undefined) {
196
199
  persona.disposition = disposition;
197
200
  }
@@ -4,5 +4,6 @@ export const addSecret = createAddKeyedWiring({
4
4
  idField: 'secretId',
5
5
  label: 'Secret',
6
6
  schemaPrefix: 'SecretSchema',
7
+ flagField: 'optional',
7
8
  getState: (state) => state.secrets,
8
9
  });
@@ -4,5 +4,6 @@ export const addVariable = createAddKeyedWiring({
4
4
  idField: 'variableId',
5
5
  label: 'Variable',
6
6
  schemaPrefix: 'VariableSchema',
7
+ flagField: 'required',
7
8
  getState: (state) => state.variables,
8
9
  });
@@ -38,7 +38,8 @@ export function addWireRemoteAddon(node, state, logger) {
38
38
  ts.isObjectLiteralExpression(prop.initializer)) {
39
39
  hasAuth = true;
40
40
  for (const authProp of prop.initializer.properties) {
41
- if (!ts.isPropertyAssignment(authProp) || !ts.isIdentifier(authProp.name))
41
+ if (!ts.isPropertyAssignment(authProp) ||
42
+ !ts.isIdentifier(authProp.name))
42
43
  continue;
43
44
  if (authProp.name.text === 'credentialId' &&
44
45
  ts.isStringLiteral(authProp.initializer)) {
@@ -1,7 +1,7 @@
1
1
  import * as ts from 'typescript';
2
2
  import { ErrorCode } from '../error-codes.js';
3
3
  import { extractStringLiteral } from '../utils/extract-node-value.js';
4
- function extractAstValue(expr, refParamName, templateParamName) {
4
+ function extractAstValue(expr, refParamName, templateParamName, itemParamName) {
5
5
  // `'GET' as const` / parenthesised values wrap the real initializer — see
6
6
  // through them or the whole property is silently dropped from the meta.
7
7
  while (ts.isAsExpression(expr) || ts.isParenthesizedExpression(expr)) {
@@ -31,6 +31,11 @@ function extractAstValue(expr, refParamName, templateParamName) {
31
31
  const path = args[1] && ts.isStringLiteral(args[1]) ? args[1].text : undefined;
32
32
  return { $ref: nodeId, path };
33
33
  }
34
+ if (itemParamName && callee.text === itemParamName) {
35
+ const pathArg = expr.arguments[0];
36
+ const path = pathArg && ts.isStringLiteral(pathArg) ? pathArg.text : undefined;
37
+ return { $ref: '$item', path };
38
+ }
34
39
  if (templateParamName && callee.text === templateParamName) {
35
40
  const templateStr = expr.arguments[0] && ts.isStringLiteral(expr.arguments[0])
36
41
  ? expr.arguments[0].text
@@ -39,7 +44,7 @@ function extractAstValue(expr, refParamName, templateParamName) {
39
44
  const refs = [];
40
45
  if (refsArg && ts.isArrayLiteralExpression(refsArg)) {
41
46
  for (const el of refsArg.elements) {
42
- const resolved = extractAstValue(el, refParamName, templateParamName);
47
+ const resolved = extractAstValue(el, refParamName, templateParamName, itemParamName);
43
48
  if (typeof resolved === 'object' &&
44
49
  resolved !== null &&
45
50
  '$ref' in resolved) {
@@ -64,7 +69,7 @@ function extractAstValue(expr, refParamName, templateParamName) {
64
69
  }
65
70
  }
66
71
  if (ts.isArrayLiteralExpression(expr)) {
67
- return expr.elements.map((el) => extractAstValue(el, refParamName, templateParamName));
72
+ return expr.elements.map((el) => extractAstValue(el, refParamName, templateParamName, itemParamName));
68
73
  }
69
74
  if (ts.isObjectLiteralExpression(expr)) {
70
75
  const obj = {};
@@ -78,7 +83,7 @@ function extractAstValue(expr, refParamName, templateParamName) {
78
83
  : null;
79
84
  if (!key)
80
85
  continue;
81
- obj[key] = extractAstValue(prop.initializer, refParamName, templateParamName);
86
+ obj[key] = extractAstValue(prop.initializer, refParamName, templateParamName, itemParamName);
82
87
  }
83
88
  return obj;
84
89
  }
@@ -125,6 +130,11 @@ function extractInputMapping(node, _checker) {
125
130
  const templateParamName = node.parameters.length > 1 && ts.isIdentifier(node.parameters[1].name)
126
131
  ? node.parameters[1].name.text
127
132
  : 'template';
133
+ // `$item` is appended after `template` so neither existing arrow shape —
134
+ // `(ref) => ...` nor `(ref, template) => ...` — shifts a parameter.
135
+ const itemParamName = node.parameters.length > 2 && ts.isIdentifier(node.parameters[2].name)
136
+ ? node.parameters[2].name.text
137
+ : undefined;
128
138
  const input = {};
129
139
  for (const prop of bodyObj.properties) {
130
140
  if (!ts.isPropertyAssignment(prop))
@@ -136,13 +146,48 @@ function extractInputMapping(node, _checker) {
136
146
  : null;
137
147
  if (!key)
138
148
  continue;
139
- const value = extractAstValue(prop.initializer, refParamName, templateParamName);
149
+ const value = extractAstValue(prop.initializer, refParamName, templateParamName, itemParamName);
140
150
  if (value !== undefined) {
141
151
  input[key] = value;
142
152
  }
143
153
  }
144
154
  return input;
145
155
  }
156
+ /**
157
+ * Extract a `forEach` source, authored either as a bare node id (`'listRows'`)
158
+ * or as a callback returning a ref (`(ref) => ref('listRows', 'rows')`).
159
+ */
160
+ function extractForEachConfig(node) {
161
+ while (ts.isAsExpression(node) || ts.isParenthesizedExpression(node)) {
162
+ node = node.expression;
163
+ }
164
+ if (ts.isStringLiteral(node)) {
165
+ return { $ref: node.text };
166
+ }
167
+ if (ts.isArrowFunction(node)) {
168
+ const refParamName = node.parameters.length > 0 && ts.isIdentifier(node.parameters[0].name)
169
+ ? node.parameters[0].name.text
170
+ : 'ref';
171
+ let body = node.body;
172
+ if (ts.isBlock(body)) {
173
+ for (const stmt of body.statements) {
174
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
175
+ body = stmt.expression;
176
+ }
177
+ }
178
+ }
179
+ if (!ts.isExpression(body))
180
+ return undefined;
181
+ const value = extractAstValue(body, refParamName, undefined);
182
+ if (typeof value === 'object' &&
183
+ value !== null &&
184
+ '$ref' in value &&
185
+ typeof value.$ref === 'string') {
186
+ return value;
187
+ }
188
+ }
189
+ return undefined;
190
+ }
146
191
  /**
147
192
  * Extract next config (string, array, or record)
148
193
  */
@@ -301,6 +346,8 @@ function extractGraphFromNewFormat(nodesNode, configNode, checker, state) {
301
346
  nodeId,
302
347
  rpcName,
303
348
  input: {},
349
+ forEach: undefined,
350
+ mode: undefined,
304
351
  next: undefined,
305
352
  onError: undefined,
306
353
  retries: undefined,
@@ -326,6 +373,8 @@ function extractGraphFromNewFormat(nodesNode, configNode, checker, state) {
326
373
  nodes[nodeId].next = nodeConfig.next;
327
374
  nodes[nodeId].onError = nodeConfig.onError;
328
375
  nodes[nodeId].input = nodeConfig.input;
376
+ nodes[nodeId].forEach = nodeConfig.forEach;
377
+ nodes[nodeId].mode = nodeConfig.mode;
329
378
  nodes[nodeId].retries = nodeConfig.retries;
330
379
  nodes[nodeId].retryDelay = nodeConfig.retryDelay;
331
380
  nodes[nodeId].notes = nodeConfig.notes;
@@ -342,6 +391,8 @@ function extractNodeConfigFromObject(obj, checker) {
342
391
  let next = undefined;
343
392
  let onError = undefined;
344
393
  let input = {};
394
+ let forEach = undefined;
395
+ let mode = undefined;
345
396
  let retries = undefined;
346
397
  let retryDelay = undefined;
347
398
  let notes = undefined;
@@ -358,6 +409,12 @@ function extractNodeConfigFromObject(obj, checker) {
358
409
  else if (propName === 'input') {
359
410
  input = extractInputMapping(prop.initializer, checker);
360
411
  }
412
+ else if (propName === 'forEach') {
413
+ forEach = extractForEachConfig(prop.initializer);
414
+ }
415
+ else if (propName === 'mode') {
416
+ mode = extractStringLiteral(prop.initializer, checker);
417
+ }
361
418
  else if (propName === 'retries') {
362
419
  if (ts.isNumericLiteral(prop.initializer)) {
363
420
  retries = Number(prop.initializer.text);
@@ -375,7 +432,7 @@ function extractNodeConfigFromObject(obj, checker) {
375
432
  notes = extractStringLiteral(prop.initializer, checker);
376
433
  }
377
434
  }
378
- return { next, onError, input, retries, retryDelay, notes };
435
+ return { next, onError, input, forEach, mode, retries, retryDelay, notes };
379
436
  }
380
437
  /**
381
438
  * Inspector for pikkuWorkflowGraph() calls
@@ -20,7 +20,7 @@ export declare enum ErrorCode {
20
20
  CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = "PKU672",
21
21
  SCENARIO_HAS_SERVICES = "PKU673",
22
22
  EXPECT_EVENTUALLY_SCENARIO_ONLY = "PKU675",
23
- SCENARIO_BROWSER_STEP_NEEDS_ACTOR = "PKU677",
23
+ SCENARIO_STEP_NEEDS_ACTOR = "PKU677",
24
24
  SCENARIO_STEP_TARGET_NOT_STATIC = "PKU678",
25
25
  SCENARIO_NOT_EXTRACTABLE = "PKU679",
26
26
  SCENARIO_HAS_NO_ASSERTION = "PKU680",
@@ -30,6 +30,7 @@ export declare enum ErrorCode {
30
30
  COMPLEX_WORKFLOW_NOT_ALLOWED = "PKU643",
31
31
  REMOTE_ADDON_NOT_DEV_DEPENDENCY = "PKU338",
32
32
  REMOTE_ADDON_AUTH_UNRESOLVED = "PKU339",
33
+ ADDON_NOT_INSTALLED = "PKU340",
33
34
  DB_COLUMN_NAME_TYPE_CONTRADICTION = "PKU480",
34
35
  DB_JSON_COLUMN_UNTYPED = "PKU481",
35
36
  DB_FORMAT_ON_NON_STRING = "PKU482",
@@ -70,6 +71,7 @@ export declare enum ErrorCode {
70
71
  DUPLICATE_CORE_VERSION = "PKU717",
71
72
  CORE_VERSION_SKEW = "PKU718",
72
73
  SPLIT_TYPE_IDENTITY = "PKU719",
74
+ SINGLETON_SERVICES_TYPE_UNRESOLVED = "PKU724",
73
75
  PII_IN_OUTPUT = "PKU910",
74
76
  ADDON_WIRING_NOT_ALLOWED = "PKU920",
75
77
  ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = "PKU921",
@@ -22,7 +22,7 @@ export var ErrorCode;
22
22
  ErrorCode["CLI_CLIENTSIDE_RENDERER_HAS_SERVICES"] = "PKU672";
23
23
  ErrorCode["SCENARIO_HAS_SERVICES"] = "PKU673";
24
24
  ErrorCode["EXPECT_EVENTUALLY_SCENARIO_ONLY"] = "PKU675";
25
- ErrorCode["SCENARIO_BROWSER_STEP_NEEDS_ACTOR"] = "PKU677";
25
+ ErrorCode["SCENARIO_STEP_NEEDS_ACTOR"] = "PKU677";
26
26
  ErrorCode["SCENARIO_STEP_TARGET_NOT_STATIC"] = "PKU678";
27
27
  ErrorCode["SCENARIO_NOT_EXTRACTABLE"] = "PKU679";
28
28
  ErrorCode["SCENARIO_HAS_NO_ASSERTION"] = "PKU680";
@@ -33,6 +33,7 @@ export var ErrorCode;
33
33
  // Remote addon (wireRemoteAddon) validation
34
34
  ErrorCode["REMOTE_ADDON_NOT_DEV_DEPENDENCY"] = "PKU338";
35
35
  ErrorCode["REMOTE_ADDON_AUTH_UNRESOLVED"] = "PKU339";
36
+ ErrorCode["ADDON_NOT_INSTALLED"] = "PKU340";
36
37
  // Database schema codegen warnings
37
38
  ErrorCode["DB_COLUMN_NAME_TYPE_CONTRADICTION"] = "PKU480";
38
39
  ErrorCode["DB_JSON_COLUMN_UNTYPED"] = "PKU481";
@@ -91,6 +92,7 @@ export var ErrorCode;
91
92
  ErrorCode["DUPLICATE_CORE_VERSION"] = "PKU717";
92
93
  ErrorCode["CORE_VERSION_SKEW"] = "PKU718";
93
94
  ErrorCode["SPLIT_TYPE_IDENTITY"] = "PKU719";
95
+ ErrorCode["SINGLETON_SERVICES_TYPE_UNRESOLVED"] = "PKU724";
94
96
  // Data classification errors
95
97
  ErrorCode["PII_IN_OUTPUT"] = "PKU910";
96
98
  // Addon authoring errors
package/dist/index.d.ts CHANGED
@@ -12,9 +12,11 @@ export type { SerializableInspectorState } from './utils/serialize-inspector-sta
12
12
  export { filterInspectorState } from './utils/filter-inspector-state.js';
13
13
  export { resolveDeployTarget } from './utils/resolve-deploy-target.js';
14
14
  export { generateCustomTypes, sanitizeTypeName, } from './utils/custom-types-generator.js';
15
+ export { unresolvedSchemaReferences } from './utils/post-process.js';
16
+ export type { SurfaceUsageCounts } from './utils/extract-surface-usage.js';
15
17
  export { createEmptyManifest, serializeManifest, } from './utils/contract-hashes.js';
16
18
  export type { ContractEntry, VersionHashEntry, VersionValidateError, VersionManifest, VersionManifestEntry, } from './utils/contract-hashes.js';
17
- export type { InspectorFeature, InspectorFeatureEntry, } from './types.js';
19
+ export type { InspectorFeature, InspectorFeatureEntry } from './types.js';
18
20
  export { serializeMCPJson } from './utils/serialize-mcp-json.js';
19
21
  export type { OpenAPISpecInfo } from './utils/serialize-openapi-json.js';
20
22
  export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './utils/workflow/dsl/index.js';
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export { serializeInspectorState, deserializeInspectorState, } from './utils/ser
6
6
  export { filterInspectorState } from './utils/filter-inspector-state.js';
7
7
  export { resolveDeployTarget } from './utils/resolve-deploy-target.js';
8
8
  export { generateCustomTypes, sanitizeTypeName, } from './utils/custom-types-generator.js';
9
+ export { unresolvedSchemaReferences } from './utils/post-process.js';
9
10
  export { createEmptyManifest, serializeManifest, } from './utils/contract-hashes.js';
10
11
  export { serializeMCPJson } from './utils/serialize-mcp-json.js';
11
12
  export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './utils/workflow/dsl/index.js';
package/dist/inspector.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as ts from 'typescript';
2
2
  import { performance } from 'perf_hooks';
3
- import { dirname, resolve } from 'path';
3
+ import { dirname, relative, resolve } from 'path';
4
4
  import { visitSetup, visitFunctions, visitRoutes } from './visit.js';
5
5
  import { TypesMap } from './types-map.js';
6
6
  import { getFilesAndMethods } from './utils/get-files-and-methods.js';
@@ -11,12 +11,13 @@ import { validateExposedFunctionsGated } from './utils/validate-exposed-function
11
11
  import { validateTagsResolveToMiddleware } from './utils/validate-tags-resolve-to-middleware.js';
12
12
  import { annotateHttpRouteAuth } from './utils/annotate-http-route-auth.js';
13
13
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js';
14
- import { pikkuState } from '@pikku/core/ecosystem';
14
+ import { pikkuState } from '@pikku/core/state';
15
15
  import { resolveLatestVersions } from './utils/resolve-versions.js';
16
16
  import { finalizeWorkflows } from './utils/workflow/graph/finalize-workflows.js';
17
17
  import { finalizeWorkflowHelperTypes, finalizeWorkflowWires, } from './utils/workflow/graph/finalize-workflow-wires.js';
18
18
  import { generateAllSchemas } from './utils/schema-generator.js';
19
19
  import { extractSecretUsage } from './utils/extract-secret-usage.js';
20
+ import { accumulateSurfaceUsage, surfaceUsageArea, } from './utils/extract-surface-usage.js';
20
21
  import { loadAddonFunctionsMeta, loadAddonSchemas, } from './utils/load-addon-functions-meta.js';
21
22
  import { computeContractHashes, extractContractsFromMeta, updateManifest, createEmptyManifest, validateContracts, } from './utils/contract-hashes.js';
22
23
  /**
@@ -142,6 +143,7 @@ export function getInitialInspectorState(rootDir) {
142
143
  files: new Set(),
143
144
  usage: new Map(),
144
145
  },
146
+ surfaceUsage: {},
145
147
  credentials: {
146
148
  definitions: [],
147
149
  files: new Set(),
@@ -177,7 +179,7 @@ export function getInitialInspectorState(rootDir) {
177
179
  instances: {},
178
180
  tagMiddleware: new Map(),
179
181
  },
180
- aiMiddleware: {
182
+ agentMiddleware: {
181
183
  definitions: {},
182
184
  },
183
185
  permissions: {
@@ -299,11 +301,19 @@ export const inspect = async (logger, routeFiles, options = {}) => {
299
301
  .sort((a, b) => a.fileName < b.fileName ? -1 : a.fileName > b.fileName ? 1 : 0);
300
302
  logger.debug(`Got source files in ${(performance.now() - startSourceFiles).toFixed(2)}ms`);
301
303
  const state = getInitialInspectorState(rootDir);
304
+ const surfaceAreaCache = new Map();
302
305
  for (const sourceFile of sourceFiles) {
303
306
  const usage = extractSecretUsage(sourceFile);
304
307
  if (usage.keys.length > 0 || usage.dynamic.length > 0) {
305
308
  state.secrets.usage.set(sourceFile.fileName, usage);
306
309
  }
310
+ // Counted in the sweep that already has this file open. The public surface
311
+ // is documentation, and documentation may not cost a project a second pass
312
+ // over its source.
313
+ accumulateSurfaceUsage(sourceFile, {
314
+ area: surfaceUsageArea(sourceFile.fileName, rootDir, surfaceAreaCache),
315
+ file: relative(rootDir, sourceFile.fileName),
316
+ }, state.surfaceUsage);
307
317
  }
308
318
  // First sweep: add all functions
309
319
  const startSetup = performance.now();
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type * as ts from 'typescript';
2
2
  import type { SecretUsage } from './utils/extract-secret-usage.js';
3
+ import type { SurfaceUsageCounts } from './utils/extract-surface-usage.js';
3
4
  import type { ChannelMessageMeta, ChannelsMeta } from '@pikku/core/channel';
4
5
  import type { GatewaysMeta } from '@pikku/core/gateway';
5
6
  import type { HTTPWiringsMeta } from '@pikku/core/http';
@@ -8,8 +9,8 @@ import type { TriggerMeta, TriggerSourceMeta } from '@pikku/core/trigger';
8
9
  import type { QueueWorkersMeta } from '@pikku/core/queue';
9
10
  import type { WorkflowsMeta } from '@pikku/core/workflow';
10
11
  import type { MCPResourceMeta, MCPToolMeta, MCPPromptMeta } from '@pikku/core/mcp';
11
- import type { AIAgentMeta } from '@pikku/core/ai-agent';
12
- import type { ScorerMeta } from '@pikku/core/ai-scorer';
12
+ import type { AgentsMeta } from '@pikku/core/agent';
13
+ import type { ScorerMeta } from '@pikku/core/agent-scorer';
13
14
  import type { CLIMeta } from '@pikku/core/cli';
14
15
  import type { CLICommandMeta } from '@pikku/core/cli';
15
16
  import type { NodesMeta } from '@pikku/core/node';
@@ -20,7 +21,9 @@ import type { SystemRoleDefinitions } from '@pikku/core/role';
20
21
  import type { PersonaDefinitions } from '@pikku/core/persona';
21
22
  import type { VariableDefinitions } from '@pikku/core/variable';
22
23
  import type { TypesMap } from './types-map.js';
23
- import type { FunctionsMeta, FunctionServicesMeta, FunctionWiresMeta, JSONValue } from '@pikku/core';
24
+ import type { FunctionsMeta } from '@pikku/core/services';
25
+ import type { FunctionServicesMeta, FunctionWiresMeta } from '@pikku/core/function';
26
+ import type { JSONValue } from '@pikku/core/utils';
24
27
  import type { OpenAPISpecInfo } from './utils/serialize-openapi-json.js';
25
28
  import type { ErrorCode, CodedDiagnostic } from './error-codes.js';
26
29
  import type { VersionManifest, VersionValidateError } from './utils/contract-hashes.js';
@@ -545,7 +548,7 @@ export interface InspectorState {
545
548
  files: Set<string>;
546
549
  };
547
550
  agents: {
548
- agentsMeta: AIAgentMeta;
551
+ agentsMeta: AgentsMeta;
549
552
  files: Map<string, {
550
553
  path: string;
551
554
  exportedName: string;
@@ -592,6 +595,8 @@ export interface InspectorState {
592
595
  /** Secret reads found per source file, keyed by absolute path. */
593
596
  usage: Map<string, SecretUsage>;
594
597
  };
598
+ /** How often each `#pikku/*` export is imported, and from where. */
599
+ surfaceUsage: SurfaceUsageCounts;
595
600
  credentials: {
596
601
  definitions: CredentialDefinitions;
597
602
  files: Set<string>;
@@ -619,7 +624,7 @@ export interface InspectorState {
619
624
  };
620
625
  middleware: InspectorMiddlewareState;
621
626
  channelMiddleware: InspectorChannelMiddlewareState;
622
- aiMiddleware: InspectorAIMiddlewareState;
627
+ agentMiddleware: InspectorAIMiddlewareState;
623
628
  permissions: InspectorPermissionState;
624
629
  serviceAggregation: {
625
630
  requiredServices: Set<string>;
@@ -1,3 +1,4 @@
1
+ import { resolveFunctionMeta } from './resolve-function-meta.js';
1
2
  /**
2
3
  * Resolve, per HTTP route, whether reaching it requires a session.
3
4
  *
@@ -22,7 +23,7 @@ export function annotateHttpRouteAuth(state) {
22
23
  const gatedPackages = packagesGatedByTheirAddon(state);
23
24
  for (const routes of Object.values(state.http?.meta ?? {})) {
24
25
  for (const route of Object.values(routes)) {
25
- const meta = state.functions.meta[route.refTarget ?? route.pikkuFuncId];
26
+ const meta = resolveFunctionMeta(state, route.pikkuFuncId);
26
27
  route.requiresSession =
27
28
  // A pikkuFunc always requires a session, on every path.
28
29
  meta?.sessionless === false ||
@@ -38,7 +38,8 @@ const SINKS = [
38
38
  export function findRevealedSecretSinks(checker, handler) {
39
39
  const violations = [];
40
40
  const visit = (node) => {
41
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
41
+ if (ts.isCallExpression(node) &&
42
+ ts.isPropertyAccessExpression(node.expression)) {
42
43
  const sink = describeSink(checker, node.expression);
43
44
  if (sink) {
44
45
  for (const arg of node.arguments) {
@@ -1,4 +1,4 @@
1
- import type { FunctionsMeta } from '@pikku/core';
1
+ import type { FunctionsMeta } from '@pikku/core/services';
2
2
  import type { TypesMap } from '../types-map.js';
3
3
  import type { SchemaRef } from '../types.js';
4
4
  export declare function computeRequiredSchemas(functionsMeta: FunctionsMeta, typesMap: TypesMap, additionalTypes?: string[], schemaLookup?: Map<string, SchemaRef>): Set<string>;
@@ -1,4 +1,5 @@
1
- import type { FunctionsMeta, JSONValue } from '@pikku/core';
1
+ import type { FunctionsMeta } from '@pikku/core/services';
2
+ import type { JSONValue } from '@pikku/core/utils';
2
3
  import type { TypesMap } from '../types-map.js';
3
4
  import { ErrorCode } from '../error-codes.js';
4
5
  export type ContractEntry = {
@@ -1,4 +1,4 @@
1
- import { parseVersionedId } from '@pikku/core';
1
+ import { parseVersionedId } from '@pikku/core/utils';
2
2
  import { ErrorCode } from '../error-codes.js';
3
3
  import { canonicalJSON, hashString } from './hash.js';
4
4
  function isHashEntry(v) {
@@ -1,7 +1,7 @@
1
1
  import * as ts from 'typescript';
2
2
  import { createHash } from 'crypto';
3
3
  import { relative } from 'path';
4
- import { formatVersionedId } from '@pikku/core';
4
+ import { formatVersionedId } from '@pikku/core/utils';
5
5
  /**
6
6
  * Deterministic placeholder id for an anonymous/unnamed pikku function or
7
7
  * permission. Derived from the call expression's source location (relative path
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import type { FunctionServicesMeta, FunctionWiresMeta } from '@pikku/core';
2
+ import type { FunctionServicesMeta, FunctionWiresMeta } from '@pikku/core/function';
3
3
  /**
4
4
  * Extract services from a function's first parameter destructuring pattern
5
5
  */
@@ -0,0 +1,29 @@
1
+ import ts from 'typescript';
2
+ /**
3
+ * How often each `#pikku/*` export is imported, the source areas it was
4
+ * imported from, and the files those imports are written in. Counters only:
5
+ * this is accumulated during the sweep that already visits every source file,
6
+ * so nothing here may retain a node.
7
+ */
8
+ export type SurfaceUsageCounts = Record<string, Record<string, {
9
+ imports: number;
10
+ seenIn: string[];
11
+ files: string[];
12
+ }>>;
13
+ /**
14
+ * A label a person recognises: the workspace package a file belongs to, or the
15
+ * top-level source directory it sits under when the project is one package.
16
+ * The root package is skipped deliberately — naming it would label every file
17
+ * in a single-package project identically.
18
+ */
19
+ export declare const surfaceUsageArea: (fileName: string, rootDir: string, cache: Map<string, string | null>) => string;
20
+ /**
21
+ * Counts the `#pikku/*` imports of one file into `into`. Only the file's own
22
+ * top-level statements are looked at — an import declaration can appear nowhere
23
+ * else — so this costs a single pass over the statement list and retains
24
+ * nothing.
25
+ */
26
+ export declare const accumulateSurfaceUsage: (sourceFile: ts.SourceFile, { area, file }: {
27
+ area: string;
28
+ file: string;
29
+ }, into: SurfaceUsageCounts) => void;