@pikku/inspector 0.12.60 → 0.12.62

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 (139) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/OPTIMIZATION-PLAN.md +19 -19
  3. package/dist/add/add-auth.js +1 -1
  4. package/dist/add/add-channel.d.ts +1 -1
  5. package/dist/add/add-cli.js +5 -0
  6. package/dist/add/add-functions.js +10 -1
  7. package/dist/add/add-http-route.js +18 -18
  8. package/dist/add/add-keyed-wiring.d.ts +2 -0
  9. package/dist/add/add-keyed-wiring.js +3 -1
  10. package/dist/add/add-personas.js +4 -1
  11. package/dist/add/add-secret.js +1 -0
  12. package/dist/add/add-variable.js +1 -0
  13. package/dist/add/add-wire-remote-addon.js +2 -1
  14. package/dist/add/add-workflow-graph.js +63 -6
  15. package/dist/add/add-workflow.d.ts +1 -1
  16. package/dist/error-codes.d.ts +3 -1
  17. package/dist/error-codes.js +3 -1
  18. package/dist/index.d.ts +4 -2
  19. package/dist/index.js +1 -0
  20. package/dist/inspector.js +12 -2
  21. package/dist/types.d.ts +25 -21
  22. package/dist/utils/annotate-http-route-auth.js +2 -1
  23. package/dist/utils/check-secret-sinks.js +2 -1
  24. package/dist/utils/compute-required-schemas.d.ts +1 -1
  25. package/dist/utils/contract-hashes.d.ts +2 -2
  26. package/dist/utils/contract-hashes.js +1 -1
  27. package/dist/utils/extract-function-name.js +1 -1
  28. package/dist/utils/extract-services.d.ts +1 -1
  29. package/dist/utils/extract-surface-usage.d.ts +29 -0
  30. package/dist/utils/extract-surface-usage.js +97 -0
  31. package/dist/utils/filter-inspector-state.js +53 -16
  32. package/dist/utils/filter-utils.d.ts +1 -1
  33. package/dist/utils/get-property-value.js +42 -3
  34. package/dist/utils/load-addon-functions-meta.js +31 -0
  35. package/dist/utils/middleware.d.ts +1 -1
  36. package/dist/utils/permissions.d.ts +1 -1
  37. package/dist/utils/post-process.d.ts +27 -7
  38. package/dist/utils/post-process.js +79 -14
  39. package/dist/utils/resolve-deploy-target.d.ts +1 -1
  40. package/dist/utils/resolve-function-meta.d.ts +1 -1
  41. package/dist/utils/resolve-function-types.d.ts +1 -1
  42. package/dist/utils/resolve-versions.js +1 -1
  43. package/dist/utils/schema-generator.d.ts +1 -1
  44. package/dist/utils/serialize-inspector-state.d.ts +2 -1
  45. package/dist/utils/serialize-inspector-state.js +2 -0
  46. package/dist/utils/serialize-openapi-json.d.ts +2 -2
  47. package/dist/utils/validate-auth-sessionless.js +2 -1
  48. package/dist/utils/workflow/derive-workflow-plan.d.ts +1 -1
  49. package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +1 -1
  50. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +1 -1
  51. package/dist/utils/workflow/graph/finalize-workflows.js +1 -1
  52. package/dist/utils/workflow/graph/index.d.ts +2 -1
  53. package/dist/utils/workflow/graph/index.js +1 -1
  54. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +6 -1
  55. package/dist/utils/workflow/graph/serialize-workflow-graph.js +26 -2
  56. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +11 -2
  57. package/package.json +10 -10
  58. package/run-tests.sh +0 -0
  59. package/src/add/add-agent.ts +2 -4
  60. package/src/add/add-auth.ts +2 -2
  61. package/src/add/add-channel.ts +1 -4
  62. package/src/add/add-cli.ts +7 -1
  63. package/src/add/add-credential.ts +3 -6
  64. package/src/add/add-file-with-factory.ts +2 -6
  65. package/src/add/add-functions.ts +24 -31
  66. package/src/add/add-gateway.ts +1 -1
  67. package/src/add/add-http-route-ref.test.ts +149 -0
  68. package/src/add/add-http-route.ts +26 -29
  69. package/src/add/add-keyed-wiring.ts +8 -7
  70. package/src/add/add-permission.ts +1 -1
  71. package/src/add/add-personas.test.ts +26 -2
  72. package/src/add/add-personas.ts +4 -3
  73. package/src/add/add-scenario-step.test.ts +37 -2
  74. package/src/add/add-scope.test.ts +15 -15
  75. package/src/add/add-scope.ts +5 -9
  76. package/src/add/add-secret.ts +1 -0
  77. package/src/add/add-system-role.test.ts +4 -4
  78. package/src/add/add-system-role.ts +2 -4
  79. package/src/add/add-variable.ts +1 -0
  80. package/src/add/add-wire-remote-addon.ts +4 -1
  81. package/src/add/add-workflow-graph-foreach.test.ts +174 -0
  82. package/src/add/add-workflow-graph-input.test.ts +1 -1
  83. package/src/add/add-workflow-graph.ts +78 -6
  84. package/src/add/add-workflow-sleep-expression.test.ts +3 -4
  85. package/src/add/add-workflow.ts +1 -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 +19 -5
  91. package/src/inspector.ts +19 -2
  92. package/src/types.ts +25 -28
  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 -3
  99. package/src/utils/extract-function-name.ts +1 -1
  100. package/src/utils/extract-services.ts +1 -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.ts +1 -1
  109. package/src/utils/permissions.ts +1 -1
  110. package/src/utils/post-process.test.ts +59 -2
  111. package/src/utils/post-process.ts +88 -25
  112. package/src/utils/resolve-deploy-target.ts +1 -1
  113. package/src/utils/resolve-function-meta.ts +1 -4
  114. package/src/utils/resolve-function-types.ts +1 -1
  115. package/src/utils/resolve-versions.ts +2 -5
  116. package/src/utils/schema-generator.ts +3 -3
  117. package/src/utils/serialize-inspector-state.ts +4 -1
  118. package/src/utils/serialize-mcp-json.ts +1 -1
  119. package/src/utils/serialize-openapi-json.ts +2 -2
  120. package/src/utils/validate-auth-sessionless.ts +2 -1
  121. package/src/utils/validate-tags-resolve-to-middleware.test.ts +4 -1
  122. package/src/utils/workflow/derive-workflow-plan.test.ts +1 -1
  123. package/src/utils/workflow/derive-workflow-plan.ts +1 -1
  124. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +1 -1
  125. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +2 -2
  126. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +1 -1
  127. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +1 -4
  128. package/src/utils/workflow/graph/finalize-workflow-wires.ts +1 -1
  129. package/src/utils/workflow/graph/finalize-workflows.ts +2 -2
  130. package/src/utils/workflow/graph/index.ts +18 -1
  131. package/src/utils/workflow/graph/serialize-workflow-graph.ts +34 -3
  132. package/src/utils/workflow/graph/workflow-graph.types.ts +12 -2
  133. package/tsconfig.tsbuildinfo +1 -1
  134. package/dist/add/add-ai-agent.d.ts +0 -4
  135. package/dist/add/add-ai-agent.js +0 -442
  136. package/dist/add/add-ai-scorer.d.ts +0 -2
  137. package/dist/add/add-ai-scorer.js +0 -61
  138. package/dist/utils/secret-brokers.d.ts +0 -2
  139. package/dist/utils/secret-brokers.js +0 -9
