@pikku/inspector 0.12.47 → 0.12.49

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 (58) hide show
  1. package/CHANGELOG.md +218 -0
  2. package/LICENSE +106 -0
  3. package/README.md +28 -2
  4. package/dist/add/add-feature.d.ts +19 -0
  5. package/dist/add/add-feature.js +200 -0
  6. package/dist/add/add-functions.js +38 -9
  7. package/dist/add/add-workflow.js +22 -29
  8. package/dist/add/scenario-instrumentation.d.ts +24 -0
  9. package/dist/add/scenario-instrumentation.js +29 -0
  10. package/dist/error-codes.d.ts +3 -0
  11. package/dist/error-codes.js +3 -0
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.js +1 -0
  14. package/dist/inspector.js +39 -2
  15. package/dist/types.d.ts +43 -0
  16. package/dist/utils/extract-function-name.js +27 -0
  17. package/dist/utils/post-process.d.ts +26 -0
  18. package/dist/utils/post-process.js +92 -0
  19. package/dist/utils/serialize-inspector-state.d.ts +2 -1
  20. package/dist/utils/serialize-inspector-state.js +2 -0
  21. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +203 -19
  22. package/dist/utils/workflow/dsl/patterns.d.ts +21 -0
  23. package/dist/utils/workflow/dsl/patterns.js +30 -0
  24. package/dist/utils/workflow/dsl/validation.d.ts +5 -0
  25. package/dist/utils/workflow/dsl/validation.js +9 -0
  26. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +36 -0
  27. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +5 -1
  28. package/dist/visit.js +2 -0
  29. package/package.json +4 -3
  30. package/src/add/add-feature.test.ts +306 -0
  31. package/src/add/add-feature.ts +253 -0
  32. package/src/add/add-functions.ts +47 -9
  33. package/src/add/add-scenario-step.test.ts +311 -0
  34. package/src/add/add-workflow-silent-drops.test.ts +98 -0
  35. package/src/add/add-workflow.ts +25 -37
  36. package/src/add/scenario-instrumentation.test.ts +89 -0
  37. package/src/add/scenario-instrumentation.ts +36 -0
  38. package/src/add/scenario-marker.test.ts +89 -0
  39. package/src/add/scenario-not-extractable.test.ts +159 -0
  40. package/src/add/scenario-services.test.ts +103 -0
  41. package/src/add/scenario-skip.test.ts +83 -0
  42. package/src/error-codes.ts +3 -0
  43. package/src/index.ts +6 -0
  44. package/src/inspector.ts +50 -1
  45. package/src/tsconfig-paths.test.ts +90 -0
  46. package/src/types.ts +45 -0
  47. package/src/utils/extract-function-name.ts +36 -0
  48. package/src/utils/post-process.test.ts +30 -1
  49. package/src/utils/post-process.ts +103 -0
  50. package/src/utils/serialize-inspector-state.ts +8 -1
  51. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +236 -22
  52. package/src/utils/workflow/dsl/patterns.ts +38 -0
  53. package/src/utils/workflow/dsl/validation.ts +12 -0
  54. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +35 -0
  55. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +36 -0
  56. package/src/utils/workflow/graph/workflow-graph.types.ts +5 -0
  57. package/src/visit.ts +2 -0
  58. package/tsconfig.tsbuildinfo +1 -1
