@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
@@ -45,7 +45,10 @@ export function addWireRemoteAddon(
45
45
  ) {
46
46
  hasAuth = true
47
47
  for (const authProp of prop.initializer.properties) {
48
- if (!ts.isPropertyAssignment(authProp) || !ts.isIdentifier(authProp.name))
48
+ if (
49
+ !ts.isPropertyAssignment(authProp) ||
50
+ !ts.isIdentifier(authProp.name)
51
+ )
49
52
  continue
50
53
  if (
51
54
  authProp.name.text === 'credentialId' &&
@@ -0,0 +1,174 @@
1
+ import { strict as assert } from 'assert'
2
+ import { describe, test } from 'node:test'
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { inspect } from '../inspector.js'
7
+ import type { InspectorLogger } from '../types.js'
8
+
9
+ function makeLogger(): InspectorLogger {
10
+ return {
11
+ debug: () => {},
12
+ info: () => {},
13
+ warn: () => {},
14
+ error: () => {},
15
+ diagnostic: () => {},
16
+ critical: () => {},
17
+ hasCriticalErrors: () => false,
18
+ }
19
+ }
20
+
21
+ async function inspectGraph(graphSource: string[], graphName: string) {
22
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-graph-foreach-'))
23
+ const stepFile = join(rootDir, 'my.steps.ts')
24
+ const graphFile = join(rootDir, 'my.graph.ts')
25
+
26
+ await writeFile(
27
+ stepFile,
28
+ [
29
+ "import { pikkuSessionlessFunc } from '@pikku/core'",
30
+ 'export const listRows = pikkuSessionlessFunc({ func: async () => ({ rows: [] }) })',
31
+ 'export const postVideo = pikkuSessionlessFunc({ func: async () => ({ ok: true }) })',
32
+ ].join('\n')
33
+ )
34
+ await writeFile(graphFile, graphSource.join('\n'))
35
+
36
+ try {
37
+ const state = await inspect(makeLogger(), [stepFile, graphFile], {
38
+ rootDir,
39
+ })
40
+ const graph = state.workflows.graphMeta[graphName]
41
+ assert.ok(graph, `graph meta '${graphName}' should be registered`)
42
+ return graph
43
+ } finally {
44
+ await rm(rootDir, { recursive: true, force: true })
45
+ }
46
+ }
47
+
48
+ describe('addWorkflowGraph — forEach extraction', () => {
49
+ test('forEach node id and $item() lower into the serialized meta', async () => {
50
+ const graph = await inspectGraph(
51
+ [
52
+ "import { pikkuWorkflowGraph } from '@pikku/core/workflow'",
53
+ 'export const myGraph = pikkuWorkflowGraph({',
54
+ " name: 'foreach-graph',",
55
+ " nodes: { listRows: 'listRows', postVideo: 'postVideo' },",
56
+ ' config: {',
57
+ " listRows: { next: 'postVideo' },",
58
+ ' postVideo: {',
59
+ " forEach: 'listRows',",
60
+ ' input: (ref, template, $item) => ({',
61
+ " url: $item('URL VIDEO'),",
62
+ ' whole: $item(),',
63
+ " listed: ref('listRows'),",
64
+ ' }),',
65
+ ' },',
66
+ ' },',
67
+ '})',
68
+ ],
69
+ 'foreach-graph'
70
+ )
71
+
72
+ const node = graph.nodes['postVideo'] as any
73
+ assert.deepEqual(node.forEach, { $ref: 'listRows' })
74
+ assert.equal(node.mode, undefined)
75
+ assert.deepEqual(node.input, {
76
+ url: { $ref: '$item', path: 'URL VIDEO' },
77
+ whole: { $ref: '$item', path: undefined },
78
+ listed: { $ref: 'listRows', path: undefined },
79
+ })
80
+ })
81
+
82
+ test('forEach accepts a (ref) callback with a path, plus a sequential mode', async () => {
83
+ const graph = await inspectGraph(
84
+ [
85
+ "import { pikkuWorkflowGraph } from '@pikku/core/workflow'",
86
+ 'export const myGraph = pikkuWorkflowGraph({',
87
+ " name: 'foreach-callback-graph',",
88
+ " nodes: { listRows: 'listRows', postVideo: 'postVideo' },",
89
+ ' config: {',
90
+ " listRows: { next: 'postVideo' },",
91
+ ' postVideo: {',
92
+ " forEach: (ref) => ref('listRows', 'rows'),",
93
+ " mode: 'sequential',",
94
+ " input: (ref, template, $item) => ({ url: $item('URL') }),",
95
+ ' },',
96
+ ' },',
97
+ '})',
98
+ ],
99
+ 'foreach-callback-graph'
100
+ )
101
+
102
+ const node = graph.nodes['postVideo'] as any
103
+ assert.deepEqual(node.forEach, { $ref: 'listRows', path: 'rows' })
104
+ assert.equal(node.mode, 'sequential')
105
+ })
106
+
107
+ test('an existing (ref) => ... node keeps its exact input and gains no forEach', async () => {
108
+ const graph = await inspectGraph(
109
+ [
110
+ "import { pikkuWorkflowGraph } from '@pikku/core/workflow'",
111
+ "import { template } from '@pikku/core/workflow'",
112
+ 'export const myGraph = pikkuWorkflowGraph({',
113
+ " name: 'plain-graph',",
114
+ " nodes: { listRows: 'listRows', postVideo: 'postVideo' },",
115
+ ' config: {',
116
+ " listRows: { next: 'postVideo' },",
117
+ ' postVideo: {',
118
+ ' input: (ref) => ({',
119
+ " rows: ref('listRows', 'rows'),",
120
+ " label: template('rows: $0', [ref('listRows', 'rows')]),",
121
+ ' }),',
122
+ ' },',
123
+ ' },',
124
+ '})',
125
+ ],
126
+ 'plain-graph'
127
+ )
128
+
129
+ const node = graph.nodes['postVideo'] as any
130
+ assert.equal(node.forEach, undefined)
131
+ assert.equal(node.mode, undefined)
132
+ assert.deepEqual(node.input, {
133
+ rows: { $ref: 'listRows', path: 'rows' },
134
+ label: {
135
+ $template: {
136
+ parts: ['rows: ', ''],
137
+ expressions: [{ $ref: 'listRows', path: 'rows' }],
138
+ },
139
+ },
140
+ })
141
+ })
142
+
143
+ test('an existing (ref, template) => ... node still resolves template from the 2nd param', async () => {
144
+ const graph = await inspectGraph(
145
+ [
146
+ "import { pikkuWorkflowGraph } from '@pikku/core/workflow'",
147
+ 'export const myGraph = pikkuWorkflowGraph({',
148
+ " name: 'two-param-graph',",
149
+ " nodes: { listRows: 'listRows', postVideo: 'postVideo' },",
150
+ ' config: {',
151
+ " listRows: { next: 'postVideo' },",
152
+ ' postVideo: {',
153
+ ' input: (ref, template) => ({',
154
+ " label: template('rows: $0', [ref('listRows', 'rows')]),",
155
+ ' }),',
156
+ ' },',
157
+ ' },',
158
+ '})',
159
+ ],
160
+ 'two-param-graph'
161
+ )
162
+
163
+ const node = graph.nodes['postVideo'] as any
164
+ assert.equal(node.forEach, undefined)
165
+ assert.deepEqual(node.input, {
166
+ label: {
167
+ $template: {
168
+ parts: ['rows: ', ''],
169
+ expressions: [{ $ref: 'listRows', path: 'rows' }],
170
+ },
171
+ },
172
+ })
173
+ })
174
+ })
@@ -10,7 +10,8 @@ import type {
10
10
  function extractAstValue(
11
11
  expr: ts.Expression,
12
12
  refParamName: string,
13
- templateParamName: string | undefined
13
+ templateParamName: string | undefined,
14
+ itemParamName?: string
14
15
  ): unknown {
15
16
  // `'GET' as const` / parenthesised values wrap the real initializer — see
16
17
  // through them or the whole property is silently dropped from the meta.
@@ -43,6 +44,12 @@ function extractAstValue(
43
44
  args[1] && ts.isStringLiteral(args[1]) ? args[1].text : undefined
44
45
  return { $ref: nodeId, path } as DataRef
45
46
  }
47
+ if (itemParamName && callee.text === itemParamName) {
48
+ const pathArg = expr.arguments[0]
49
+ const path =
50
+ pathArg && ts.isStringLiteral(pathArg) ? pathArg.text : undefined
51
+ return { $ref: '$item', path } as DataRef
52
+ }
46
53
  if (templateParamName && callee.text === templateParamName) {
47
54
  const templateStr =
48
55
  expr.arguments[0] && ts.isStringLiteral(expr.arguments[0])
@@ -55,7 +62,8 @@ function extractAstValue(
55
62
  const resolved = extractAstValue(
56
63
  el,
57
64
  refParamName,
58
- templateParamName
65
+ templateParamName,
66
+ itemParamName
59
67
  )
60
68
  if (
61
69
  typeof resolved === 'object' &&
@@ -84,7 +92,7 @@ function extractAstValue(
84
92
  }
85
93
  if (ts.isArrayLiteralExpression(expr)) {
86
94
  return expr.elements.map((el) =>
87
- extractAstValue(el, refParamName, templateParamName)
95
+ extractAstValue(el, refParamName, templateParamName, itemParamName)
88
96
  )
89
97
  }
90
98
  if (ts.isObjectLiteralExpression(expr)) {
@@ -100,7 +108,8 @@ function extractAstValue(
100
108
  obj[key] = extractAstValue(
101
109
  prop.initializer,
102
110
  refParamName,
103
- templateParamName
111
+ templateParamName,
112
+ itemParamName
104
113
  )
105
114
  }
106
115
  return obj
@@ -160,6 +169,13 @@ function extractInputMapping(
160
169
  ? node.parameters[1].name.text
161
170
  : 'template'
162
171
 
172
+ // `$item` is appended after `template` so neither existing arrow shape —
173
+ // `(ref) => ...` nor `(ref, template) => ...` — shifts a parameter.
174
+ const itemParamName =
175
+ node.parameters.length > 2 && ts.isIdentifier(node.parameters[2].name)
176
+ ? node.parameters[2].name.text
177
+ : undefined
178
+
163
179
  const input: Record<string, unknown | DataRef> = {}
164
180
 
165
181
  for (const prop of bodyObj.properties) {
@@ -176,7 +192,8 @@ function extractInputMapping(
176
192
  const value = extractAstValue(
177
193
  prop.initializer,
178
194
  refParamName,
179
- templateParamName
195
+ templateParamName,
196
+ itemParamName
180
197
  )
181
198
  if (value !== undefined) {
182
199
  input[key] = value
@@ -186,6 +203,49 @@ function extractInputMapping(
186
203
  return input
187
204
  }
188
205
 
206
+ /**
207
+ * Extract a `forEach` source, authored either as a bare node id (`'listRows'`)
208
+ * or as a callback returning a ref (`(ref) => ref('listRows', 'rows')`).
209
+ */
210
+ function extractForEachConfig(node: ts.Node): DataRef | undefined {
211
+ while (ts.isAsExpression(node) || ts.isParenthesizedExpression(node)) {
212
+ node = node.expression
213
+ }
214
+
215
+ if (ts.isStringLiteral(node)) {
216
+ return { $ref: node.text }
217
+ }
218
+
219
+ if (ts.isArrowFunction(node)) {
220
+ const refParamName =
221
+ node.parameters.length > 0 && ts.isIdentifier(node.parameters[0].name)
222
+ ? node.parameters[0].name.text
223
+ : 'ref'
224
+
225
+ let body: ts.Node = node.body
226
+ if (ts.isBlock(body)) {
227
+ for (const stmt of body.statements) {
228
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
229
+ body = stmt.expression
230
+ }
231
+ }
232
+ }
233
+ if (!ts.isExpression(body)) return undefined
234
+
235
+ const value = extractAstValue(body, refParamName, undefined)
236
+ if (
237
+ typeof value === 'object' &&
238
+ value !== null &&
239
+ '$ref' in value &&
240
+ typeof (value as DataRef).$ref === 'string'
241
+ ) {
242
+ return value as DataRef
243
+ }
244
+ }
245
+
246
+ return undefined
247
+ }
248
+
189
249
  /**
190
250
  * Extract next config (string, array, or record)
191
251
  */
@@ -380,6 +440,8 @@ function extractGraphFromNewFormat(
380
440
  nodeId,
381
441
  rpcName,
382
442
  input: {},
443
+ forEach: undefined,
444
+ mode: undefined,
383
445
  next: undefined,
384
446
  onError: undefined,
385
447
  retries: undefined,
@@ -410,6 +472,8 @@ function extractGraphFromNewFormat(
410
472
  nodes[nodeId].next = nodeConfig.next
411
473
  nodes[nodeId].onError = nodeConfig.onError
412
474
  nodes[nodeId].input = nodeConfig.input
475
+ nodes[nodeId].forEach = nodeConfig.forEach
476
+ nodes[nodeId].mode = nodeConfig.mode
413
477
  nodes[nodeId].retries = nodeConfig.retries
414
478
  nodes[nodeId].retryDelay = nodeConfig.retryDelay
415
479
  nodes[nodeId].notes = nodeConfig.notes
@@ -431,6 +495,8 @@ function extractNodeConfigFromObject(
431
495
  next: any
432
496
  onError: any
433
497
  input: Record<string, any>
498
+ forEach: DataRef | undefined
499
+ mode: string | undefined
434
500
  retries: number | undefined
435
501
  retryDelay: string | number | undefined
436
502
  notes: string | undefined
@@ -438,6 +504,8 @@ function extractNodeConfigFromObject(
438
504
  let next: any = undefined
439
505
  let onError: any = undefined
440
506
  let input: Record<string, any> = {}
507
+ let forEach: DataRef | undefined = undefined
508
+ let mode: string | undefined = undefined
441
509
  let retries: number | undefined = undefined
442
510
  let retryDelay: string | number | undefined = undefined
443
511
  let notes: string | undefined = undefined
@@ -453,6 +521,10 @@ function extractNodeConfigFromObject(
453
521
  onError = extractNextConfig(prop.initializer, checker)
454
522
  } else if (propName === 'input') {
455
523
  input = extractInputMapping(prop.initializer, checker)
524
+ } else if (propName === 'forEach') {
525
+ forEach = extractForEachConfig(prop.initializer)
526
+ } else if (propName === 'mode') {
527
+ mode = extractStringLiteral(prop.initializer, checker)
456
528
  } else if (propName === 'retries') {
457
529
  if (ts.isNumericLiteral(prop.initializer)) {
458
530
  retries = Number(prop.initializer.text)
@@ -468,7 +540,7 @@ function extractNodeConfigFromObject(
468
540
  }
469
541
  }
470
542
 
471
- return { next, onError, input, retries, retryDelay, notes }
543
+ return { next, onError, input, forEach, mode, retries, retryDelay, notes }
472
544
  }
473
545
 
474
546
  /**
@@ -77,10 +77,9 @@ describe('sleep with a duration that is not a literal', () => {
77
77
 
78
78
  test('a literal duration still round-trips as a value', async () => {
79
79
  const { graph, diags } = await run(
80
- [
81
- " await workflow.sleep('Wait', '5s')",
82
- ' return { sent: true }',
83
- ].join('\n')
80
+ [" await workflow.sleep('Wait', '5s')", ' return { sent: true }'].join(
81
+ '\n'
82
+ )
84
83
  )
85
84
 
86
85
  assert.deepEqual(diags, [])
@@ -178,7 +178,7 @@ describe('addon contract metadata', () => {
178
178
  await writeFile(
179
179
  appFile,
180
180
  [
181
- "import { wireAddon } from '@pikku/core/rpc'",
181
+ "import { wireAddon } from '@pikku/core/addon'",
182
182
  "import { wireHTTPRoutes } from '@pikku/core/http'",
183
183
  "import { wireChannel } from '@pikku/core/channel'",
184
184
  "import { wireCLI } from '@pikku/core/cli'",
@@ -167,3 +167,97 @@ describe('config value metadata (docsUrl)', () => {
167
167
  }
168
168
  })
169
169
  })
170
+
171
+ /**
172
+ * `optional` and `required` are the deploy gate's whole input. A flag the
173
+ * inspector drops leaves the gate seeing a bare declaration, which is how an
174
+ * optional secret suspends a deploy over a value production is meant NOT to
175
+ * have.
176
+ */
177
+ describe('config value gating flags', () => {
178
+ test('carries optional from defineSecret into the definition', async () => {
179
+ const rootDir = await mkdtemp(`${fixtureRoot}-secret-optional-`)
180
+ const file = join(rootDir, 'secret.ts')
181
+
182
+ await writeFile(
183
+ file,
184
+ [
185
+ "import { defineSecret } from '@pikku/core'",
186
+ "import { z } from 'zod'",
187
+ 'export const ActorSchema = z.string()',
188
+ 'export const StripeSchema = z.string()',
189
+ 'defineSecret({',
190
+ " name: 'scenarioActor',",
191
+ " displayName: 'Scenario Actor Secret',",
192
+ " secretId: 'SCENARIO_ACTOR_SECRET',",
193
+ ' optional: true,',
194
+ ' schema: ActorSchema,',
195
+ '})',
196
+ 'defineSecret({',
197
+ " name: 'stripe',",
198
+ " displayName: 'Stripe',",
199
+ " secretId: 'STRIPE_KEY',",
200
+ ' schema: StripeSchema,',
201
+ '})',
202
+ ].join('\n')
203
+ )
204
+
205
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
206
+ try {
207
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
208
+ const optional = state.secrets.definitions.find(
209
+ (d) => d.secretId === 'SCENARIO_ACTOR_SECRET'
210
+ )
211
+ const required = state.secrets.definitions.find(
212
+ (d) => d.secretId === 'STRIPE_KEY'
213
+ )
214
+ assert.equal(optional!.optional, true)
215
+ assert.equal(required!.optional, undefined)
216
+ } finally {
217
+ await rm(rootDir, { recursive: true, force: true })
218
+ }
219
+ })
220
+
221
+ test('carries required from defineVariable into the definition', async () => {
222
+ const rootDir = await mkdtemp(`${fixtureRoot}-variable-required-`)
223
+ const file = join(rootDir, 'variable.ts')
224
+
225
+ await writeFile(
226
+ file,
227
+ [
228
+ "import { defineVariable } from '@pikku/core'",
229
+ "import { z } from 'zod'",
230
+ 'export const ConsoleUrlSchema = z.string()',
231
+ 'export const CorsSchema = z.string()',
232
+ 'defineVariable({',
233
+ " name: 'consoleUrl',",
234
+ " displayName: 'Console URL',",
235
+ " variableId: 'CONSOLE_URL',",
236
+ ' required: true,',
237
+ ' schema: ConsoleUrlSchema,',
238
+ '})',
239
+ 'defineVariable({',
240
+ " name: 'corsOrigins',",
241
+ " displayName: 'Allowed Browser Origins',",
242
+ " variableId: 'CORS_ORIGINS',",
243
+ ' schema: CorsSchema,',
244
+ '})',
245
+ ].join('\n')
246
+ )
247
+
248
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
249
+ try {
250
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
251
+ const required = state.variables.definitions.find(
252
+ (d) => d.variableId === 'CONSOLE_URL'
253
+ )
254
+ const optional = state.variables.definitions.find(
255
+ (d) => d.variableId === 'CORS_ORIGINS'
256
+ )
257
+ assert.equal(required!.required, true)
258
+ assert.equal(optional!.required, undefined)
259
+ } finally {
260
+ await rm(rootDir, { recursive: true, force: true })
261
+ }
262
+ })
263
+ })
@@ -75,7 +75,10 @@ async function runInspect(source: string) {
75
75
  await writeFile(file, PRELUDE + source)
76
76
  const { logger, diagnostics } = makeLogger()
77
77
  try {
78
- await inspect(logger, [file], { rootDir: tmpDir, classificationCheck: true })
78
+ await inspect(logger, [file], {
79
+ rootDir: tmpDir,
80
+ classificationCheck: true,
81
+ })
79
82
  } finally {
80
83
  await rm(tmpDir, { recursive: true, force: true })
81
84
  }
@@ -22,7 +22,7 @@ export enum ErrorCode {
22
22
  CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = 'PKU672',
23
23
  SCENARIO_HAS_SERVICES = 'PKU673',
24
24
  EXPECT_EVENTUALLY_SCENARIO_ONLY = 'PKU675',
25
- SCENARIO_BROWSER_STEP_NEEDS_ACTOR = 'PKU677',
25
+ SCENARIO_STEP_NEEDS_ACTOR = 'PKU677',
26
26
  SCENARIO_STEP_TARGET_NOT_STATIC = 'PKU678',
27
27
  SCENARIO_NOT_EXTRACTABLE = 'PKU679',
28
28
  SCENARIO_HAS_NO_ASSERTION = 'PKU680',
@@ -34,6 +34,7 @@ export enum ErrorCode {
34
34
  // Remote addon (wireRemoteAddon) validation
35
35
  REMOTE_ADDON_NOT_DEV_DEPENDENCY = 'PKU338',
36
36
  REMOTE_ADDON_AUTH_UNRESOLVED = 'PKU339',
37
+ ADDON_NOT_INSTALLED = 'PKU340',
37
38
 
38
39
  // Database schema codegen warnings
39
40
  DB_COLUMN_NAME_TYPE_CONTRADICTION = 'PKU480',
@@ -106,6 +107,7 @@ export enum ErrorCode {
106
107
  DUPLICATE_CORE_VERSION = 'PKU717',
107
108
  CORE_VERSION_SKEW = 'PKU718',
108
109
  SPLIT_TYPE_IDENTITY = 'PKU719',
110
+ SINGLETON_SERVICES_TYPE_UNRESOLVED = 'PKU724',
109
111
 
110
112
  // Data classification errors
111
113
  PII_IN_OUTPUT = 'PKU910',
package/src/index.ts CHANGED
@@ -21,6 +21,8 @@ export {
21
21
  generateCustomTypes,
22
22
  sanitizeTypeName,
23
23
  } from './utils/custom-types-generator.js'
24
+ export { unresolvedSchemaReferences } from './utils/post-process.js'
25
+ export type { SurfaceUsageCounts } from './utils/extract-surface-usage.js'
24
26
  export {
25
27
  createEmptyManifest,
26
28
  serializeManifest,
@@ -32,10 +34,7 @@ export type {
32
34
  VersionManifest,
33
35
  VersionManifestEntry,
34
36
  } from './utils/contract-hashes.js'
35
- export type {
36
- InspectorFeature,
37
- InspectorFeatureEntry,
38
- } from './types.js'
37
+ export type { InspectorFeature, InspectorFeatureEntry } from './types.js'
39
38
  export { serializeMCPJson } from './utils/serialize-mcp-json.js'
40
39
  export type { OpenAPISpecInfo } from './utils/serialize-openapi-json.js'
41
40
  export {
package/src/inspector.ts 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 type {
@@ -42,7 +42,7 @@ import { validateExposedFunctionsGated } from './utils/validate-exposed-function
42
42
  import { validateTagsResolveToMiddleware } from './utils/validate-tags-resolve-to-middleware.js'
43
43
  import { annotateHttpRouteAuth } from './utils/annotate-http-route-auth.js'
44
44
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js'
45
- import { pikkuState } from '@pikku/core/ecosystem'
45
+ import { pikkuState } from '@pikku/core/state'
46
46
  import { resolveLatestVersions } from './utils/resolve-versions.js'
47
47
  import { finalizeWorkflows } from './utils/workflow/graph/finalize-workflows.js'
48
48
  import {
@@ -51,6 +51,10 @@ import {
51
51
  } from './utils/workflow/graph/finalize-workflow-wires.js'
52
52
  import { generateAllSchemas } from './utils/schema-generator.js'
53
53
  import { extractSecretUsage } from './utils/extract-secret-usage.js'
54
+ import {
55
+ accumulateSurfaceUsage,
56
+ surfaceUsageArea,
57
+ } from './utils/extract-surface-usage.js'
54
58
  import {
55
59
  loadAddonFunctionsMeta,
56
60
  loadAddonSchemas,
@@ -186,6 +190,7 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
186
190
  files: new Set(),
187
191
  usage: new Map(),
188
192
  },
193
+ surfaceUsage: {},
189
194
  credentials: {
190
195
  definitions: [],
191
196
  files: new Set(),
@@ -221,7 +226,7 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
221
226
  instances: {},
222
227
  tagMiddleware: new Map(),
223
228
  },
224
- aiMiddleware: {
229
+ agentMiddleware: {
225
230
  definitions: {},
226
231
  },
227
232
  permissions: {
@@ -378,11 +383,23 @@ export const inspect = async (
378
383
 
379
384
  const state = getInitialInspectorState(rootDir)
380
385
 
386
+ const surfaceAreaCache = new Map<string, string | null>()
381
387
  for (const sourceFile of sourceFiles) {
382
388
  const usage = extractSecretUsage(sourceFile)
383
389
  if (usage.keys.length > 0 || usage.dynamic.length > 0) {
384
390
  state.secrets.usage.set(sourceFile.fileName, usage)
385
391
  }
392
+ // Counted in the sweep that already has this file open. The public surface
393
+ // is documentation, and documentation may not cost a project a second pass
394
+ // over its source.
395
+ accumulateSurfaceUsage(
396
+ sourceFile,
397
+ {
398
+ area: surfaceUsageArea(sourceFile.fileName, rootDir, surfaceAreaCache),
399
+ file: relative(rootDir, sourceFile.fileName),
400
+ },
401
+ state.surfaceUsage
402
+ )
386
403
  }
387
404
 
388
405
  // First sweep: add all functions
package/src/types.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'
@@ -12,8 +13,8 @@ import type {
12
13
  MCPToolMeta,
13
14
  MCPPromptMeta,
14
15
  } from '@pikku/core/mcp'
15
- import type { AIAgentMeta } from '@pikku/core/ai-agent'
16
- import type { ScorerMeta } from '@pikku/core/ai-scorer'
16
+ import type { AgentsMeta } from '@pikku/core/agent'
17
+ import type { ScorerMeta } from '@pikku/core/agent-scorer'
17
18
  import type { CLIMeta } from '@pikku/core/cli'
18
19
  import type { CLICommandMeta } from '@pikku/core/cli'
19
20
  import type { NodesMeta } from '@pikku/core/node'
@@ -24,12 +25,12 @@ import type { SystemRoleDefinitions } from '@pikku/core/role'
24
25
  import type { PersonaDefinitions } from '@pikku/core/persona'
25
26
  import type { VariableDefinitions } from '@pikku/core/variable'
26
27
  import type { TypesMap } from './types-map.js'
28
+ import type { FunctionsMeta } from '@pikku/core/services'
27
29
  import type {
28
- FunctionsMeta,
29
30
  FunctionServicesMeta,
30
31
  FunctionWiresMeta,
31
- JSONValue,
32
- } from '@pikku/core'
32
+ } from '@pikku/core/function'
33
+ import type { JSONValue } from '@pikku/core/utils'
33
34
  import type { OpenAPISpecInfo } from './utils/serialize-openapi-json.js'
34
35
  import type { ErrorCode, CodedDiagnostic } from './error-codes.js'
35
36
  import type {
@@ -613,7 +614,7 @@ export interface InspectorState {
613
614
  files: Set<string>
614
615
  }
615
616
  agents: {
616
- agentsMeta: AIAgentMeta
617
+ agentsMeta: AgentsMeta
617
618
  files: Map<string, { path: string; exportedName: string }>
618
619
  }
619
620
  scorers: {
@@ -654,6 +655,8 @@ export interface InspectorState {
654
655
  /** Secret reads found per source file, keyed by absolute path. */
655
656
  usage: Map<string, SecretUsage>
656
657
  }
658
+ /** How often each `#pikku/*` export is imported, and from where. */
659
+ surfaceUsage: SurfaceUsageCounts
657
660
  credentials: {
658
661
  definitions: CredentialDefinitions
659
662
  files: Set<string>
@@ -681,7 +684,7 @@ export interface InspectorState {
681
684
  }
682
685
  middleware: InspectorMiddlewareState
683
686
  channelMiddleware: InspectorChannelMiddlewareState
684
- aiMiddleware: InspectorAIMiddlewareState
687
+ agentMiddleware: InspectorAIMiddlewareState
685
688
  permissions: InspectorPermissionState
686
689
  serviceAggregation: {
687
690
  requiredServices: Set<string>