@@ -127,6 +127,46 @@ export const assertStringLiteralProperty = (
127
127
  }
128
128
  }
129
129
 
130
+ /**
131
+ * Fold a node that is a compile-time constant string down to its value.
132
+ *
133
+ * The fallback below this is `getText()`, which returns the node's *source*.
134
+ * For a lone `'…'` that is indistinguishable from the value, so nobody noticed
135
+ * that anything written as an expression comes through raw: a `description`
136
+ * split across lines as `'Fetch the thing ' + 'from upstream'` reaches
137
+ * `infra.json` with the quotes and the `+` still in it, and is rendered that
138
+ * way in the fabric console.
139
+ *
140
+ * Deliberately checker-free, unlike `extractStringLiteral`. Every value this
141
+ * resolves is a literal in the same expression, so no symbol resolution is
142
+ * needed, and `getPropertyValue` is called from a dozen places that have no
143
+ * checker to hand. Anything it cannot fold — an identifier, a template with a
144
+ * substitution — returns `undefined` and takes the old path, so this only ever
145
+ * turns a wrong answer into a right one.
146
+ */
147
+ const foldStaticString = (node: ts.Node): string | undefined => {
148
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
149
+ return node.text
150
+ }
151
+ if (ts.isParenthesizedExpression(node)) {
152
+ return foldStaticString(node.expression)
153
+ }
154
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
155
+ return foldStaticString(node.expression)
156
+ }
157
+ if (
158
+ ts.isBinaryExpression(node) &&
159
+ node.operatorToken.kind === ts.SyntaxKind.PlusToken
160
+ ) {
161
+ const left = foldStaticString(node.left)
162
+ if (left === undefined) return undefined
163
+ const right = foldStaticString(node.right)
164
+ if (right === undefined) return undefined
165
+ return left + right
166
+ }
167
+ return undefined
168
+ }
169
+
130
170
  export const getPropertyValue = (
131
171
  obj: ts.ObjectLiteralExpression,
132
172
  propertyName: string
@@ -174,11 +214,9 @@ export const getPropertyValue = (
174
214
  return Number(initializer.text)
175
215
  }
176
216
 
177
- if (
178
- ts.isStringLiteral(initializer) ||
179
- ts.isNoSubstitutionTemplateLiteral(initializer)
180
- ) {
181
- return initializer.text
217
+ const staticString = foldStaticString(initializer)
218
+ if (staticString !== undefined) {
219
+ return staticString
182
220
  }
183
221
 
184
222
  return initializer.getText()
@@ -1,3 +1,4 @@
1
+ import { existsSync } from 'fs'
1
2
  import { readFile, readdir } from 'fs/promises'
2
3
  import { createRequire } from 'module'
3
4
  import { join, dirname } from 'path'
@@ -141,6 +142,44 @@ const resolveAddonMeta = (
141
142
  return null
142
143
  }
143
144
 
145
+ /**
146
+ * Give the addon's input/output type names a declaration file to be imported
147
+ * from, so a wiring that references an addon function can name its contract.
148
+ *
149
+ * Without this the only thing that knows where `GetScenarioArtifactInput` lives
150
+ * is the type checker, and it only knows once the consumer's *own* rpc map has
151
+ * already been generated with the addon's routes in it — which is to say on the
152
+ * second run. A first, cold run resolved `ref('console:getScenarioArtifact')`
153
+ * against a map that did not mention it yet and fell back to the whole
154
+ * `FlattenedRPCMap`, so the same source produced a different http-map depending
155
+ * on whether `.pikku` happened to be there. The addon publishes these types at a
156
+ * fixed subpath; reading them from the metadata makes the first run agree with
157
+ * the second.
158
+ */
159
+ const registerAddonTypes = (
160
+ state: InspectorState,
161
+ metaPath: string,
162
+ meta: Record<string, any>
163
+ ): void => {
164
+ // Derived from the metadata's own location rather than resolved as a package
165
+ // subpath: the addon exports that declaration under a `types`-only condition,
166
+ // which `require.resolve` will not follow.
167
+ const typesPath = join(
168
+ dirname(metaPath),
169
+ '..',
170
+ 'rpc',
171
+ 'pikku-rpc-wirings-map.internal.gen.d.ts'
172
+ )
173
+ if (!existsSync(typesPath)) return
174
+
175
+ for (const funcMeta of Object.values(meta)) {
176
+ for (const name of [funcMeta.inputSchemaName, funcMeta.outputSchemaName]) {
177
+ if (!name || state.functions.typesMap.exists(name, typesPath)) continue
178
+ state.functions.typesMap.addType(name, typesPath)
179
+ }
180
+ }
181
+ }
182
+
144
183
  /**
145
184
  * After the setup sweep discovers wireAddon() declarations, load each addon
146
185
  * package's function metadata so that wiring handlers (channels, HTTP routes,
@@ -177,6 +216,7 @@ export async function loadAddonFunctionsMeta(
177
216
  logger.debug(
178
217
  `Loaded ${Object.keys(meta).length} addon functions for '${namespace}' from ${decl.package}`
179
218
  )
219
+ registerAddonTypes(state, metaPath, meta)
180
220
 
181
221
  // If wireAddon has mcp: true, expose addon functions with mcp: true as MCP tools
182
222
  if (decl.mcp) {
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript'
2
- import type { MiddlewareMetadata } from '@pikku/core/ecosystem/types'
2
+ import type { MiddlewareMetadata } from '@pikku/core/middleware'
3
3
  import { extractFunctionName } from './extract-function-name.js'
4
4
  import type { InspectorState } from '../types.js'
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript'
2
- import type { PermissionMetadata } from '@pikku/core/ecosystem/types'
2
+ import type { PermissionMetadata } from '@pikku/core/function'
3
3
  import { extractFunctionName } from './extract-function-name.js'
4
4
  import type { InspectorState } from '../types.js'
5
5
 
@@ -1,6 +1,6 @@
1
1
  import { strict as assert } from 'assert'
2
2
  import { describe, test } from 'node:test'
3
- import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
3
+ import { mkdirSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'
4
4
  import { tmpdir } from 'node:os'
5
5
  import { join } from 'node:path'
6
6
  import {
@@ -579,10 +579,67 @@ describe('validateRemoteAddonDependencies (wireRemoteAddon must be a devDependen
579
579
  }
580
580
  })
581
581
 
582
- test('ignores a non-remote (wireAddon) declaration', () => {
582
+ test('a non-remote (wireAddon) declaration is judged on resolvability, not package.json', () => {
583
+ // Being listed as a dependency is not what makes an addon usable — it is
584
+ // discovered because it is wired, and it can arrive by workspace link or a
585
+ // local addons/ directory without ever appearing here. So a declared but
586
+ // absent package is still an error.
583
587
  const { logger, criticals } = makeCriticalLogger()
584
588
  const dir = writePkg({ '@addon/local': '1.0.0' }, {})
585
589
  try {
590
+ validateRemoteAddonDependencies(
591
+ logger,
592
+ makeRemoteState(dir, { package: '@addon/local' })
593
+ )
594
+ assert.equal(criticals.length, 1)
595
+ assert.equal(criticals[0]!.code, ErrorCode.ADDON_NOT_INSTALLED)
596
+ } finally {
597
+ rmSync(dir, { recursive: true, force: true })
598
+ }
599
+ })
600
+
601
+ test('a non-remote addon present only in node_modules passes', () => {
602
+ // The inverse, and the case that matters: installed by link, absent from
603
+ // package.json. Reported as missing, this would fail every project whose
604
+ // addon lives in a local addons/ directory.
605
+ const { logger, criticals } = makeCriticalLogger()
606
+ const dir = writePkg({}, {})
607
+ try {
608
+ const pkgDir = join(dir, 'node_modules', '@addon', 'local')
609
+ mkdirSync(pkgDir, { recursive: true })
610
+ writeFileSync(
611
+ join(pkgDir, 'package.json'),
612
+ JSON.stringify({ name: '@addon/local', version: '1.0.0' })
613
+ )
614
+ validateRemoteAddonDependencies(
615
+ logger,
616
+ makeRemoteState(dir, { package: '@addon/local' })
617
+ )
618
+ assert.deepEqual(criticals, [])
619
+ } finally {
620
+ rmSync(dir, { recursive: true, force: true })
621
+ }
622
+ })
623
+
624
+ test('an exports map that refuses package.json is not read as missing', () => {
625
+ // `require.resolve('<pkg>/package.json')` throws ERR_PACKAGE_PATH_NOT_EXPORTED
626
+ // on a package with a restrictive exports map. Reaching the exports map
627
+ // means the package was found, so that must not count as absent.
628
+ const { logger, criticals } = makeCriticalLogger()
629
+ const dir = writePkg({}, {})
630
+ try {
631
+ const pkgDir = join(dir, 'node_modules', '@addon', 'local')
632
+ mkdirSync(pkgDir, { recursive: true })
633
+ writeFileSync(
634
+ join(pkgDir, 'package.json'),
635
+ JSON.stringify({
636
+ name: '@addon/local',
637
+ version: '1.0.0',
638
+ main: 'index.js',
639
+ exports: { '.': './index.js' },
640
+ })
641
+ )
642
+ writeFileSync(join(pkgDir, 'index.js'), 'module.exports = {}\n')
586
643
  validateRemoteAddonDependencies(
587
644
  logger,
588
645
  makeRemoteState(dir, { package: '@addon/local' })
@@ -7,19 +7,20 @@ import type {
7
7
  } from '../types.js'
8
8
  import type {
9
9
  FunctionServicesMeta,
10
- MiddlewareMetadata,
11
10
  PermissionMetadata,
12
- } from '@pikku/core/ecosystem/types'
11
+ } from '@pikku/core/function'
12
+ import type { MiddlewareMetadata } from '@pikku/core/middleware'
13
13
  import type ts from 'typescript'
14
14
  import { existsSync, readFileSync } from 'node:fs'
15
+ import { createRequire } from 'node:module'
15
16
  import { join } from 'node:path'
16
17
  import { extractTypeKeys } from './type-utils.js'
17
18
  import { ErrorCode } from '../error-codes.js'
18
19
  import { findSecretAliasServices } from './secret-alias-services.js'
19
20
  import { relative } from 'node:path'
20
21
  import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js'
21
- import { flattenScopeDefinitions } from '@pikku/core/ecosystem/scope'
22
- import type { WorkflowStepMeta } from '@pikku/core/ecosystem/workflow'
22
+ import { flattenScopeDefinitions } from '@pikku/core/scope'
23
+ import type { WorkflowStepMeta } from '@pikku/core/workflow'
23
24
  import { DYNAMIC_SCENARIO_STEP_TARGET } from './workflow/dsl/patterns.js'
24
25
 
25
26
  /**
@@ -480,10 +481,56 @@ export function validateVariableOverrides(
480
481
  }
481
482
 
482
483
  /**
484
+ * Can `pkg` be resolved from `rootDir`, by any means the loader would use?
485
+ *
486
+ * Resolution is attempted against `<pkg>/package.json` first and the bare
487
+ * specifier second, because neither alone is conclusive. A package with a
488
+ * restrictive `exports` map refuses the first with
489
+ * `ERR_PACKAGE_PATH_NOT_EXPORTED` — which is not a failure at all: reaching the
490
+ * exports map means the package was found. Only a module-not-found from both
491
+ * attempts means it is genuinely absent.
492
+ */
493
+ const isPackageResolvable = (pkg: string, rootDir: string): boolean => {
494
+ const NOT_FOUND = new Set(['MODULE_NOT_FOUND', 'ERR_MODULE_NOT_FOUND'])
495
+ let req: ReturnType<typeof createRequire>
496
+ try {
497
+ req = createRequire(join(rootDir, 'package.json'))
498
+ } catch {
499
+ return true // cannot ask; do not accuse
500
+ }
501
+ for (const specifier of [`${pkg}/package.json`, pkg]) {
502
+ try {
503
+ req.resolve(specifier)
504
+ return true
505
+ } catch (e: any) {
506
+ if (!NOT_FOUND.has(e?.code)) return true
507
+ }
508
+ }
509
+ return false
510
+ }
511
+
512
+ /**
513
+ * Every wired addon must be installed, in the bucket matching how it is used.
514
+ *
483
515
  * A `wireRemoteAddon` package ships types only — its handlers run on the host —
484
516
  * so it MUST be a devDependency, not a production dependency (a prod dep would
485
- * drag in the runtime deps remote consumption exists to avoid). This is the
486
- * mirror image of `wireAddon`, which requires a production dependency.
517
+ * drag in the runtime deps remote consumption exists to avoid).
518
+ *
519
+ * A plain `wireAddon` only has to be *resolvable*. The failure it allows is
520
+ * quiet in a way the remote one is not — an absent package makes the metadata
521
+ * load fail, which is caught and downgraded to a warning, so every
522
+ * `ref('<namespace>:…')` resolves against nothing and the surface is dead at
523
+ * runtime. The generated console is the common case: `scaffold.console` emits
524
+ * `wireAddon({ package: '@pikku/addon-console' })` and refs into it, and a
525
+ * project that never installed the package gets a console that 404s with
526
+ * nothing in the build output saying why.
527
+ *
528
+ * Deliberately a resolution check and not a `package.json` one. An addon is
529
+ * discovered because it is *wired*, not because it is a declared dependency —
530
+ * addons arrive by workspace link, by a local `addons/` directory recorded in
531
+ * `pikku-addons.json`, and by plain install, and only the last of those shows
532
+ * up in `dependencies`. Asking the resolver is the only question that matches
533
+ * what the loader will actually do.
487
534
  */
488
535
  export function validateRemoteAddonDependencies(
489
536
  logger: InspectorLogger,
@@ -492,10 +539,19 @@ export function validateRemoteAddonDependencies(
492
539
  const { wireAddonDeclarations } = state.rpc
493
540
  if (!wireAddonDeclarations || wireAddonDeclarations.size === 0) return
494
541
 
495
- const hasRemote = Array.from(wireAddonDeclarations.values()).some(
496
- (d) => d.remote
497
- )
498
- if (!hasRemote) return
542
+ // Local addons first, and independently of the manifest below: their check
543
+ // asks the resolver, not `package.json`, so a project without a manifest (as
544
+ // in some tests) must not skip it.
545
+ for (const [namespace, decl] of wireAddonDeclarations.entries()) {
546
+ if (decl.remote) continue
547
+ if (isPackageResolvable(decl.package, state.rootDir)) continue
548
+ logger.critical(
549
+ ErrorCode.ADDON_NOT_INSTALLED,
550
+ `Addon '${namespace}' ('${decl.package}') is wired with wireAddon but cannot be resolved — every ref('${namespace}:…') will resolve to nothing and the surface will be dead at runtime. Install it, or remove the wireAddon call.`
551
+ )
552
+ }
553
+
554
+ if (!Array.from(wireAddonDeclarations.values()).some((d) => d.remote)) return
499
555
 
500
556
  const pkgJsonPath = join(state.rootDir, 'package.json')
501
557
  if (!existsSync(pkgJsonPath)) return // no manifest to check (e.g. some tests)
@@ -724,10 +780,7 @@ export function computeRequiredSchemas(
724
780
  * the functions that are actually invoked, so this blocks CI through
725
781
  * `--fail-on-error` without failing a dev server over a function nobody calls.
726
782
  */
727
- export function validateSchemaReferences(
728
- logger: InspectorLogger,
729
- state: InspectorState
730
- ): void {
783
+ export function unresolvedSchemaReferences(state: InspectorState): string[] {
731
784
  const { functions, schemas } = state
732
785
  const available = new Set(Object.keys(schemas ?? {}))
733
786
  const unresolved: string[] = []
@@ -752,6 +805,15 @@ export function validateSchemaReferences(
752
805
  }
753
806
  }
754
807
 
808
+ return unresolved
809
+ }
810
+
811
+ export function validateSchemaReferences(
812
+ logger: InspectorLogger,
813
+ state: InspectorState
814
+ ): void {
815
+ const unresolved = unresolvedSchemaReferences(state)
816
+
755
817
  if (unresolved.length === 0) return
756
818
 
757
819
  logger.diagnostic({
@@ -842,10 +904,11 @@ export function validateScenarioServices(
842
904
  *
843
905
  * - the step target must be a static string literal (PKU678) — otherwise it
844
906
  * can't be bundled, typed or drawn;
845
- * - a step declaring a `browser` binding must be given an actor (PKU677), since
846
- * the browser context signs in as that persona. It is checked statically even
847
- * though the binding only runs on a browser run: a step that can never be
848
- * driven by a human is a latent failure, not a passing default run.
907
+ * - a step that runs as a persona must be given one (PKU677) — declared by
908
+ * `actor: true`, or implied by a `browser` binding, since the browser context
909
+ * signs in as that persona. It is checked statically even though a browser
910
+ * binding only runs on a browser run: a step that can never be driven by a
911
+ * human is a latent failure, not a passing default run.
849
912
  * - a scenario written as a step ladder must assert something (PKU680).
850
913
  *
851
914
  * It also marks the step's function as used, so `scenario run` boots the
@@ -872,14 +935,14 @@ export function validateScenarioSteps(
872
935
  }
873
936
  state.serviceAggregation.usedFunctions.add(step.stepFunc)
874
937
  const stepMeta = state.functions.meta[step.stepFunc]
875
- if (
876
- stepMeta?.scenarioStepSurfaces?.includes('browser') &&
877
- !step.actor
878
- ) {
938
+ if (stepMeta?.scenarioStepRequiresActor && !step.actor) {
939
+ const why = stepMeta.scenarioStepSurfaces?.includes('browser')
940
+ ? 'declares a browser binding'
941
+ : 'is driven by a persona'
879
942
  logger.critical(
880
- ErrorCode.SCENARIO_BROWSER_STEP_NEEDS_ACTOR,
881
- `Scenario '${workflowName}' calls step '${step.stepFunc}', which declares a browser binding, without an actor. ` +
882
- `Pass { actor: actors.<name> } so the browser context signs in as that persona.`
943
+ ErrorCode.SCENARIO_STEP_NEEDS_ACTOR,
944
+ `Scenario '${workflowName}' calls step '${step.stepFunc}', which ${why}, without an actor. ` +
945
+ `Pass { actor: actors.<name> } so it runs as that persona.`
883
946
  )
884
947
  }
885
948
  continue
@@ -1,4 +1,4 @@
1
- import type { FunctionMeta } from '@pikku/core/ecosystem/services'
1
+ import type { FunctionMeta } from '@pikku/core/services'
2
2
 
3
3
  /**
4
4
  * Thrown when a function's explicit `deploy: 'serverless'` conflicts
@@ -1,7 +1,4 @@
1
- import type {
2
- FunctionMeta,
3
- FunctionsMeta,
4
- } from '@pikku/core/ecosystem/services'
1
+ import type { FunctionMeta, FunctionsMeta } from '@pikku/core/services'
5
2
 
6
3
  /**
7
4
  * Look up function metadata by pikkuFuncId, checking both local functions
@@ -1,5 +1,5 @@
1
1
  import type { TypesMap } from '../types-map.js'
2
- import type { FunctionsMeta } from '@pikku/core/ecosystem/services'
2
+ import type { FunctionsMeta } from '@pikku/core/services'
3
3
 
4
4
  export function resolveFunctionIOTypes(
5
5
  pikkuFuncId: string,
@@ -1,8 +1,5 @@
1
- import {
2
- parseVersionedId,
3
- formatVersionedId,
4
- } from '@pikku/core/ecosystem/types'
5
- import type { CLICommandMeta } from '@pikku/core/ecosystem/cli'
1
+ import { parseVersionedId, formatVersionedId } from '@pikku/core/utils'
2
+ import type { CLICommandMeta } from '@pikku/core/cli'
6
3
  import type { InspectorState, InspectorLogger } from '../types.js'
7
4
  import { ErrorCode } from '../error-codes.js'
8
5
 
@@ -12,9 +12,9 @@ import { createGenerator, RootlessError } from 'ts-json-schema-generator'
12
12
  import { register } from 'tsx/esm/api'
13
13
  import * as z from 'zod'
14
14
  import { zodToTs, createAuxiliaryTypeStore } from 'zod-to-ts'
15
- import type { FunctionsMeta } from '@pikku/core/ecosystem/services'
16
- import type { JSONValue } from '@pikku/core/ecosystem/types'
17
- import type { HTTPWiringsMeta } from '@pikku/core/ecosystem/http'
15
+ import type { FunctionsMeta } from '@pikku/core/services'
16
+ import type { JSONValue } from '@pikku/core/utils'
17
+ import type { HTTPWiringsMeta } from '@pikku/core/http'
18
18
  import type { TypesMap } from '../types-map.js'
19
19
  import { ErrorCode } from '../error-codes.js'
20
20
  import type { InspectorLogger, InspectorState, SchemaRef } from '../types.js'
@@ -1,5 +1,5 @@
1
1
  import type { SecretUsage } from './extract-secret-usage.js'
2
- import type { JSONValue } from '@pikku/core/ecosystem/types'
2
+ import type { JSONValue } from '@pikku/core/utils'
3
3
  import type {
4
4
  InspectorDiagnostic,
5
5
  InspectorFeature,
@@ -212,6 +212,7 @@ export interface SerializableInspectorState {
212
212
  files: string[]
213
213
  usage: Record<string, SecretUsage>
214
214
  }
215
+ surfaceUsage: InspectorState['surfaceUsage']
215
216
  credentials: {
216
217
  definitions: InspectorState['credentials']['definitions']
217
218
  files: string[]
@@ -440,6 +441,7 @@ export function serializeInspectorState(
440
441
  files: Array.from(state.secrets.files),
441
442
  usage: Object.fromEntries(state.secrets.usage),
442
443
  },
444
+ surfaceUsage: state.surfaceUsage,
443
445
  credentials: {
444
446
  definitions: state.credentials.definitions,
445
447
  files: Array.from(state.credentials.files),
@@ -648,6 +650,7 @@ export function deserializeInspectorState(
648
650
  files: new Set(data.secrets?.files || []),
649
651
  usage: new Map(Object.entries(data.secrets?.usage || {})),
650
652
  },
653
+ surfaceUsage: data.surfaceUsage || {},
651
654
  credentials: {
652
655
  definitions: data.credentials?.definitions || [],
653
656
  files: new Set(data.credentials?.files || []),
@@ -1,5 +1,5 @@
1
1
  import type { InspectorLogger, InspectorState } from '../types.js'
2
- import type { JSONValue } from '@pikku/core/ecosystem/types'
2
+ import type { JSONValue } from '@pikku/core/utils'
3
3
  import { resolveFunctionMeta } from './resolve-function-meta.js'
4
4
 
5
5
  interface MCPEndpoint {
@@ -1,5 +1,5 @@
1
- import type { FunctionsMeta } from '@pikku/core/ecosystem/services'
2
- import type { HTTPWiringsMeta } from '@pikku/core/ecosystem/http'
1
+ import type { FunctionsMeta } from '@pikku/core/services'
2
+ import type { HTTPWiringsMeta } from '@pikku/core/http'
3
3
  import type { InspectorLogger } from '../types.js'
4
4
  import _convertSchema from '@openapi-contrib/json-schema-to-openapi-schema'
5
5
  const convertSchema =
@@ -2,6 +2,7 @@ import type * as ts from 'typescript'
2
2
  import { getPropertyValue } from './get-property-value.js'
3
3
  import { ErrorCode } from '../error-codes.js'
4
4
  import type { InspectorLogger, InspectorState } from '../types.js'
5
+ import { resolveFunctionMeta } from './resolve-function-meta.js'
5
6
 
6
7
  export function validateAuthSessionless(
7
8
  logger: InspectorLogger,
@@ -11,7 +12,7 @@ export function validateAuthSessionless(
11
12
  wireDescription: string,
12
13
  inheritedAuth?: boolean
13
14
  ): boolean {
14
- const fnMeta = state.functions.meta[funcName]
15
+ const fnMeta = resolveFunctionMeta(state, funcName)
15
16
  if (!fnMeta) return true
16
17
 
17
18
  const routeAuth = getPropertyValue(obj, 'auth')
@@ -59,7 +59,10 @@ describe('validateTagsResolveToMiddleware', () => {
59
59
  })
60
60
 
61
61
  test('stays quiet when http middleware is registered for the tag', () => {
62
- assert.deepEqual(run(stateWith({ console: { tags: ['admin'] } }, ['admin'])), [])
62
+ assert.deepEqual(
63
+ run(stateWith({ console: { tags: ['admin'] } }, ['admin'])),
64
+ []
65
+ )
63
66
  })
64
67
 
65
68
  test('stays quiet when only channel middleware is registered for the tag', () => {
@@ -1,6 +1,6 @@
1
1
  import { strict as assert } from 'assert'
2
2
  import { describe, test } from 'node:test'
3
- import type { WorkflowStepMeta } from '@pikku/core/ecosystem/workflow'
3
+ import type { WorkflowStepMeta } from '@pikku/core/workflow'
4
4
  import { deriveWorkflowPlan } from './derive-workflow-plan.js'
5
5
 
6
6
  const rpc = (stepName: string, rpcName = 'rpc.fn'): WorkflowStepMeta =>
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  WorkflowStepMeta,
3
3
  WorkflowPlannedStep,
4
- } from '@pikku/core/ecosystem/workflow'
4
+ } from '@pikku/core/workflow'
5
5
 
6
6
  /**
7
7
  * Derive the static UI plan for a DSL workflow from its extracted step tree.
@@ -2,7 +2,7 @@ import { describe, test } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
  import * as ts from 'typescript'
4
4
  import { extractDSLWorkflow } from './extract-dsl-workflow.js'
5
- import type { ApprovalStepMeta } from '@pikku/core/ecosystem/workflow'
5
+ import type { ApprovalStepMeta } from '@pikku/core/workflow'
6
6
 
7
7
  const checker = {
8
8
  getSymbolAtLocation: () => undefined,
@@ -20,8 +20,8 @@ import type {
20
20
  Condition,
21
21
  WorkflowContext,
22
22
  ContextVariable,
23
- } from '@pikku/core/ecosystem/workflow'
24
- import type { ScenarioStepMeta } from '@pikku/core/ecosystem/scenario'
23
+ } from '@pikku/core/workflow'
24
+ import type { ScenarioStepMeta } from '@pikku/core/scenario'
25
25
  import {
26
26
  isWorkflowDoCall,
27
27
  isWorkflowExpectEventuallyCall,
@@ -1,7 +1,7 @@
1
1
  import { test } from 'node:test'
2
2
  import assert from 'node:assert'
3
3
  import { convertDslToGraph } from './convert-dsl-to-graph.js'
4
- import type { WorkflowsMeta } from '@pikku/core/ecosystem/workflow'
4
+ import type { WorkflowsMeta } from '@pikku/core/workflow'
5
5
 
6
6
  const scenarioMeta = (
7
7
  overrides: Partial<WorkflowsMeta[string]> = {}
@@ -1,10 +1,7 @@
1
1
  /**
2
2
  * Converts DSL (Domain Specific Language) step-based format to graph node format
3
3
  */
4
- import type {
5
- WorkflowStepMeta,
6
- WorkflowsMeta,
7
- } from '@pikku/core/ecosystem/workflow'
4
+ import type { WorkflowStepMeta, WorkflowsMeta } from '@pikku/core/workflow'
8
5
  import type {
9
6
  SerializedGraphNode,
10
7
  SerializedWorkflowGraph,
@@ -3,7 +3,7 @@ import type {
3
3
  SerializedWorkflowGraph,
4
4
  WorkflowWires,
5
5
  } from './workflow-graph.types.js'
6
- import type { CLICommandMeta } from '@pikku/core/ecosystem/cli'
6
+ import type { CLICommandMeta } from '@pikku/core/cli'
7
7
 
8
8
  function parseWorkflowFuncId(
9
9
  pikkuFuncId: string
@@ -1,9 +1,9 @@
1
- import type { FunctionsMeta } from '@pikku/core/ecosystem/services'
1
+ import type { FunctionsMeta } from '@pikku/core/services'
2
2
  import {
3
3
  isVersionedId,
4
4
  formatVersionedId,
5
5
  parseVersionedId,
6
- } from '@pikku/core/ecosystem/types'
6
+ } from '@pikku/core/utils'
7
7
  import type { SerializedWorkflowGraph } from './workflow-graph.types.js'
8
8
  import { canonicalJSON, hashString } from '../../hash.js'
9
9
  import { convertDslToGraph } from './convert-dsl-to-graph.js'
@@ -1,7 +1,24 @@
1
1
  /**
2
2
  * Workflow graph serialization exports
3
3
  */
4
- export * from './workflow-graph.types.js'
4
+ export {
5
+ isDataRef,
6
+ isFunctionNode,
7
+ ref,
8
+ state,
9
+ } from './workflow-graph.types.js'
10
+ export type {
11
+ BranchCondition,
12
+ ContextVariable,
13
+ DataRef,
14
+ FlowNode,
15
+ ForEachMode,
16
+ FunctionNode,
17
+ NodeOptions,
18
+ SerializedWorkflowGraph,
19
+ SerializedWorkflowGraphs,
20
+ WorkflowContext,
21
+ } from './workflow-graph.types.js'
5
22
  export { serializeWorkflowGraph } from './serialize-workflow-graph.js'
6
23
  export { convertDslToGraph } from './convert-dsl-to-graph.js'
7
24
  export { finalizeWorkflows } from './finalize-workflows.js'