@pikku/inspector 0.12.75 → 0.12.82

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 (223) hide show
  1. package/CHANGELOG.md +357 -0
  2. package/dist/add/add-analytics.d.ts +9 -0
  3. package/dist/add/add-analytics.js +178 -0
  4. package/dist/add/add-auth.js +0 -54
  5. package/dist/add/add-feature-flag.d.ts +2 -0
  6. package/dist/add/add-feature-flag.js +162 -0
  7. package/dist/add/add-middleware.js +66 -42
  8. package/dist/add/add-wire-addon.js +7 -4
  9. package/dist/add/add-wire-remote-addon.js +1 -0
  10. package/dist/error-codes.d.ts +1 -0
  11. package/dist/error-codes.js +1 -0
  12. package/dist/index.d.ts +3 -1
  13. package/dist/index.js +3 -1
  14. package/dist/inspector.js +5 -0
  15. package/dist/surface/collect-surface.d.ts +58 -0
  16. package/dist/surface/collect-surface.js +613 -0
  17. package/dist/types.d.ts +36 -12
  18. package/dist/utils/filter-inspector-state.js +14 -0
  19. package/dist/utils/load-addon-functions-meta.js +24 -11
  20. package/dist/utils/post-process.js +15 -0
  21. package/dist/utils/read-module-specifiers.d.ts +8 -0
  22. package/dist/utils/read-module-specifiers.js +23 -0
  23. package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
  24. package/dist/utils/read-tsconfig-out-dir.js +12 -0
  25. package/dist/utils/resolve-deploy-target.d.ts +7 -0
  26. package/dist/utils/resolve-deploy-target.js +15 -7
  27. package/dist/utils/serialize-inspector-state.d.ts +6 -0
  28. package/dist/utils/serialize-inspector-state.js +10 -0
  29. package/dist/visit.js +4 -0
  30. package/package.json +16 -6
  31. package/run-tests.sh +21 -6
  32. package/src/add/add-addon-bans.ts +0 -85
  33. package/src/add/add-agent-scorer.test.ts +0 -197
  34. package/src/add/add-agent-scorer.ts +0 -83
  35. package/src/add/add-agent.test.ts +0 -87
  36. package/src/add/add-agent.ts +0 -638
  37. package/src/add/add-approval-description.ts +0 -76
  38. package/src/add/add-auth.test.ts +0 -791
  39. package/src/add/add-auth.ts +0 -424
  40. package/src/add/add-channel.ts +0 -778
  41. package/src/add/add-cli-renderers.test.ts +0 -74
  42. package/src/add/add-cli.ts +0 -1006
  43. package/src/add/add-credential.ts +0 -186
  44. package/src/add/add-feature.test.ts +0 -306
  45. package/src/add/add-feature.ts +0 -253
  46. package/src/add/add-file-extends-core-type.ts +0 -66
  47. package/src/add/add-file-with-config.ts +0 -45
  48. package/src/add/add-file-with-factory.test.ts +0 -134
  49. package/src/add/add-file-with-factory.ts +0 -288
  50. package/src/add/add-functions.test.ts +0 -743
  51. package/src/add/add-functions.ts +0 -1548
  52. package/src/add/add-gateway.ts +0 -143
  53. package/src/add/add-http-route-ref.test.ts +0 -149
  54. package/src/add/add-http-route.ts +0 -530
  55. package/src/add/add-http-routes.ts +0 -471
  56. package/src/add/add-keyed-wiring.ts +0 -180
  57. package/src/add/add-mcp-prompt.ts +0 -157
  58. package/src/add/add-mcp-resource.ts +0 -175
  59. package/src/add/add-middleware.test.ts +0 -196
  60. package/src/add/add-middleware.ts +0 -879
  61. package/src/add/add-permission-auth.test.ts +0 -59
  62. package/src/add/add-permission.ts +0 -311
  63. package/src/add/add-personas.test.ts +0 -406
  64. package/src/add/add-personas.ts +0 -287
  65. package/src/add/add-queue-worker.ts +0 -117
  66. package/src/add/add-rpc-invocations.test.ts +0 -287
  67. package/src/add/add-rpc-invocations.ts +0 -221
  68. package/src/add/add-scenario-step.test.ts +0 -509
  69. package/src/add/add-schedule.ts +0 -120
  70. package/src/add/add-scope.test.ts +0 -419
  71. package/src/add/add-scope.ts +0 -221
  72. package/src/add/add-secret.ts +0 -10
  73. package/src/add/add-subject-scenario-steps.test.ts +0 -161
  74. package/src/add/add-system-role.test.ts +0 -297
  75. package/src/add/add-system-role.ts +0 -162
  76. package/src/add/add-trigger.ts +0 -165
  77. package/src/add/add-variable.ts +0 -10
  78. package/src/add/add-wire-addon.test.ts +0 -185
  79. package/src/add/add-wire-addon.ts +0 -146
  80. package/src/add/add-wire-remote-addon.ts +0 -80
  81. package/src/add/add-workflow-edge-cases.test.ts +0 -201
  82. package/src/add/add-workflow-fanout.test.ts +0 -166
  83. package/src/add/add-workflow-graph-foreach.test.ts +0 -174
  84. package/src/add/add-workflow-graph-input.test.ts +0 -94
  85. package/src/add/add-workflow-graph-notes.test.ts +0 -74
  86. package/src/add/add-workflow-graph.ts +0 -644
  87. package/src/add/add-workflow-on-error.test.ts +0 -118
  88. package/src/add/add-workflow-return-switch.test.ts +0 -121
  89. package/src/add/add-workflow-silent-drops.test.ts +0 -261
  90. package/src/add/add-workflow-sleep-expression.test.ts +0 -96
  91. package/src/add/add-workflow.test.ts +0 -155
  92. package/src/add/add-workflow.ts +0 -482
  93. package/src/add/addon-bans.test.ts +0 -121
  94. package/src/add/addon-contracts.test.ts +0 -221
  95. package/src/add/allow-escape-hatches.test.ts +0 -144
  96. package/src/add/audit-meta.test.ts +0 -75
  97. package/src/add/classified-sink-check.test.ts +0 -347
  98. package/src/add/config-value-metadata.test.ts +0 -263
  99. package/src/add/dynamic-import-check.test.ts +0 -119
  100. package/src/add/expect-eventually-scenario-only.test.ts +0 -108
  101. package/src/add/function-body-span.test.ts +0 -96
  102. package/src/add/inline-wiring-function.test.ts +0 -104
  103. package/src/add/pii-check.test.ts +0 -258
  104. package/src/add/rpc-type-cast.test.ts +0 -123
  105. package/src/add/scenario-instrumentation.test.ts +0 -89
  106. package/src/add/scenario-instrumentation.ts +0 -42
  107. package/src/add/scenario-marker.test.ts +0 -89
  108. package/src/add/scenario-not-extractable.test.ts +0 -159
  109. package/src/add/scenario-services.test.ts +0 -103
  110. package/src/add/scenario-skip.test.ts +0 -83
  111. package/src/add/validate-workflow-graph-addons.test.ts +0 -102
  112. package/src/add/wire-name-literal.test.ts +0 -117
  113. package/src/error-codes-emitted.test.ts +0 -76
  114. package/src/error-codes.ts +0 -145
  115. package/src/index.ts +0 -66
  116. package/src/inspector.ts +0 -575
  117. package/src/schema-generator.ts +0 -1
  118. package/src/source-file-cache.ts +0 -73
  119. package/src/tsconfig-paths.test.ts +0 -90
  120. package/src/types-map.ts +0 -141
  121. package/src/types.ts +0 -775
  122. package/src/utils/annotate-http-route-auth.test.ts +0 -161
  123. package/src/utils/annotate-http-route-auth.ts +0 -53
  124. package/src/utils/check-classified-sinks.ts +0 -215
  125. package/src/utils/check-pii-output.ts +0 -120
  126. package/src/utils/compute-diagnostics.test.ts +0 -106
  127. package/src/utils/compute-required-schemas.ts +0 -48
  128. package/src/utils/contract-hashes.test.ts +0 -553
  129. package/src/utils/contract-hashes.ts +0 -387
  130. package/src/utils/custom-types-generator.test.ts +0 -124
  131. package/src/utils/custom-types-generator.ts +0 -150
  132. package/src/utils/detect-schema-vendor.test.ts +0 -123
  133. package/src/utils/detect-schema-vendor.ts +0 -120
  134. package/src/utils/does-type-extend-core-type.ts +0 -53
  135. package/src/utils/ensure-function-metadata.ts +0 -385
  136. package/src/utils/extract-function-name.ts +0 -612
  137. package/src/utils/extract-function-node.ts +0 -70
  138. package/src/utils/extract-node-value.test.ts +0 -117
  139. package/src/utils/extract-node-value.ts +0 -172
  140. package/src/utils/extract-secret-usage.test.ts +0 -81
  141. package/src/utils/extract-secret-usage.ts +0 -73
  142. package/src/utils/extract-services.ts +0 -70
  143. package/src/utils/extract-surface-usage.test.ts +0 -147
  144. package/src/utils/extract-surface-usage.ts +0 -121
  145. package/src/utils/filter-inspector-state.test.ts +0 -2232
  146. package/src/utils/filter-inspector-state.ts +0 -1298
  147. package/src/utils/filter-utils.test.ts +0 -843
  148. package/src/utils/filter-utils.ts +0 -161
  149. package/src/utils/find-root-dir.ts +0 -68
  150. package/src/utils/find-zod-transform.test.ts +0 -70
  151. package/src/utils/find-zod-transform.ts +0 -103
  152. package/src/utils/get-exported-variable-name.ts +0 -48
  153. package/src/utils/get-files-and-methods.ts +0 -157
  154. package/src/utils/get-property-value.test.ts +0 -141
  155. package/src/utils/get-property-value.ts +0 -331
  156. package/src/utils/hash.ts +0 -26
  157. package/src/utils/literal-properties.ts +0 -97
  158. package/src/utils/load-addon-functions-meta.test.ts +0 -157
  159. package/src/utils/load-addon-functions-meta.ts +0 -508
  160. package/src/utils/load-addon-scopes.test.ts +0 -138
  161. package/src/utils/middleware.test.ts +0 -204
  162. package/src/utils/middleware.ts +0 -303
  163. package/src/utils/nested-project-filter.test.ts +0 -53
  164. package/src/utils/nested-project-filter.ts +0 -28
  165. package/src/utils/permissions-shorthand.test.ts +0 -79
  166. package/src/utils/permissions.test.ts +0 -125
  167. package/src/utils/permissions.ts +0 -129
  168. package/src/utils/post-process.test.ts +0 -1094
  169. package/src/utils/post-process.ts +0 -1604
  170. package/src/utils/resolve-addon-package.ts +0 -106
  171. package/src/utils/resolve-core-type.test.ts +0 -142
  172. package/src/utils/resolve-core-type.ts +0 -35
  173. package/src/utils/resolve-deploy-target.test.ts +0 -135
  174. package/src/utils/resolve-deploy-target.ts +0 -65
  175. package/src/utils/resolve-function-meta.ts +0 -25
  176. package/src/utils/resolve-function-types.ts +0 -42
  177. package/src/utils/resolve-identifier.ts +0 -46
  178. package/src/utils/resolve-ref-contract.ts +0 -71
  179. package/src/utils/resolve-versions.test.ts +0 -499
  180. package/src/utils/resolve-versions.ts +0 -228
  181. package/src/utils/schema-cache.test.ts +0 -124
  182. package/src/utils/schema-generator.test.ts +0 -272
  183. package/src/utils/schema-generator.ts +0 -780
  184. package/src/utils/secret-alias-services.test.ts +0 -146
  185. package/src/utils/secret-alias-services.ts +0 -58
  186. package/src/utils/serialize-inspector-state.ts +0 -742
  187. package/src/utils/serialize-mcp-json.ts +0 -150
  188. package/src/utils/serialize-middleware-groups-meta.ts +0 -36
  189. package/src/utils/serialize-openapi-json.ts +0 -277
  190. package/src/utils/serialize-permissions-groups-meta.ts +0 -7
  191. package/src/utils/single-declaration.ts +0 -57
  192. package/src/utils/type-utils.ts +0 -145
  193. package/src/utils/validate-auth-sessionless.ts +0 -30
  194. package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
  195. package/src/utils/validate-exposed-functions-gated.ts +0 -103
  196. package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
  197. package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
  198. package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
  199. package/src/utils/workflow/derive-workflow-plan.ts +0 -117
  200. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
  201. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
  202. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
  203. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
  204. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
  205. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
  206. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
  207. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
  208. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
  209. package/src/utils/workflow/dsl/index.ts +0 -11
  210. package/src/utils/workflow/dsl/patterns.ts +0 -440
  211. package/src/utils/workflow/dsl/validation.ts +0 -203
  212. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
  213. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
  214. package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
  215. package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
  216. package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
  217. package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
  218. package/src/utils/workflow/graph/index.ts +0 -28
  219. package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
  220. package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
  221. package/src/visit.ts +0 -181
  222. package/tsconfig.json +0 -19
  223. package/tsconfig.tsbuildinfo +0 -1
