@pikku/inspector 0.12.11 → 0.12.13

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 (65) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/add/add-cli.js +10 -3
  3. package/dist/add/add-credential.js +2 -1
  4. package/dist/add/add-functions.js +48 -1
  5. package/dist/add/add-http-route.js +24 -5
  6. package/dist/add/add-keyed-wiring.js +3 -1
  7. package/dist/add/add-middleware.js +33 -4
  8. package/dist/add/add-permission.js +7 -7
  9. package/dist/add/add-workflow-graph.js +20 -1
  10. package/dist/error-codes.d.ts +3 -1
  11. package/dist/error-codes.js +3 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +1 -0
  14. package/dist/inspector.js +2 -5
  15. package/dist/types.d.ts +10 -19
  16. package/dist/utils/check-pii-output.d.ts +14 -0
  17. package/dist/utils/check-pii-output.js +63 -0
  18. package/dist/utils/extract-function-name.js +6 -0
  19. package/dist/utils/filter-inspector-state.js +187 -59
  20. package/dist/utils/filter-utils.js +13 -5
  21. package/dist/utils/get-property-value.d.ts +10 -0
  22. package/dist/utils/get-property-value.js +30 -0
  23. package/dist/utils/post-process.d.ts +2 -3
  24. package/dist/utils/post-process.js +3 -23
  25. package/dist/utils/resolve-addon-package.d.ts +4 -5
  26. package/dist/utils/resolve-addon-package.js +64 -16
  27. package/dist/utils/resolve-deploy-target.d.ts +28 -0
  28. package/dist/utils/resolve-deploy-target.js +56 -0
  29. package/dist/utils/resolve-versions.js +79 -0
  30. package/dist/utils/schema-generator.js +31 -12
  31. package/package.json +2 -2
  32. package/src/add/add-cli.ts +10 -3
  33. package/src/add/add-credential.ts +3 -0
  34. package/src/add/add-functions.test.ts +149 -0
  35. package/src/add/add-functions.ts +61 -1
  36. package/src/add/add-gateway.ts +5 -1
  37. package/src/add/add-http-route.ts +26 -6
  38. package/src/add/add-keyed-wiring.ts +7 -1
  39. package/src/add/add-mcp-prompt.ts +5 -1
  40. package/src/add/add-mcp-resource.ts +5 -1
  41. package/src/add/add-middleware.ts +42 -4
  42. package/src/add/add-permission.ts +7 -7
  43. package/src/add/add-schedule.ts +5 -1
  44. package/src/add/add-workflow-graph.ts +19 -1
  45. package/src/add/pii-check.test.ts +197 -0
  46. package/src/add/wire-name-literal.test.ts +114 -0
  47. package/src/error-codes.ts +4 -0
  48. package/src/index.ts +1 -0
  49. package/src/inspector.ts +1 -5
  50. package/src/types.ts +19 -15
  51. package/src/utils/check-pii-output.ts +76 -0
  52. package/src/utils/extract-function-name.ts +8 -0
  53. package/src/utils/filter-inspector-state.test.ts +168 -64
  54. package/src/utils/filter-inspector-state.ts +290 -64
  55. package/src/utils/filter-utils.test.ts +30 -15
  56. package/src/utils/filter-utils.ts +14 -5
  57. package/src/utils/get-property-value.ts +40 -0
  58. package/src/utils/post-process.ts +3 -38
  59. package/src/utils/resolve-addon-package.ts +65 -14
  60. package/src/utils/resolve-deploy-target.test.ts +105 -0
  61. package/src/utils/resolve-deploy-target.ts +63 -0
  62. package/src/utils/resolve-versions.test.ts +108 -0
  63. package/src/utils/resolve-versions.ts +86 -0
  64. package/src/utils/schema-generator.ts +37 -13
  65. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ ## 0.12.13