package/src/types.ts CHANGED
@@ -273,6 +273,12 @@ export type InspectorOptions = Partial<{
273
273
  rootDir: string
274
274
  isAddon: boolean
275
275
  sourceFile: ts.SourceFile
276
+ /**
277
+ * The project's tsconfig, read for its path mappings only. The inspector's
278
+ * program needs them to resolve imports the project reaches through `paths`;
279
+ * unlike `schemaConfig.tsconfig` this is also needed on a setup-only run.
280
+ */
281
+ tsconfig: string
276
282
  types: Partial<{
277
283
  configFileType: string
278
284
  userSessionType: string
@@ -423,6 +429,36 @@ export interface AuthDefinition {
423
429
  services: FunctionServicesMeta
424
430
  }
425
431
 
432
+ /** One statically resolved entry of a feature's `scenarios` array. */
433
+ export interface InspectorFeatureEntry {
434
+ /** The scenario's declared export name, with any import alias followed back. */
435
+ scenario: string
436
+ /** A `{ scenario, data }` entry's literal data — gherkin's `Examples:`. */
437
+ data?: unknown
438
+ }
439
+
440
+ /** A `pikkuFeature` export, as far as it can be read from the source. */
441
+ export interface InspectorFeature {
442
+ path: string
443
+ exportedName: string
444
+ name?: string
445
+ description?: string
446
+ tags?: string[]
447
+ entries: InspectorFeatureEntry[]
448
+ /**
449
+ * Entries that are not literal — a spread, a `.map()`, a call. Counted rather
450
+ * than dropped so the console can say the listing is partial instead of
451
+ * quietly showing a short feature.
452
+ */
453
+ unresolvedEntries: number
454
+ /**
455
+ * Feature hooks run once around the whole group, never per scenario, and are
456
+ * runtime-only — so only their presence is recorded.
457
+ */
458
+ hasBefore: boolean
459
+ hasAfter: boolean
460
+ }
461
+
426
462
  export interface InspectorState {
427
463
  rootDir: string // Root directory inferred from source files
428
464
  singletonServicesTypeImportMap: PathToNameAndType
@@ -466,6 +502,15 @@ export interface InspectorState {
466
502
  files: Map<string, { path: string; exportedName: string }>
467
503
  graphMeta: SerializedWorkflowGraphs
468
504
  graphFiles: Map<string, { path: string; exportedName: string }>
505
+ /**
506
+ * `pikkuFeature` exports, keyed by export identifier.
507
+ *
508
+ * Everything the config states literally is extracted here, because a
509
+ * feature is the document structure the console renders. Entries built by
510
+ * an ordinary loop cannot be enumerated statically — those are counted in
511
+ * `unresolvedEntries` and resolved at runtime by object identity instead.
512
+ */
513
+ featureFiles: Map<string, InspectorFeature>
469
514
  invokedWorkflows: Set<string>
470
515
  }
471
516
  rpc: {
@@ -45,6 +45,40 @@ export function funcIdToTypeName(id: string): string {
45
45
  .join('')
46
46
  }
47
47
 
48
+ /**
49
+ * `pikkuScenarioStep({ name })` is the one wrapper whose `name` is its identity:
50
+ * scenarios reference a step by that string, so it must become the pikkuFuncId
51
+ * (and hence the generated step map's key), not just a cosmetic label the way
52
+ * `pikkuMiddleware`/`pikkuAgent` use it.
53
+ */
54
+ function extractScenarioStepName(
55
+ callExpr: ts.Node,
56
+ result: ExtractedFunctionName
57
+ ): void {
58
+ if (
59
+ !ts.isCallExpression(callExpr) ||
60
+ !ts.isIdentifier(callExpr.expression) ||
61
+ callExpr.expression.text !== 'pikkuScenarioStep'
62
+ ) {
63
+ return
64
+ }
65
+ const firstArg = callExpr.arguments[0]
66
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) {
67
+ return
68
+ }
69
+ for (const prop of firstArg.properties) {
70
+ if (
71
+ ts.isPropertyAssignment(prop) &&
72
+ ts.isIdentifier(prop.name) &&
73
+ prop.name.text === 'name' &&
74
+ ts.isStringLiteral(prop.initializer)
75
+ ) {
76
+ result.explicitName = prop.initializer.text
77
+ return
78
+ }
79
+ }
80
+ }
81
+
48
82
  export function extractFunctionName(
49
83
  callExpr: ts.Node,
50
84
  checker: ts.TypeChecker,
@@ -63,6 +97,8 @@ export function extractFunctionName(
63
97
  version: null,
64
98
  }
65
99
 
100
+ extractScenarioStepName(callExpr, result)
101
+
66
102
  const workflowHelpers = new Set([
67
103
  'workflow',
68
104
  'workflowStart',
@@ -64,6 +64,7 @@ function makeState(
64
64
  addonFunctions?: Record<string, Record<string, any>>
65
65
  addonRequiredParentServices?: string[]
66
66
  authServices?: string[]
67
+ graphMeta?: Record<string, any>
67
68
  } = {}
68
69
  ): Omit<InspectorState, 'typesLookup'> {
69
70
  return {
@@ -95,7 +96,7 @@ function makeState(
95
96
  scheduledTasks: { meta: {} },
96
97
  mcpEndpoints: { toolsMeta: {}, promptsMeta: {}, resourcesMeta: {} },
97
98
  agents: { agentsMeta: {} },
98
- workflows: { meta: {}, graphMeta: {} },
99
+ workflows: { meta: {}, graphMeta: overrides.graphMeta ?? {} },
99
100
  wireServicesMeta: new Map(),
100
101
  rpc: { internalMeta: {}, exposedMeta: {} },
101
102
  addonFunctions: overrides.addonFunctions ?? {},
@@ -117,6 +118,34 @@ const CONSOLE_PARENT_SERVICES = [
117
118
  'credentialService',
118
119
  ]
119
120
 
121
+ describe('aggregateRequiredServices — synthetic workflow queue workers', () => {
122
+ const graphMeta = {
123
+ orderWorkflow: {
124
+ name: 'orderWorkflow',
125
+ source: 'dsl',
126
+ nodes: { 'step-1': { rpcName: 'chargeCard' } },
127
+ },
128
+ loginScenario: {
129
+ name: 'loginScenario',
130
+ source: 'scenario',
131
+ nodes: { 'step-1': { rpcName: 'opensPage' } },
132
+ },
133
+ }
134
+
135
+ // A scenario IS a workflow, so it used to be handed an orchestrator queue
136
+ // worker in the app's queue meta — which the app bootstrap registers and every
137
+ // deployed bundle therefore carries, leaving a provider to create a production
138
+ // queue named after a test. Nothing dispatches a scenario through a queue:
139
+ // `pikku scenario run` executes its steps in-process.
140
+ test('a scenario gets no orchestrator queue worker', () => {
141
+ const state = makeState({ graphMeta })
142
+ aggregateRequiredServices(state)
143
+ assert.deepEqual(Object.keys(state.queueWorkers.meta), [
144
+ 'wf-orchestrator-order-workflow',
145
+ ])
146
+ })
147
+ })
148
+
120
149
  describe('aggregateRequiredServices — per-function addon services', () => {
121
150
  test('a used addon function adds only its own parent services', () => {
122
151
  const state = makeState({
@@ -16,6 +16,8 @@ import { extractTypeKeys } from './type-utils.js'
16
16
  import { ErrorCode } from '../error-codes.js'
17
17
  import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js'
18
18
  import { flattenScopeDefinitions } from '@pikku/core/scope'
19
+ import type { WorkflowStepMeta } from '@pikku/core/workflow'
20
+ import { DYNAMIC_SCENARIO_STEP_TARGET } from './workflow/dsl/patterns.js'
19
21
 
20
22
  /**
21
23
  * Stamp the inspected authorize/callbacks service set onto the generated auth
@@ -260,6 +262,12 @@ export function aggregateRequiredServices(
260
262
  // per-workflow queue entries and falls back to shared queue names.
261
263
  for (const [, graph] of Object.entries(state.workflows.graphMeta)) {
262
264
  if (!graph.nodes || !graph.name) continue
265
+ // A scenario is a workflow, but it only ever runs in-process under
266
+ // `pikku scenario run` — no step of one is dispatched through a queue. The
267
+ // synthetic entries were pure leakage: they put a
268
+ // `wf-orchestrator-<scenario>` worker into the app's queue meta, which every
269
+ // bundle imports and a provider then creates as a real production queue.
270
+ if (graph.source === 'scenario') continue
263
271
 
264
272
  const toKebab = (s: string) =>
265
273
  s
@@ -696,6 +704,101 @@ export function validateAgentModels(
696
704
  }
697
705
  }
698
706
 
707
+ /**
708
+ * Scenarios are pure stories of remote RPCs (same rule as client-side CLI
709
+ * renderers): the func may only destructure logger/config — everything else
710
+ * must go through actor steps so the flow runs against the TARGET
711
+ * environment, never local services.
712
+ *
713
+ * This runs in post-processing rather than in `addWorkflow` because
714
+ * `addWorkflow` runs in the `visitSetup` pass, which completes before
715
+ * `addFunctions` (`visitFunctions`) has populated `state.functions.meta` — so
716
+ * the check read `undefined` there and never fired.
717
+ */
718
+ export function validateScenarioServices(
719
+ logger: InspectorLogger,
720
+ state: InspectorState | Omit<InspectorState, 'typesLookup'>
721
+ ): void {
722
+ for (const [workflowName, meta] of Object.entries(state.workflows.meta)) {
723
+ if (!meta.scenario) continue
724
+ const funcMeta = state.functions.meta[meta.pikkuFuncId]
725
+ if (!funcMeta?.services) continue
726
+ const disallowed = funcMeta.services.services.filter(
727
+ (svc) => svc !== 'logger' && svc !== 'config'
728
+ )
729
+ if (disallowed.length > 0) {
730
+ logger.critical(
731
+ ErrorCode.SCENARIO_HAS_SERVICES,
732
+ `Scenario '${workflowName}' destructures services: ${disallowed.join(', ')}. ` +
733
+ `Scenarios may only use 'logger'/'config' — drive everything else through ` +
734
+ `actor steps (workflow.do(step, rpc, data, { actor: actors.x })) so the flow ` +
735
+ `runs against the target environment.`
736
+ )
737
+ }
738
+ }
739
+ }
740
+
741
+ /**
742
+ * Walk every scenario step, wherever it is nested, and validate the two things
743
+ * that can only be known once both the workflow meta and the function meta
744
+ * exist:
745
+ *
746
+ * - the step target must be a static string literal (PKU678) — otherwise it
747
+ * can't be bundled, typed or drawn;
748
+ * - a `browser: true` step must be given an actor (PKU677), since the browser
749
+ * context signs in as that persona.
750
+ *
751
+ * It also marks the step's function as used, so `scenario run` boots the
752
+ * singletons the step actually needs.
753
+ */
754
+ export function validateScenarioSteps(
755
+ logger: InspectorLogger,
756
+ state: InspectorState | Omit<InspectorState, 'typesLookup'>
757
+ ): void {
758
+ const visit = (workflowName: string, steps: WorkflowStepMeta[]): void => {
759
+ for (const step of steps) {
760
+ if (step.type === 'scenarioStep') {
761
+ if (step.stepFunc === DYNAMIC_SCENARIO_STEP_TARGET) {
762
+ logger.critical(
763
+ ErrorCode.SCENARIO_STEP_TARGET_NOT_STATIC,
764
+ `Scenario '${workflowName}' calls step '${step.stepName}' with a target that isn't a string literal. ` +
765
+ `A step target must be statically resolvable so it can be bundled, typed and drawn — ` +
766
+ `loop over data sets instead of computing the step name.`
767
+ )
768
+ continue
769
+ }
770
+ state.serviceAggregation.usedFunctions.add(step.stepFunc)
771
+ const stepMeta = state.functions.meta[step.stepFunc]
772
+ if (stepMeta?.scenarioStepBrowser && !step.actor) {
773
+ logger.critical(
774
+ ErrorCode.SCENARIO_BROWSER_STEP_NEEDS_ACTOR,
775
+ `Scenario '${workflowName}' calls browser step '${step.stepFunc}' without an actor. ` +
776
+ `Pass { actor: actors.<name> } so the browser context signs in as that persona.`
777
+ )
778
+ }
779
+ continue
780
+ }
781
+ if (step.type === 'branch') {
782
+ for (const branch of step.branches) visit(workflowName, branch.steps)
783
+ if (step.elseSteps) visit(workflowName, step.elseSteps)
784
+ } else if (step.type === 'switch') {
785
+ for (const c of step.cases ?? []) {
786
+ if (c.steps) visit(workflowName, c.steps)
787
+ }
788
+ if (step.defaultSteps) visit(workflowName, step.defaultSteps)
789
+ } else if (step.type === 'fanout' && step.body) {
790
+ visit(workflowName, step.body as WorkflowStepMeta[])
791
+ } else if (step.type === 'parallel' && step.children) {
792
+ visit(workflowName, step.children as WorkflowStepMeta[])
793
+ }
794
+ }
795
+ }
796
+
797
+ for (const [workflowName, meta] of Object.entries(state.workflows.meta)) {
798
+ visit(workflowName, meta.steps ?? [])
799
+ }
800
+ }
801
+
699
802
  /**
700
803
  * A `pikkuWorkflowGraph` node that references the `graph:` namespace (e.g.
701
804
  * `graph:editFields`) needs @pikku/addon-graph wired — otherwise the RPC never
@@ -1,5 +1,9 @@
1
1
  import type { JSONValue } from '@pikku/core'
2
- import type { InspectorDiagnostic, InspectorState } from '../types.js'
2
+ import type {
3
+ InspectorDiagnostic,
4
+ InspectorFeature,
5
+ InspectorState,
6
+ } from '../types.js'
3
7
  import { TypesMap } from '../types-map.js'
4
8
 
5
9
  /**
@@ -139,6 +143,7 @@ export interface SerializableInspectorState {
139
143
  files: Array<[string, { path: string; exportedName: string }]>
140
144
  graphMeta: InspectorState['workflows']['graphMeta']
141
145
  graphFiles: Array<[string, { path: string; exportedName: string }]>
146
+ featureFiles: Array<[string, InspectorFeature]>
142
147
  invokedWorkflows: string[]
143
148
  }
144
149
  rpc: {
@@ -362,6 +367,7 @@ export function serializeInspectorState(
362
367
  files: Array.from(state.workflows.files.entries()),
363
368
  graphMeta: state.workflows.graphMeta,
364
369
  graphFiles: Array.from(state.workflows.graphFiles.entries()),
370
+ featureFiles: Array.from(state.workflows.featureFiles.entries()),
365
371
  invokedWorkflows: Array.from(state.workflows.invokedWorkflows),
366
372
  },
367
373
  rpc: {
@@ -558,6 +564,7 @@ export function deserializeInspectorState(
558
564
  files: new Map(data.workflows.files),
559
565
  graphMeta: data.workflows.graphMeta || {},
560
566
  graphFiles: new Map(data.workflows.graphFiles || []),
567
+ featureFiles: new Map(data.workflows.featureFiles || []),
561
568
  invokedWorkflows: new Set(data.workflows.invokedWorkflows || []),
562
569
  },
563
570
  rpc: {