@@ -1,155 +0,0 @@
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(
10
- criticals: Array<{ code: string; message: string }>
11
- ): InspectorLogger {
12
- return {
13
- debug: () => {},
14
- info: () => {},
15
- warn: () => {},
16
- error: () => {},
17
- diagnostic: ({ code, message }) => {
18
- criticals.push({ code, message })
19
- },
20
- critical: (code: any, message: string) => {
21
- criticals.push({ code, message })
22
- },
23
- hasCriticalErrors: () => criticals.length > 0,
24
- }
25
- }
26
-
27
- describe('addWorkflow — workflow.do RPC detection', () => {
28
- test('detects RPC step when result is assigned to a const', async () => {
29
- const rootDir = await mkdtemp(join(tmpdir(), 'pikku-workflow-'))
30
- const wfFile = join(rootDir, 'my.workflow.ts')
31
- const stepFile = join(rootDir, 'my.steps.ts')
32
-
33
- await writeFile(
34
- stepFile,
35
- [
36
- "import { pikkuSessionlessFunc } from '@pikku/core'",
37
- 'export const doThing = pikkuSessionlessFunc({',
38
- ' func: async ({ logger }) => ({ ok: true }),',
39
- '})',
40
- ].join('\n')
41
- )
42
-
43
- await writeFile(
44
- wfFile,
45
- [
46
- "import { pikkuWorkflowFunc } from '@pikku/core/workflow'",
47
- 'export const myWorkflow = pikkuWorkflowFunc(async (_, _input, { workflow }) => {',
48
- " const result = await workflow.do('Do thing', 'doThing', {})",
49
- ' return { id: result.ok }',
50
- '})',
51
- ].join('\n')
52
- )
53
-
54
- const criticals: Array<{ code: string; message: string }> = []
55
- try {
56
- const state = await inspect(makeLogger(criticals), [stepFile, wfFile], {
57
- rootDir,
58
- })
59
- assert.ok(
60
- state.rpc.invokedFunctions.has('doThing'),
61
- 'doThing should be in invokedFunctions'
62
- )
63
- assert.ok(
64
- state.rpc.internalFiles.has('doThing'),
65
- 'doThing should be in internalFiles'
66
- )
67
- } finally {
68
- await rm(rootDir, { recursive: true, force: true })
69
- }
70
- })
71
-
72
- test('detects RPC step when result is reassigned to a pre-declared null variable', async () => {
73
- // Regression: `let x = null; x = await workflow.do(...)` was treated as a
74
- // set-step instead of an RPC step, so the referenced function was never registered.
75
- const rootDir = await mkdtemp(join(tmpdir(), 'pikku-workflow-reassign-'))
76
- const wfFile = join(rootDir, 'project.workflow.ts')
77
- const stepFile = join(rootDir, 'project.steps.ts')
78
-
79
- await writeFile(
80
- stepFile,
81
- [
82
- "import { pikkuSessionlessFunc } from '@pikku/core'",
83
- 'export const launchSandbox = pikkuSessionlessFunc({',
84
- ' func: async ({ logger }) => ({ sandboxId: "abc" }),',
85
- '})',
86
- ].join('\n')
87
- )
88
-
89
- await writeFile(
90
- wfFile,
91
- [
92
- "import { pikkuWorkflowFunc } from '@pikku/core/workflow'",
93
- 'export const createProjectWorkflow = pikkuWorkflowFunc(async (_, input, { workflow }) => {',
94
- ' let launched: { sandboxId: string } | null = null',
95
- ' if (input.createSandbox) {',
96
- " launched = await workflow.do('Launch sandbox', 'launchSandbox', { projectId: input.projectId })",
97
- ' }',
98
- ' return { sandboxId: launched?.sandboxId ?? null }',
99
- '})',
100
- ].join('\n')
101
- )
102
-
103
- const criticals: Array<{ code: string; message: string }> = []
104
- try {
105
- const state = await inspect(makeLogger(criticals), [stepFile, wfFile], {
106
- rootDir,
107
- })
108
- assert.ok(
109
- state.rpc.invokedFunctions.has('launchSandbox'),
110
- 'launchSandbox should be in invokedFunctions even when assigned to a pre-declared null variable'
111
- )
112
- assert.ok(
113
- state.rpc.internalFiles.has('launchSandbox'),
114
- 'launchSandbox should be in internalFiles so it gets registered in pikku-functions.gen.ts'
115
- )
116
- } finally {
117
- await rm(rootDir, { recursive: true, force: true })
118
- }
119
- })
120
-
121
- test('still treats plain reassignment to context var as a set step', async () => {
122
- const rootDir = await mkdtemp(join(tmpdir(), 'pikku-workflow-set-'))
123
- const wfFile = join(rootDir, 'set.workflow.ts')
124
-
125
- await writeFile(
126
- wfFile,
127
- [
128
- "import { pikkuWorkflowFunc } from '@pikku/core/workflow'",
129
- 'export const setWorkflow = pikkuWorkflowFunc(async (_, input, { workflow }) => {',
130
- ' let status = "pending"',
131
- ' status = "done"',
132
- " await workflow.do('No-op', 'noopStep', {})",
133
- ' return { status }',
134
- '})',
135
- ].join('\n')
136
- )
137
-
138
- const criticals: Array<{ code: string; message: string }> = []
139
- try {
140
- const state = await inspect(makeLogger(criticals), [wfFile], { rootDir })
141
- const meta = state.workflows.meta['setWorkflow']
142
- assert.ok(meta, 'workflow should be registered')
143
- const steps = meta.steps ?? []
144
- const setStep = steps.find(
145
- (s: any) => s.type === 'set' && s.variable === 'status'
146
- )
147
- assert.ok(
148
- setStep,
149
- 'plain string reassignment should still produce a set step'
150
- )
151
- } finally {
152
- await rm(rootDir, { recursive: true, force: true })
153
- }
154
- })
155
- })
@@ -1,482 +0,0 @@
1
- import * as ts from 'typescript'
2
- import type { AddWiring, InspectorState } from '../types.js'
3
- import { extractFunctionName } from '../utils/extract-function-name.js'
4
- import { extractFunctionNode } from '../utils/extract-function-node.js'
5
- import { ErrorCode } from '../error-codes.js'
6
- import type { WorkflowStepMeta, WorkflowContext } from '@pikku/core/workflow'
7
- import {
8
- extractStringLiteral,
9
- isStringLike,
10
- isFunctionLike,
11
- extractDescription,
12
- extractDuration,
13
- } from '../utils/extract-node-value.js'
14
- import {
15
- getCommonWireMetaData,
16
- getPropertyValue,
17
- } from '../utils/get-property-value.js'
18
- import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js'
19
- import {
20
- getSourceText,
21
- extractActorFromOptions,
22
- } from '../utils/workflow/dsl/patterns.js'
23
-
24
- /**
25
- * Extract a workflow step's display name without letting a non-static name
26
- * (e.g. a function call) abort the scan. The step name is cosmetic, so a
27
- * resolution failure must never prevent the RPC from being registered.
28
- */
29
- function extractStepName(node: ts.Node, checker: ts.TypeChecker): string {
30
- try {
31
- return extractStringLiteral(node, checker)
32
- } catch {
33
- return getSourceText(node)
34
- }
35
- }
36
-
37
- /**
38
- * Walk a function body for any `X.expectEventually(...)` call. Used to enforce
39
- * that the durable-polling assertion is a scenario-only primitive — regular
40
- * workflows must not carry test/assertion semantics.
41
- */
42
- function containsExpectEventuallyCall(node: ts.Node): boolean {
43
- if (
44
- ts.isCallExpression(node) &&
45
- ts.isPropertyAccessExpression(node.expression) &&
46
- node.expression.name.text === 'expectEventually'
47
- ) {
48
- return true
49
- }
50
- let found = false
51
- ts.forEachChild(node, (child) => {
52
- if (!found && !ts.isFunctionDeclaration(child)) {
53
- found = containsExpectEventuallyCall(child)
54
- }
55
- })
56
- return found
57
- }
58
-
59
- /**
60
- * Recursively check if any step has inline type (non-serializable)
61
- */
62
- function hasInlineSteps(steps: WorkflowStepMeta[]): boolean {
63
- for (const step of steps) {
64
- if (step.type === 'inline') {
65
- return true
66
- } else if (step.type === 'branch') {
67
- for (const branch of step.branches) {
68
- if (hasInlineSteps(branch.steps)) return true
69
- }
70
- if (step.elseSteps && hasInlineSteps(step.elseSteps)) return true
71
- } else if (step.type === 'switch' && step.cases) {
72
- for (const c of step.cases) {
73
- if (c.steps && hasInlineSteps(c.steps)) return true
74
- }
75
- if (step.defaultSteps && hasInlineSteps(step.defaultSteps)) return true
76
- } else if (step.type === 'fanout' && step.body) {
77
- if (hasInlineSteps(step.body)) return true
78
- } else if (step.type === 'parallel' && step.children) {
79
- if (hasInlineSteps(step.children)) return true
80
- }
81
- }
82
- return false
83
- }
84
-
85
- /**
86
- * Recursively collect all RPC names from workflow steps
87
- */
88
- export function collectInvokedRPCs(
89
- steps: WorkflowStepMeta[],
90
- rpcs: Set<string>
91
- ): void {
92
- for (const step of steps) {
93
- if (step.type === 'rpc' && step.rpcName) {
94
- rpcs.add(step.rpcName)
95
- // The compensation handler is invoked at runtime too, so it has to be
96
- // wired like any other step.
97
- if (step.options?.onError) {
98
- rpcs.add(step.options.onError)
99
- }
100
- } else if (step.type === 'scenarioStep' && step.stepFunc) {
101
- // Steps are bundled like RPCs — the runner calls them through
102
- // runPikkuFunc — but they are deliberately never RPC-registered.
103
- rpcs.add(step.stepFunc)
104
- } else if (step.type === 'branch') {
105
- for (const branch of step.branches) {
106
- collectInvokedRPCs(branch.steps, rpcs)
107
- }
108
- if (step.elseSteps) collectInvokedRPCs(step.elseSteps, rpcs)
109
- } else if (step.type === 'switch' && step.cases) {
110
- for (const c of step.cases) {
111
- if (c.steps) collectInvokedRPCs(c.steps, rpcs)
112
- }
113
- if (step.defaultSteps) collectInvokedRPCs(step.defaultSteps, rpcs)
114
- } else if (step.type === 'fanout' && step.body) {
115
- collectInvokedRPCs(step.body as WorkflowStepMeta[], rpcs)
116
- } else if (step.type === 'parallel' && step.children) {
117
- collectInvokedRPCs(step.children as WorkflowStepMeta[], rpcs)
118
- }
119
- }
120
- }
121
-
122
- /**
123
- * Scan for workflow.do(), workflow.expectEventually(), workflow.sleep(), and
124
- * workflow.cancel() calls to extract workflow steps
125
- */
126
- function getWorkflowInvocations(
127
- node: ts.Node,
128
- checker: ts.TypeChecker,
129
- state: InspectorState,
130
- workflowName: string,
131
- steps: WorkflowStepMeta[]
132
- ) {
133
- // Look for property access expressions: workflow.do or workflow.sleep
134
- if (ts.isPropertyAccessExpression(node)) {
135
- const { name } = node
136
-
137
- // Check if this is accessing 'do' or 'sleep' property
138
- if (
139
- name.text === 'do' ||
140
- name.text === 'sleep' ||
141
- name.text === 'cancel' ||
142
- name.text === 'expectEventually'
143
- ) {
144
- // Check if the parent is a call expression
145
- const parent = node.parent
146
- if (ts.isCallExpression(parent) && parent.expression === node) {
147
- const args = parent.arguments
148
-
149
- if (name.text === 'do' && args.length >= 2) {
150
- // workflow.do(stepName, rpcName|fn, data?, options?)
151
- const stepNameArg = args[0]
152
- const secondArg = args[1]
153
- const optionsArg =
154
- args.length >= 4 ? args[args.length - 1] : undefined
155
-
156
- const stepName = extractStepName(stepNameArg, checker)
157
- const description =
158
- extractDescription(optionsArg, checker) ?? undefined
159
-
160
- // Determine form by checking 2nd argument type
161
- if (isStringLike(secondArg, checker)) {
162
- // RPC form: workflow.do(stepName, rpcName, data, options?)
163
- const rpcName = extractStringLiteral(secondArg, checker)
164
- steps.push({
165
- type: 'rpc',
166
- stepName,
167
- rpcName,
168
- actor: extractActorFromOptions(optionsArg),
169
- })
170
- state.rpc.invokedFunctions.add(rpcName)
171
- } else if (isFunctionLike(secondArg)) {
172
- // Inline form: workflow.do(stepName, fn, options?)
173
- steps.push({
174
- type: 'inline',
175
- stepName: stepName || '<dynamic>',
176
- description: description || '<dynamic>',
177
- })
178
- }
179
- } else if (name.text === 'expectEventually' && args.length >= 4) {
180
- // workflow.expectEventually(stepName, rpcName, data, predicate, options?)
181
- const stepName = extractStepName(args[0], checker)
182
- if (isStringLike(args[1], checker)) {
183
- const rpcName = extractStringLiteral(args[1], checker)
184
- steps.push({
185
- type: 'rpc',
186
- stepName,
187
- rpcName,
188
- expectEventually: true,
189
- actor: extractActorFromOptions(
190
- args.length >= 5 ? args[4] : undefined
191
- ),
192
- })
193
- state.rpc.invokedFunctions.add(rpcName)
194
- }
195
- } else if (name.text === 'sleep' && args.length >= 2) {
196
- // workflow.sleep(stepName, duration)
197
- const stepNameArg = args[0]
198
- const durationArg = args[1]
199
-
200
- const stepName = extractStepName(stepNameArg, checker)
201
- const duration = extractDuration(durationArg, checker)
202
-
203
- steps.push({
204
- type: 'sleep',
205
- stepName: stepName || '<dynamic>',
206
- duration: duration || '<dynamic>',
207
- })
208
- } else if (name.text === 'cancel') {
209
- // workflow.cancel(reason?)
210
- steps.push({
211
- type: 'cancel',
212
- })
213
- }
214
- }
215
- }
216
- }
217
-
218
- // Recurse into children, including arrow functions (for Promise.all callbacks)
219
- // but skip function declarations (which would be separate functions)
220
- ts.forEachChild(node, (child) => {
221
- if (ts.isFunctionDeclaration(child)) {
222
- return
223
- }
224
- getWorkflowInvocations(child, checker, state, workflowName, steps)
225
- })
226
- }
227
-
228
- /**
229
- * Inspector for pikkuWorkflowFunc(), pikkuWorkflowComplexFunc() and
230
- * pikkuScenario() calls. Detects workflow registration and extracts metadata.
231
- */
232
- export const addWorkflow: AddWiring = (
233
- logger,
234
- node,
235
- checker,
236
- state,
237
- options
238
- ) => {
239
- if (!ts.isCallExpression(node)) {
240
- return
241
- }
242
-
243
- const args = node.arguments
244
- const firstArg = args[0]
245
- const expression = node.expression
246
-
247
- if (!ts.isIdentifier(expression)) {
248
- return
249
- }
250
-
251
- let wrapperType: 'dsl' | 'complex' | 'scenario' | null = null
252
- if (expression.text === 'pikkuWorkflowFunc') {
253
- wrapperType = 'dsl'
254
- } else if (expression.text === 'pikkuWorkflowComplexFunc') {
255
- if (!options.allow?.complexWorkflows) {
256
- logger.critical(
257
- ErrorCode.COMPLEX_WORKFLOW_NOT_ALLOWED,
258
- `'${expression.text}' is used here, but this project has not enabled complex workflows. ` +
259
- `Their inline steps cannot be serialized, so they are absent from the workflow graph and ` +
260
- `cannot be replayed or migrated across a definition change. Express the workflow with ` +
261
- `pikkuWorkflowFunc and the step DSL, or set \`allow: { complexWorkflows: true }\` in ` +
262
- `pikku.config.json if the project accepts that trade.`
263
- )
264
- return
265
- }
266
- wrapperType = 'complex'
267
- } else if (expression.text === 'pikkuScenario') {
268
- // A scenario is a complex workflow whose steps run as actors over the
269
- // real transport — same extraction rules as complex, distinct meta.
270
- wrapperType = 'scenario'
271
- } else {
272
- return
273
- }
274
-
275
- if (!firstArg) {
276
- return
277
- }
278
-
279
- // Extract workflow name and metadata using same logic as add-functions
280
- const { pikkuFuncId, name, exportedName } = extractFunctionName(
281
- node,
282
- checker,
283
- state.rootDir
284
- )
285
-
286
- const workflowName = exportedName || name
287
-
288
- if (!workflowName) {
289
- logger.critical(
290
- ErrorCode.MISSING_NAME,
291
- `Could not determine workflow name from export.`
292
- )
293
- return
294
- }
295
-
296
- // Extract the function node (either direct function or from config.func)
297
- const { funcNode, resolvedFunc } = extractFunctionNode(firstArg, checker)
298
-
299
- // Extract metadata if using object form
300
- let tags: string[] | undefined
301
- let title: string | undefined
302
- let summary: string | undefined
303
- let description: string | undefined
304
- let errors: string[] | undefined
305
- let expose: boolean | undefined
306
- let skip: string | undefined
307
-
308
- if (ts.isObjectLiteralExpression(firstArg)) {
309
- const metadata = getCommonWireMetaData(
310
- firstArg,
311
- 'Workflow',
312
- workflowName,
313
- logger,
314
- checker
315
- )
316
- if (metadata.disabled) return
317
- tags = metadata.tags
318
- title = metadata.title
319
- summary = metadata.summary
320
- description = metadata.description
321
- errors = metadata.errors
322
-
323
- expose = getPropertyValue(firstArg, 'expose') as boolean | undefined
324
- skip = (getPropertyValue(firstArg, 'skip') as string | null) ?? undefined
325
- }
326
-
327
- // Validate that we got a valid function
328
- if (
329
- ts.isObjectLiteralExpression(firstArg) &&
330
- (!funcNode || funcNode === firstArg)
331
- ) {
332
- logger.critical(
333
- ErrorCode.MISSING_FUNC,
334
- `No valid 'func' property for workflow '${workflowName}'.`
335
- )
336
- return
337
- }
338
-
339
- if (!resolvedFunc) {
340
- logger.critical(
341
- ErrorCode.MISSING_FUNC,
342
- `Could not resolve workflow function for '${workflowName}'.`
343
- )
344
- return
345
- }
346
-
347
- if (
348
- wrapperType !== 'scenario' &&
349
- containsExpectEventuallyCall(resolvedFunc)
350
- ) {
351
- logger.critical(
352
- ErrorCode.EXPECT_EVENTUALLY_SCENARIO_ONLY,
353
- `Workflow '${workflowName}' calls workflow.expectEventually(), which is only ` +
354
- `available in scenarios (pikkuScenario). Move it into a scenario, or drive ` +
355
- `the assertion outside the workflow.`
356
- )
357
- return
358
- }
359
-
360
- // Track workflow file for wiring generation
361
- if (exportedName) {
362
- state.workflows.files.set(pikkuFuncId, {
363
- path: node.getSourceFile().fileName,
364
- exportedName,
365
- })
366
- }
367
-
368
- let steps: WorkflowStepMeta[] = []
369
- let context: WorkflowContext | undefined = undefined
370
- let dsl: boolean | undefined = undefined
371
- let asserts: boolean | undefined = undefined
372
-
373
- // Try DSL workflow extraction first
374
- // Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
375
- const result = extractDSLWorkflow(node, checker, {
376
- allowInline: wrapperType !== 'dsl',
377
- })
378
-
379
- if (result.status === 'ok' && result.steps) {
380
- // Extraction succeeded
381
- steps = result.steps
382
- context = result.context
383
- asserts = result.asserts
384
-
385
- // Check if workflow contains inline steps (non-serializable)
386
- if (hasInlineSteps(steps)) {
387
- if (wrapperType === 'dsl') {
388
- // pikkuWorkflowFunc should not have inline steps
389
- logger.critical(
390
- ErrorCode.INVALID_DSL_WORKFLOW,
391
- `Workflow '${workflowName}' uses pikkuWorkflowFunc but contains inline steps which are not allowed in DSL workflows. Use pikkuWorkflowComplexFunc instead.`
392
- )
393
- return
394
- }
395
- // pikkuWorkflowComplexFunc with inline steps is marked as non-dsl
396
- dsl = false
397
- } else {
398
- // pikkuWorkflowComplexFunc is always non-dsl, pikkuWorkflowFunc is dsl
399
- dsl = wrapperType === 'dsl'
400
- }
401
-
402
- // Collect all invoked RPCs from workflow steps
403
- const rpcs = new Set<string>()
404
- collectInvokedRPCs(steps, rpcs)
405
- for (const rpc of rpcs) {
406
- state.rpc.invokedFunctions.add(rpc)
407
- }
408
- } else {
409
- // DSL extraction failed
410
- if (wrapperType === 'dsl') {
411
- // For pikkuWorkflowFunc, this is a critical error
412
- // But still track RPC invocations for function registration
413
- getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps)
414
- logger.critical(
415
- ErrorCode.INVALID_DSL_WORKFLOW,
416
- `Workflow '${workflowName}' does not conform to DSL workflow rules:\n${result.reason || 'Unknown error'}`
417
- )
418
- return
419
- } else if (wrapperType === 'scenario') {
420
- // The fallback below understands do/sleep but not step/given/when/then,
421
- // so a scenario that lands here registers with zero steps. Silence would
422
- // leave the whole scenario passing vacuously.
423
- logger.critical(
424
- ErrorCode.SCENARIO_NOT_EXTRACTABLE,
425
- `Scenario '${workflowName}' could not be extracted, so none of its steps were recorded and it would run as an empty scenario:\n${result.reason || 'Unknown error'}`
426
- )
427
- return
428
- } else {
429
- // For pikkuWorkflowComplexFunc, fall back to basic extraction
430
- logger.debug(
431
- `Workflow '${workflowName}' could not be extracted as DSL workflow: ${result.reason || 'Unknown error'}. Falling back to basic extraction.`
432
- )
433
- dsl = false
434
- }
435
- }
436
-
437
- // For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
438
- // patterns the DSL extractor doesn't handle (array+push, nested Promise.all
439
- // with identifier args, etc.) are still registered as invoked functions.
440
- // When DSL extraction already produced steps this pass is registration-only:
441
- // appending to `steps` would duplicate nodes and clobber the graph.
442
- if (wrapperType !== 'dsl') {
443
- const sink: WorkflowStepMeta[] = steps.length > 0 ? [] : steps
444
- getWorkflowInvocations(resolvedFunc, checker, state, workflowName, sink)
445
- }
446
-
447
- // Actor names the flow's steps run as (scenarios) — powers the console
448
- // personas view and the Scenario graph badges.
449
- const actorNames = [
450
- ...new Set(
451
- steps
452
- .map((s) => ('actor' in s ? s.actor : undefined))
453
- .filter((a): a is string => typeof a === 'string')
454
- ),
455
- ]
456
-
457
- state.workflows.meta[workflowName] = {
458
- pikkuFuncId,
459
- name: workflowName,
460
- steps,
461
- context,
462
- dsl,
463
- title,
464
- summary,
465
- description,
466
- errors,
467
- tags,
468
- expose,
469
- skip,
470
- scenario: wrapperType === 'scenario' ? true : undefined,
471
- asserts,
472
- actors: actorNames.length > 0 ? actorNames : undefined,
473
- }
474
-
475
- // The scenario "logger/config only" rule (PKU673) and the implicit
476
- // workflow platform-service injection both used to live here, reading
477
- // state.functions.meta[pikkuFuncId]. addWorkflow runs in the visitSetup
478
- // pass, which completes before addFunctions populates that meta, so both
479
- // were dead. The rule now lives in validateScenarioServices (post-process),
480
- // and aggregateRequiredServices already adds the platform services globally
481
- // whenever any workflow exists.
482
- }