2
+
3
+ ### Patch Changes
4
+
5
+ - 665bdb0: Add end-to-end data classification for SQLite and Postgres projects.
6
+
7
+ **Core (`@pikku/core`):** New `Private<T>` and `Secret<T>` intersection brands, `ClassificationManifest`, `ColumnClassification`, and `AnonymizeStrategy` types exported from `data-classification.ts`.
8
+
9
+ **CLI (`@pikku/cli`):**
10
+ - SQL comment annotations: `-- @public`, `-- @private[:strategy]`, `-- @secret[:strategy]` on `CREATE TABLE` columns and `ALTER TABLE ... ADD COLUMN` statements. Unannotated columns default to `private`.
11
+ - `pikku db migrate` now emits a `classification.gen.ts` manifest alongside `schema.d.ts`.
12
+ - New `pikku db audit` command — prints a per-column classification summary and warns on `private`/`secret` columns with no anonymize strategy.
13
+ - Postgres dialect support in `resolveDb`, `PostgresMigrationExecutor`, and `PostgresIntrospector`.
14
+
15
+ **Inspector (`@pikku/inspector`):** New PKU910 check — `findPiiPaths()` walks inferred function return types looking for `__pii__` brands (including inside `Array<T>`, `Record<K,V>`, and index signatures) and fails the build if a function exposes branded fields in its output.
16
+
17
+ - Updated dependencies [665bdb0]
18
+ - @pikku/core@0.12.25
19
+
20
+ ## 0.12.12
21
+
22
+ ### Patch Changes
23
+
24
+ - 9060165: Agents now declare their model directly as `<provider>/<model>` (e.g. `openai/gpt-4o`). The `models`, `agentDefaults`, and `agentOverrides` config blocks have been removed.
25
+
26
+ **Migration:** replace any bare `model: 'alias'` values with the full provider-qualified form and remove those blocks from `pikku.config.json`.
27
+
28
+ - Updated dependencies [9060165]
29
+ - Updated dependencies [9060165]
30
+ - Updated dependencies [9060165]
31
+ - @pikku/core@0.12.21
32
+
1
33
  ## 0.12.11
2
34
 
3
35
  ### Patch Changes
@@ -295,10 +295,16 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
295
295
  meta.options = processOptions(logger, optionsNode, typeChecker, inspectorState, options, pikkuFuncId);
296
296
  }
297
297
  break;
298
- case 'subcommands':
299
- if (ts.isObjectLiteralExpression(prop.initializer)) {
298
+ case 'subcommands': {
299
+ let subcommandsNode = prop.initializer;
300
+ if (ts.isIdentifier(prop.initializer)) {
301
+ subcommandsNode = resolveIdentifier(prop.initializer, typeChecker, [
302
+ 'defineCLICommands',
303
+ ]);
304
+ }
305
+ if (subcommandsNode && ts.isObjectLiteralExpression(subcommandsNode)) {
300
306
  meta.subcommands = {};
301
- for (const subProp of prop.initializer.properties) {
307
+ for (const subProp of subcommandsNode.properties) {
302
308
  if (!ts.isPropertyAssignment(subProp))
303
309
  continue;
304
310
  const subName = getPropertyName(subProp);
@@ -311,6 +317,7 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
311
317
  }
312
318
  }
313
319
  break;
320
+ }
314
321
  case 'isDefault':
315
322
  if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
316
323
  prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import { getPropertyValue, getArrayPropertyValue, } from '../utils/get-property-value.js';
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
5
  export const addCredential = (logger, node, checker, state, _options) => {
@@ -17,6 +17,7 @@ export const addCredential = (logger, node, checker, state, _options) => {
17
17
  }
18
18
  if (ts.isObjectLiteralExpression(firstArg)) {
19
19
  const obj = firstArg;
20
+ assertStringLiteralProperty(obj, 'name', 'Credential', logger);
20
21
  const nameValue = getPropertyValue(obj, 'name');
21
22
  const displayNameValue = getPropertyValue(obj, 'displayName');
22
23
  const descriptionValue = getPropertyValue(obj, 'description');
@@ -5,10 +5,12 @@ import { extractFunctionNode } from '../utils/extract-function-node.js';
5
5
  import { extractUsedWires } from '../utils/extract-services.js';
6
6
  import { formatVersionedId, parseVersionedId } from '@pikku/core';
7
7
  import { getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
8
+ import { canonicalJSON, hashString } from '../utils/hash.js';
8
9
  import { resolveMiddleware } from '../utils/middleware.js';
9
10
  import { resolvePermissions } from '../utils/permissions.js';
10
11
  import { extractWireNames } from '../utils/post-process.js';
11
12
  import { ErrorCode } from '../error-codes.js';
13
+ import { findPiiPaths } from '../utils/check-pii-output.js';
12
14
  const isValidVariableName = (name) => {
13
15
  const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
14
16
  return regex.test(name);
@@ -222,6 +224,20 @@ const areCompatibleFunctionIds = (existingId, incomingId) => {
222
224
  const incomingParsed = parseVersionedId(incomingId);
223
225
  return existingParsed.baseName === incomingParsed.baseName;
224
226
  };
227
+ function printNode(node) {
228
+ return ts
229
+ .createPrinter({ removeComments: true })
230
+ .printNode(ts.EmitHint.Unspecified, node, node.getSourceFile());
231
+ }
232
+ function computeImplementationHash(args) {
233
+ const { wrapper, handler, objectNode, isDirectFunction } = args;
234
+ return hashString(canonicalJSON({
235
+ wrapper,
236
+ isDirectFunction,
237
+ handler: printNode(handler),
238
+ config: objectNode ? printNode(objectNode) : null,
239
+ }));
240
+ }
225
241
  /**
226
242
  * Inspect pikkuFunc calls, extract input/output and first-arg destructuring,
227
243
  * then push into state.functions.meta.
@@ -409,7 +425,12 @@ export const addFunctions = (logger, node, checker, state, options) => {
409
425
  }
410
426
  }
411
427
  if (version !== undefined) {
412
- const baseName = explicitName || exportedName || pikkuFuncId;
428
+ let baseName = explicitName || exportedName || pikkuFuncId;
429
+ // Strip trailing VN suffix if it matches the version (e.g. getDataV1 + version:1 → getData@v1)
430
+ const vSuffix = `V${version}`;
431
+ if (baseName.endsWith(vSuffix) && baseName.length > vSuffix.length) {
432
+ baseName = baseName.slice(0, -vSuffix.length);
433
+ }
413
434
  pikkuFuncId = formatVersionedId(baseName, version);
414
435
  }
415
436
  const isMCPToolFunc = expression.text === 'pikkuMCPToolFunc';
@@ -606,6 +627,25 @@ export const addFunctions = (logger, node, checker, state, options) => {
606
627
  }
607
628
  }
608
629
  }
630
+ // ── PII brand check ───────────────────────────────────────────────────────
631
+ // Walk the function body's ACTUAL inferred return type looking for Private<T>
632
+ // / Secret<T> brands (__pii__ property). This runs for every function,
633
+ // including those with a Zod output schema, because the TS return type
634
+ // reflects what the body actually returns before any Zod coercion.
635
+ {
636
+ const sig = checker.getSignatureFromDeclaration(handler);
637
+ if (sig) {
638
+ const rawRet = checker.getReturnTypeOfSignature(sig);
639
+ const unwrapped = unwrapPromise(checker, rawRet);
640
+ const piiPaths = findPiiPaths(checker, unwrapped);
641
+ if (piiPaths.length > 0) {
642
+ logger.critical(ErrorCode.PII_IN_OUTPUT, `Function '${name}' exposes PII-classified field(s) in its return type: ` +
643
+ piiPaths.map((p) => `'${p}'`).join(', ') +
644
+ `.\n Either strip these fields before returning or mark the column ` +
645
+ `@public in the migration if it is safe to expose.`);
646
+ }
647
+ }
648
+ }
609
649
  // --- resolve middleware ---
610
650
  let middleware = objectNode
611
651
  ? resolveMiddleware(state, objectNode, tags, checker)
@@ -636,6 +676,12 @@ export const addFunctions = (logger, node, checker, state, options) => {
636
676
  }
637
677
  }
638
678
  const sessionless = expression.text !== 'pikkuFunc';
679
+ const implementationHash = computeImplementationHash({
680
+ wrapper: expression.text,
681
+ handler,
682
+ objectNode,
683
+ isDirectFunction,
684
+ });
639
685
  state.functions.meta[pikkuFuncId] = {
640
686
  pikkuFuncId,
641
687
  functionType: 'user',
@@ -655,6 +701,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
655
701
  deploy: deploy || undefined,
656
702
  approvalRequired: approvalRequired || undefined,
657
703
  approvalDescription: approvalDescription || undefined,
704
+ implementationHash,
658
705
  version,
659
706
  title,
660
707
  tags: tags || undefined,
@@ -146,17 +146,36 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
146
146
  return;
147
147
  }
148
148
  const input = fnMeta.inputs?.[0] || null;
149
+ const getRouteInputKeys = () => {
150
+ const targetFuncName = refAddonTarget ?? funcName;
151
+ const inputTypes = state.typesLookup.get(targetFuncName);
152
+ if (inputTypes && inputTypes.length > 0) {
153
+ return extractTypeKeys(inputTypes[0]);
154
+ }
155
+ const targetMeta = resolveFunctionMeta(state, targetFuncName);
156
+ if (targetMeta?.inputSchemaName) {
157
+ const schema = state.schemas[targetMeta.inputSchemaName];
158
+ const properties = schema?.properties;
159
+ if (properties && typeof properties === 'object') {
160
+ return Object.keys(properties);
161
+ }
162
+ }
163
+ return null;
164
+ };
149
165
  // Validate that route params and query params exist in function input type
150
166
  if (params.length > 0 || query.length > 0) {
151
- const inputTypes = state.typesLookup.get(funcName);
152
- if (inputTypes && inputTypes.length > 0) {
153
- const inputKeys = extractTypeKeys(inputTypes[0]);
167
+ const inputKeys = getRouteInputKeys();
168
+ if (!inputKeys) {
169
+ // Input shape isn't inspectable at this phase (e.g. addon ref or opaque handler).
170
+ // Skip param/query validation rather than emitting a false positive.
171
+ }
172
+ else {
154
173
  // Check path params
155
174
  if (params.length > 0) {
156
175
  const missingParams = params.filter((p) => !inputKeys.includes(p));
157
176
  if (missingParams.length > 0) {
158
177
  logger.critical(ErrorCode.ROUTE_PARAM_MISMATCH, `Route '${fullRoute}' has path parameter(s) [${missingParams.join(', ')}] ` +
159
- `not found in function '${funcName}' input type. ` +
178
+ `not found in function '${refAddonTarget ?? funcName}' input type. ` +
160
179
  `Input type has: [${inputKeys.join(', ')}]`);
161
180
  return;
162
181
  }
@@ -166,7 +185,7 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
166
185
  const missingQuery = query.filter((q) => !inputKeys.includes(q));
167
186
  if (missingQuery.length > 0) {
168
187
  logger.critical(ErrorCode.ROUTE_QUERY_MISMATCH, `Route '${fullRoute}' has query parameter(s) [${missingQuery.join(', ')}] ` +
169
- `not found in function '${funcName}' input type. ` +
188
+ `not found in function '${refAddonTarget ?? funcName}' input type. ` +
170
189
  `Input type has: [${inputKeys.join(', ')}]`);
171
190
  return;
172
191
  }
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import { getPropertyValue } from '../utils/get-property-value.js';
2
+ import { getPropertyValue, 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
5
  export const createAddKeyedWiring = (config) => {
@@ -19,6 +19,8 @@ export const createAddKeyedWiring = (config) => {
19
19
  }
20
20
  if (ts.isObjectLiteralExpression(firstArg)) {
21
21
  const obj = firstArg;
22
+ assertStringLiteralProperty(obj, 'name', config.label, logger);
23
+ assertStringLiteralProperty(obj, config.idField, config.label, logger);
22
24
  const nameValue = getPropertyValue(obj, 'name');
23
25
  const displayNameValue = getPropertyValue(obj, 'displayName');
24
26
  const descriptionValue = getPropertyValue(obj, 'description');
@@ -171,7 +171,7 @@ export const addMiddleware = (logger, node, checker, state) => {
171
171
  logger.debug(`• Found middleware factory with services: ${services.services.join(', ')}${name ? ` (name: ${name})` : ''}${description ? ` (description: ${description})` : ''}`);
172
172
  return;
173
173
  }
174
- if (expression.text === 'addMiddleware') {
174
+ if (expression.text === 'addTagMiddleware') {
175
175
  const tagArg = args[0];
176
176
  const middlewareArrayArg = args[1];
177
177
  if (!tagArg || !middlewareArrayArg)
@@ -181,11 +181,11 @@ export const addMiddleware = (logger, node, checker, state) => {
181
181
  tag = tagArg.text;
182
182
  }
183
183
  if (!tag) {
184
- logger.warn(`• addMiddleware call without valid tag string`);
184
+ logger.warn(`• addTagMiddleware call without valid tag string`);
185
185
  return;
186
186
  }
187
187
  if (!ts.isArrayLiteralExpression(middlewareArrayArg)) {
188
- logger.error(`• addMiddleware('${tag}', ...) must have a literal array as second argument`);
188
+ logger.error(`• addTagMiddleware('${tag}', ...) must have a literal array as second argument`);
189
189
  return;
190
190
  }
191
191
  const refs = extractMiddlewareRefs(middlewareArrayArg, checker, state.rootDir);
@@ -236,7 +236,7 @@ export const addMiddleware = (logger, node, checker, state) => {
236
236
  }
237
237
  if (!isFactory && exportedName) {
238
238
  logger.warn(`• Middleware group '${exportedName}' for tag '${tag}' is not wrapped in a factory function. ` +
239
- `For tree-shaking, use: export const ${exportedName} = () => addMiddleware('${tag}', [...])`);
239
+ `For tree-shaking, use: export const ${exportedName} = () => addTagMiddleware('${tag}', [...])`);
240
240
  }
241
241
  state.middleware.tagMiddleware.set(tag, {
242
242
  exportName: exportedName,
@@ -253,6 +253,34 @@ export const addMiddleware = (logger, node, checker, state) => {
253
253
  logger.debug(`• Found tag middleware group: ${tag} -> [${instanceIds.join(', ')}] (${isFactory ? 'factory' : 'direct'})`);
254
254
  return;
255
255
  }
256
+ if (expression.text === 'addGlobalMiddleware') {
257
+ const middlewareArrayArg = args[0];
258
+ if (!middlewareArrayArg ||
259
+ !ts.isArrayLiteralExpression(middlewareArrayArg)) {
260
+ logger.error(`• addGlobalMiddleware(...) must have a literal array as its only argument`);
261
+ return;
262
+ }
263
+ const refs = extractMiddlewareRefs(middlewareArrayArg, checker, state.rootDir);
264
+ const definitionIds = refs.map((r) => r.definitionId);
265
+ if (definitionIds.length > 0) {
266
+ renameTempDefinitions(state, definitionIds, 'global', 'middleware');
267
+ }
268
+ const sourceFile = node.getSourceFile().fileName;
269
+ for (let i = 0; i < refs.length; i++) {
270
+ const instanceId = makeContextBasedId('global', 'middleware', String(i));
271
+ state.middleware.instances[instanceId] = {
272
+ definitionId: definitionIds[i],
273
+ sourceFile,
274
+ position: node.getStart(),
275
+ isFactoryCall: refs[i].isFactoryCall,
276
+ };
277
+ }
278
+ // Without this, bootstrap codegen's "import every file with a wire-call"
279
+ // pass skips middleware-only files and the registration never runs.
280
+ state.http.files.add(sourceFile);
281
+ logger.debug(`• Found global middleware group with ${refs.length} entries`);
282
+ return;
283
+ }
256
284
  if (expression.text === 'addHTTPMiddleware') {
257
285
  const patternArg = args[0];
258
286
  const middlewareArrayArg = args[1];
@@ -332,6 +360,7 @@ export const addMiddleware = (logger, node, checker, state) => {
332
360
  instanceIds,
333
361
  isFactory,
334
362
  });
363
+ state.http.files.add(sourceFile);
335
364
  logger.debug(`• Found HTTP route middleware group: ${pattern} -> [${instanceIds.join(', ')}] (${isFactory ? 'factory' : 'direct'})`);
336
365
  return;
337
366
  }
@@ -27,7 +27,7 @@ function isInsidePermissionContainer(node) {
27
27
  if (ts.isCallExpression(current) &&
28
28
  ts.isIdentifier(current.expression) &&
29
29
  (current.expression.text === 'pikkuPermissionFactory' ||
30
- current.expression.text === 'addPermission' ||
30
+ current.expression.text === 'addTagPermission' ||
31
31
  current.expression.text === 'addHTTPPermission')) {
32
32
  return true;
33
33
  }
@@ -247,9 +247,9 @@ export const addPermission = (logger, node, checker, state) => {
247
247
  }
248
248
  // Handle addPermission('tag', [permission1, permission2])
249
249
  // Supports two patterns:
250
- // 1. export const x = () => addPermission('tag', [...]) (factory - tree-shakeable)
251
- // 2. export const x = addPermission('tag', [...]) (direct - no tree-shaking)
252
- if (expression.text === 'addPermission') {
250
+ // 1. export const x = () => addTagPermission('tag', [...]) (factory - tree-shakeable)
251
+ // 2. export const x = addTagPermission('tag', [...]) (direct - no tree-shaking)
252
+ if (expression.text === 'addTagPermission') {
253
253
  const tagArg = args[0];
254
254
  const permissionsArrayArg = args[1];
255
255
  if (!tagArg || !permissionsArrayArg)
@@ -260,13 +260,13 @@ export const addPermission = (logger, node, checker, state) => {
260
260
  tag = tagArg.text;
261
261
  }
262
262
  if (!tag) {
263
- logger.warn(`• addPermission call without valid tag string`);
263
+ logger.warn(`• addTagPermission call without valid tag string`);
264
264
  return;
265
265
  }
266
266
  // Check if permissions is a literal array or object
267
267
  if (!ts.isArrayLiteralExpression(permissionsArrayArg) &&
268
268
  !ts.isObjectLiteralExpression(permissionsArrayArg)) {
269
- logger.error(`• addPermission('${tag}', ...) must have a literal array or object as second argument`);
269
+ logger.error(`• addTagPermission('${tag}', ...) must have a literal array or object as second argument`);
270
270
  return;
271
271
  }
272
272
  // Extract permission pikkuFuncIds from array
@@ -305,7 +305,7 @@ export const addPermission = (logger, node, checker, state) => {
305
305
  }
306
306
  if (!isFactory && exportedName) {
307
307
  logger.warn(`• Permission group '${exportedName}' for tag '${tag}' is not wrapped in a factory function. ` +
308
- `For tree-shaking, use: export const ${exportedName} = () => addPermission('${tag}', [...])`);
308
+ `For tree-shaking, use: export const ${exportedName} = () => addTagPermission('${tag}', [...])`);
309
309
  }
310
310
  state.permissions.tagPermissions.set(tag, {
311
311
  exportName: exportedName,
@@ -281,6 +281,8 @@ function extractGraphFromNewFormat(nodesNode, configNode, checker, state) {
281
281
  input: {},
282
282
  next: undefined,
283
283
  onError: undefined,
284
+ retries: undefined,
285
+ retryDelay: undefined,
284
286
  };
285
287
  }
286
288
  // Extract config for each node from 'config' property
@@ -301,6 +303,8 @@ function extractGraphFromNewFormat(nodesNode, configNode, checker, state) {
301
303
  nodes[nodeId].next = nodeConfig.next;
302
304
  nodes[nodeId].onError = nodeConfig.onError;
303
305
  nodes[nodeId].input = nodeConfig.input;
306
+ nodes[nodeId].retries = nodeConfig.retries;
307
+ nodes[nodeId].retryDelay = nodeConfig.retryDelay;
304
308
  }
305
309
  }
306
310
  }
@@ -314,6 +318,8 @@ function extractNodeConfigFromObject(obj, checker) {
314
318
  let next = undefined;
315
319
  let onError = undefined;
316
320
  let input = {};
321
+ let retries = undefined;
322
+ let retryDelay = undefined;
317
323
  for (const prop of obj.properties) {
318
324
  if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
319
325
  continue;
@@ -327,8 +333,21 @@ function extractNodeConfigFromObject(obj, checker) {
327
333
  else if (propName === 'input') {
328
334
  input = extractInputMapping(prop.initializer, checker);
329
335
  }
336
+ else if (propName === 'retries') {
337
+ if (ts.isNumericLiteral(prop.initializer)) {
338
+ retries = Number(prop.initializer.text);
339
+ }
340
+ }
341
+ else if (propName === 'retryDelay') {
342
+ if (ts.isNumericLiteral(prop.initializer)) {
343
+ retryDelay = Number(prop.initializer.text);
344
+ }
345
+ else if (ts.isStringLiteral(prop.initializer)) {
346
+ retryDelay = prop.initializer.text;
347
+ }
348
+ }
330
349
  }
331
- return { next, onError, input };
350
+ return { next, onError, input, retries, retryDelay };
332
351
  }
333
352
  /**
334
353
  * Inspector for pikkuWorkflowGraph() calls
@@ -8,6 +8,7 @@
8
8
  */
9
9
  export declare enum ErrorCode {
10
10
  MISSING_NAME = "PKU111",
11
+ NON_LITERAL_WIRE_NAME = "PKU118",
11
12
  MISSING_DESCRIPTION = "PKU123",
12
13
  INVALID_VALUE = "PKU124",
13
14
  MISSING_URI = "PKU220",
@@ -53,5 +54,6 @@ export declare enum ErrorCode {
53
54
  SCHEMA_AND_WIRING_COLOCATED = "PKU490",
54
55
  SERVICES_NOT_DESTRUCTURED = "PKU410",
55
56
  WIRES_NOT_DESTRUCTURED = "PKU411",
56
- WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = "PKU901"
57
+ WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = "PKU901",
58
+ PII_IN_OUTPUT = "PKU910"
57
59
  }
@@ -10,6 +10,7 @@ export var ErrorCode;
10
10
  (function (ErrorCode) {
11
11
  // Validation errors
12
12
  ErrorCode["MISSING_NAME"] = "PKU111";
13
+ ErrorCode["NON_LITERAL_WIRE_NAME"] = "PKU118";
13
14
  ErrorCode["MISSING_DESCRIPTION"] = "PKU123";
14
15
  ErrorCode["INVALID_VALUE"] = "PKU124";
15
16
  ErrorCode["MISSING_URI"] = "PKU220";
@@ -66,4 +67,6 @@ export var ErrorCode;
66
67
  ErrorCode["WIRES_NOT_DESTRUCTURED"] = "PKU411";
67
68
  // Feature Flag
68
69
  ErrorCode["WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED"] = "PKU901";
70
+ // Data classification errors
71
+ ErrorCode["PII_IN_OUTPUT"] = "PKU910";
69
72
  })(ErrorCode || (ErrorCode = {}));
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { ErrorCode } from './error-codes.js';
6
6
  export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
7
7
  export type { SerializableInspectorState } from './utils/serialize-inspector-state.js';
8
8
  export { filterInspectorState } from './utils/filter-inspector-state.js';
9
+ export { resolveDeployTarget } from './utils/resolve-deploy-target.js';
9
10
  export { generateCustomTypes, sanitizeTypeName, } from './utils/custom-types-generator.js';
10
11
  export { createEmptyManifest, serializeManifest, } from './utils/contract-hashes.js';
11
12
  export type { ContractEntry, VersionHashEntry, VersionValidateError, VersionManifest, VersionManifestEntry, } from './utils/contract-hashes.js';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export { inspect, getInitialInspectorState } from './inspector.js';
2
2
  export { ErrorCode } from './error-codes.js';
3
3
  export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
4
4
  export { filterInspectorState } from './utils/filter-inspector-state.js';
5
+ export { resolveDeployTarget } from './utils/resolve-deploy-target.js';
5
6
  export { generateCustomTypes, sanitizeTypeName, } from './utils/custom-types-generator.js';
6
7
  export { createEmptyManifest, serializeManifest, } from './utils/contract-hashes.js';
7
8
  export { serializeMCPJson } from './utils/serialize-mcp-json.js';
package/dist/inspector.js CHANGED
@@ -4,7 +4,7 @@ import { visitSetup, visitRoutes } from './visit.js';
4
4
  import { TypesMap } from './types-map.js';
5
5
  import { getFilesAndMethods } from './utils/get-files-and-methods.js';
6
6
  import { findCommonAncestor } from './utils/find-root-dir.js';
7
- import { aggregateRequiredServices, validateAgentModels, validateAgentOverrides, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, validateSchemaWiringSeparation, } from './utils/post-process.js';
7
+ import { aggregateRequiredServices, validateAgentModels, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, validateSchemaWiringSeparation, } from './utils/post-process.js';
8
8
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js';
9
9
  import { pikkuState } from '@pikku/core/internal';
10
10
  import { resolveLatestVersions } from './utils/resolve-versions.js';
@@ -203,8 +203,6 @@ export const inspect = async (logger, routeFiles, options = {}) => {
203
203
  // Use provided rootDir or infer from source files
204
204
  const rootDir = options.rootDir || findCommonAncestor(routeFiles);
205
205
  const startSourceFiles = performance.now();
206
- // Filter source files to only include files within the project rootDir
207
- // This prevents picking up types from external packages (including workspace symlinks)
208
206
  const sourceFiles = program
209
207
  .getSourceFiles()
210
208
  .filter((sf) => sf.fileName.startsWith(rootDir));
@@ -262,8 +260,7 @@ export const inspect = async (logger, routeFiles, options = {}) => {
262
260
  if (options.openAPI) {
263
261
  state.openAPISpec = await generateOpenAPISpec(logger, state.functions.meta, state.http.meta, state.schemas, options.openAPI.additionalInfo, pikkuState(null, 'misc', 'errors'));
264
262
  }
265
- validateAgentModels(logger, state, options.modelConfig);
266
- validateAgentOverrides(logger, state, options.modelConfig);
263
+ validateAgentModels(logger, state);
267
264
  validateSecretOverrides(logger, state);
268
265
  validateVariableOverrides(logger, state);
269
266
  validateCredentialOverrides(logger, state);
package/dist/types.d.ts CHANGED
@@ -143,10 +143,19 @@ export interface InspectorPermissionState {
143
143
  export type InspectorFilters = {
144
144
  names?: string[];
145
145
  tags?: string[];
146
- types?: string[];
146
+ wires?: string[];
147
147
  directories?: string[];
148
148
  httpRoutes?: string[];
149
149
  httpMethods?: string[];
150
+ excludeNames?: string[];
151
+ excludeTags?: string[];
152
+ excludeWires?: string[];
153
+ excludeDirectories?: string[];
154
+ excludeHttpRoutes?: string[];
155
+ excludeHttpMethods?: string[];
156
+ target?: Array<'serverless' | 'server'>;
157
+ excludeTarget?: Array<'serverless' | 'server'>;
158
+ serverlessIncompatible?: string[];
150
159
  };
151
160
  export type AddonConfig = {
152
161
  package: string;
@@ -154,23 +163,6 @@ export type AddonConfig = {
154
163
  secretOverrides?: Record<string, string>;
155
164
  forceInclude?: boolean;
156
165
  };
157
- export type ModelConfigEntry = string | {
158
- model: string;
159
- temperature?: number;
160
- maxSteps?: number;
161
- };
162
- export type InspectorModelConfig = {
163
- models?: Record<string, ModelConfigEntry>;
164
- agentDefaults?: {
165
- temperature?: number;
166
- maxSteps?: number;
167
- };
168
- agentOverrides?: Record<string, {
169
- model?: string;
170
- temperature?: number;
171
- maxSteps?: number;
172
- }>;
173
- };
174
166
  export type InspectorOptions = Partial<{
175
167
  setupOnly: boolean;
176
168
  rootDir: string;
@@ -193,7 +185,6 @@ export type InspectorOptions = Partial<{
193
185
  };
194
186
  tags: string[];
195
187
  manifest: VersionManifest;
196
- modelConfig: InspectorModelConfig;
197
188
  oldProgram: ts.Program | undefined;
198
189
  }>;
199
190
  export interface InspectorLogger {
@@ -0,0 +1,14 @@
1
+ import * as ts from 'typescript';
2
+ /**
3
+ * Recursively walks a resolved TypeScript type looking for `__pii__` brands —
4
+ * the structural marker emitted by `Private<T>` and `Secret<T>`.
5
+ *
6
+ * `Private<T> = T & { readonly __pii__: 'private' }` shows up in the TS type
7
+ * system as an intersection whose constituents include a type with a `__pii__`
8
+ * property. We detect that by checking whether any constituent of an
9
+ * intersection exposes a property named `__pii__`.
10
+ *
11
+ * Returns the list of dotted field paths where a brand was found
12
+ * (e.g. `['email', 'address.phone']`). An empty array means clean.
13
+ */
14
+ export declare function findPiiPaths(checker: ts.TypeChecker, type: ts.Type, path?: string, depth?: number, seen?: Set<ts.Type>): string[];
@@ -0,0 +1,63 @@
1
+ import * as ts from 'typescript';
2
+ /**
3
+ * Recursively walks a resolved TypeScript type looking for `__pii__` brands —
4
+ * the structural marker emitted by `Private<T>` and `Secret<T>`.
5
+ *
6
+ * `Private<T> = T & { readonly __pii__: 'private' }` shows up in the TS type
7
+ * system as an intersection whose constituents include a type with a `__pii__`
8
+ * property. We detect that by checking whether any constituent of an
9
+ * intersection exposes a property named `__pii__`.
10
+ *
11
+ * Returns the list of dotted field paths where a brand was found
12
+ * (e.g. `['email', 'address.phone']`). An empty array means clean.
13
+ */
14
+ export function findPiiPaths(checker, type, path = '', depth = 0, seen = new Set()) {
15
+ if (depth > 8 || seen.has(type))
16
+ return [];
17
+ seen.add(type);
18
+ // ── Is this type itself branded? ─────────────────────────────────────────
19
+ // Private<T> = T & { readonly __pii__: 'private' } → isIntersection()
20
+ // where one constituent has a `__pii__` property.
21
+ if (type.isIntersection()) {
22
+ const branded = type.types.some((t) => t.getProperties().some((p) => p.name === '__pii__'));
23
+ if (branded) {
24
+ return [path || '<return value>'];
25
+ }
26
+ }
27
+ const violations = [];
28
+ // ── Union: check every branch ─────────────────────────────────────────────
29
+ if (type.isUnion()) {
30
+ for (const branch of type.types) {
31
+ violations.push(...findPiiPaths(checker, branch, path, depth, seen));
32
+ }
33
+ return violations;
34
+ }
35
+ // ── Object: recurse into named properties ─────────────────────────────────
36
+ if (type.flags & ts.TypeFlags.Object) {
37
+ const ref = type;
38
+ for (const arg of ref.typeArguments ?? []) {
39
+ violations.push(...findPiiPaths(checker, arg, path, depth + 1, seen));
40
+ }
41
+ const numberIndex = checker.getIndexTypeOfType(type, ts.IndexKind.Number);
42
+ if (numberIndex) {
43
+ const idxPath = path ? `${path}[]` : '[]';
44
+ violations.push(...findPiiPaths(checker, numberIndex, idxPath, depth + 1, seen));
45
+ }
46
+ const stringIndex = checker.getIndexTypeOfType(type, ts.IndexKind.String);
47
+ if (stringIndex) {
48
+ const idxPath = path ? `${path}[*]` : '[*]';
49
+ violations.push(...findPiiPaths(checker, stringIndex, idxPath, depth + 1, seen));
50
+ }
51
+ for (const prop of type.getProperties()) {
52
+ if (prop.name.startsWith('__'))
53
+ continue;
54
+ const decl = prop.valueDeclaration ?? prop.declarations?.[0];
55
+ if (!decl)
56
+ continue;
57
+ const propType = checker.getTypeOfSymbolAtLocation(prop, decl);
58
+ const subPath = path ? `${path}.${prop.name}` : prop.name;
59
+ violations.push(...findPiiPaths(checker, propType, subPath, depth + 1, seen));
60
+ }
61
+ }
62
+ return violations;
63
+ }
@@ -338,6 +338,12 @@ export function extractFunctionName(callExpr, checker, rootDir) {
338
338
  result.pikkuFuncId = `__temp_${randomUUID()}`;
339
339
  }
340
340
  if (result.version !== null) {
341
+ // Strip trailing VN suffix if it matches the version (e.g. createCardV1 + version:1 → createCard@v1)
342
+ const vSuffix = `V${result.version}`;
343
+ if (result.pikkuFuncId.endsWith(vSuffix) &&
344
+ result.pikkuFuncId.length > vSuffix.length) {
345
+ result.pikkuFuncId = result.pikkuFuncId.slice(0, -vSuffix.length);
346
+ }
341
347
  result.pikkuFuncId = formatVersionedId(result.pikkuFuncId, result.version);
342
348
  }
343
349
  return result;