@pikku/inspector 0.12.74 → 0.12.81

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 (219) hide show
  1. package/CHANGELOG.md +347 -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-wire-addon.js +1 -0
  8. package/dist/add/add-wire-remote-addon.js +1 -0
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.js +3 -1
  11. package/dist/inspector.js +4 -0
  12. package/dist/surface/collect-surface.d.ts +58 -0
  13. package/dist/surface/collect-surface.js +613 -0
  14. package/dist/types.d.ts +20 -11
  15. package/dist/utils/filter-inspector-state.js +23 -13
  16. package/dist/utils/post-process.js +15 -0
  17. package/dist/utils/read-module-specifiers.d.ts +8 -0
  18. package/dist/utils/read-module-specifiers.js +23 -0
  19. package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
  20. package/dist/utils/read-tsconfig-out-dir.js +12 -0
  21. package/dist/utils/resolve-deploy-target.d.ts +7 -0
  22. package/dist/utils/resolve-deploy-target.js +15 -7
  23. package/dist/utils/serialize-inspector-state.d.ts +5 -0
  24. package/dist/utils/serialize-inspector-state.js +8 -0
  25. package/dist/visit.js +4 -0
  26. package/package.json +16 -6
  27. package/run-tests.sh +21 -6
  28. package/src/add/add-addon-bans.ts +0 -85
  29. package/src/add/add-agent-scorer.test.ts +0 -197
  30. package/src/add/add-agent-scorer.ts +0 -83
  31. package/src/add/add-agent.test.ts +0 -87
  32. package/src/add/add-agent.ts +0 -638
  33. package/src/add/add-approval-description.ts +0 -76
  34. package/src/add/add-auth.test.ts +0 -791
  35. package/src/add/add-auth.ts +0 -424
  36. package/src/add/add-channel.ts +0 -778
  37. package/src/add/add-cli-renderers.test.ts +0 -74
  38. package/src/add/add-cli.ts +0 -1006
  39. package/src/add/add-credential.ts +0 -186
  40. package/src/add/add-feature.test.ts +0 -306
  41. package/src/add/add-feature.ts +0 -253
  42. package/src/add/add-file-extends-core-type.ts +0 -66
  43. package/src/add/add-file-with-config.ts +0 -45
  44. package/src/add/add-file-with-factory.test.ts +0 -134
  45. package/src/add/add-file-with-factory.ts +0 -288
  46. package/src/add/add-functions.test.ts +0 -743
  47. package/src/add/add-functions.ts +0 -1548
  48. package/src/add/add-gateway.ts +0 -143
  49. package/src/add/add-http-route-ref.test.ts +0 -149
  50. package/src/add/add-http-route.ts +0 -530
  51. package/src/add/add-http-routes.ts +0 -471
  52. package/src/add/add-keyed-wiring.ts +0 -180
  53. package/src/add/add-mcp-prompt.ts +0 -157
  54. package/src/add/add-mcp-resource.ts +0 -175
  55. package/src/add/add-middleware.test.ts +0 -196
  56. package/src/add/add-middleware.ts +0 -879
  57. package/src/add/add-permission-auth.test.ts +0 -59
  58. package/src/add/add-permission.ts +0 -311
  59. package/src/add/add-personas.test.ts +0 -406
  60. package/src/add/add-personas.ts +0 -287
  61. package/src/add/add-queue-worker.ts +0 -117
  62. package/src/add/add-rpc-invocations.test.ts +0 -287
  63. package/src/add/add-rpc-invocations.ts +0 -221
  64. package/src/add/add-scenario-step.test.ts +0 -509
  65. package/src/add/add-schedule.ts +0 -120
  66. package/src/add/add-scope.test.ts +0 -419
  67. package/src/add/add-scope.ts +0 -221
  68. package/src/add/add-secret.ts +0 -10
  69. package/src/add/add-subject-scenario-steps.test.ts +0 -161
  70. package/src/add/add-system-role.test.ts +0 -297
  71. package/src/add/add-system-role.ts +0 -162
  72. package/src/add/add-trigger.ts +0 -165
  73. package/src/add/add-variable.ts +0 -10
  74. package/src/add/add-wire-addon.test.ts +0 -185
  75. package/src/add/add-wire-addon.ts +0 -146
  76. package/src/add/add-wire-remote-addon.ts +0 -80
  77. package/src/add/add-workflow-edge-cases.test.ts +0 -201
  78. package/src/add/add-workflow-fanout.test.ts +0 -166
  79. package/src/add/add-workflow-graph-foreach.test.ts +0 -174
  80. package/src/add/add-workflow-graph-input.test.ts +0 -94
  81. package/src/add/add-workflow-graph-notes.test.ts +0 -74
  82. package/src/add/add-workflow-graph.ts +0 -644
  83. package/src/add/add-workflow-on-error.test.ts +0 -118
  84. package/src/add/add-workflow-return-switch.test.ts +0 -121
  85. package/src/add/add-workflow-silent-drops.test.ts +0 -261
  86. package/src/add/add-workflow-sleep-expression.test.ts +0 -96
  87. package/src/add/add-workflow.test.ts +0 -155
  88. package/src/add/add-workflow.ts +0 -482
  89. package/src/add/addon-bans.test.ts +0 -121
  90. package/src/add/addon-contracts.test.ts +0 -221
  91. package/src/add/allow-escape-hatches.test.ts +0 -144
  92. package/src/add/audit-meta.test.ts +0 -75
  93. package/src/add/classified-sink-check.test.ts +0 -347
  94. package/src/add/config-value-metadata.test.ts +0 -263
  95. package/src/add/dynamic-import-check.test.ts +0 -119
  96. package/src/add/expect-eventually-scenario-only.test.ts +0 -108
  97. package/src/add/function-body-span.test.ts +0 -96
  98. package/src/add/inline-wiring-function.test.ts +0 -104
  99. package/src/add/pii-check.test.ts +0 -258
  100. package/src/add/rpc-type-cast.test.ts +0 -123
  101. package/src/add/scenario-instrumentation.test.ts +0 -89
  102. package/src/add/scenario-instrumentation.ts +0 -42
  103. package/src/add/scenario-marker.test.ts +0 -89
  104. package/src/add/scenario-not-extractable.test.ts +0 -159
  105. package/src/add/scenario-services.test.ts +0 -103
  106. package/src/add/scenario-skip.test.ts +0 -83
  107. package/src/add/validate-workflow-graph-addons.test.ts +0 -102
  108. package/src/add/wire-name-literal.test.ts +0 -117
  109. package/src/error-codes-emitted.test.ts +0 -76
  110. package/src/error-codes.ts +0 -145
  111. package/src/index.ts +0 -66
  112. package/src/inspector.ts +0 -575
  113. package/src/schema-generator.ts +0 -1
  114. package/src/source-file-cache.ts +0 -73
  115. package/src/tsconfig-paths.test.ts +0 -90
  116. package/src/types-map.ts +0 -141
  117. package/src/types.ts +0 -775
  118. package/src/utils/annotate-http-route-auth.test.ts +0 -161
  119. package/src/utils/annotate-http-route-auth.ts +0 -53
  120. package/src/utils/check-classified-sinks.ts +0 -215
  121. package/src/utils/check-pii-output.ts +0 -120
  122. package/src/utils/compute-diagnostics.test.ts +0 -106
  123. package/src/utils/compute-required-schemas.ts +0 -48
  124. package/src/utils/contract-hashes.test.ts +0 -553
  125. package/src/utils/contract-hashes.ts +0 -387
  126. package/src/utils/custom-types-generator.test.ts +0 -124
  127. package/src/utils/custom-types-generator.ts +0 -150
  128. package/src/utils/detect-schema-vendor.test.ts +0 -123
  129. package/src/utils/detect-schema-vendor.ts +0 -120
  130. package/src/utils/does-type-extend-core-type.ts +0 -53
  131. package/src/utils/ensure-function-metadata.ts +0 -385
  132. package/src/utils/extract-function-name.ts +0 -612
  133. package/src/utils/extract-function-node.ts +0 -70
  134. package/src/utils/extract-node-value.test.ts +0 -117
  135. package/src/utils/extract-node-value.ts +0 -172
  136. package/src/utils/extract-secret-usage.test.ts +0 -81
  137. package/src/utils/extract-secret-usage.ts +0 -73
  138. package/src/utils/extract-services.ts +0 -70
  139. package/src/utils/extract-surface-usage.test.ts +0 -147
  140. package/src/utils/extract-surface-usage.ts +0 -121
  141. package/src/utils/filter-inspector-state.test.ts +0 -2207
  142. package/src/utils/filter-inspector-state.ts +0 -1302
  143. package/src/utils/filter-utils.test.ts +0 -843
  144. package/src/utils/filter-utils.ts +0 -161
  145. package/src/utils/find-root-dir.ts +0 -68
  146. package/src/utils/find-zod-transform.test.ts +0 -70
  147. package/src/utils/find-zod-transform.ts +0 -103
  148. package/src/utils/get-exported-variable-name.ts +0 -48
  149. package/src/utils/get-files-and-methods.ts +0 -157
  150. package/src/utils/get-property-value.test.ts +0 -141
  151. package/src/utils/get-property-value.ts +0 -331
  152. package/src/utils/hash.ts +0 -26
  153. package/src/utils/literal-properties.ts +0 -97
  154. package/src/utils/load-addon-functions-meta.test.ts +0 -157
  155. package/src/utils/load-addon-functions-meta.ts +0 -508
  156. package/src/utils/load-addon-scopes.test.ts +0 -138
  157. package/src/utils/middleware.test.ts +0 -204
  158. package/src/utils/middleware.ts +0 -303
  159. package/src/utils/nested-project-filter.test.ts +0 -53
  160. package/src/utils/nested-project-filter.ts +0 -28
  161. package/src/utils/permissions-shorthand.test.ts +0 -79
  162. package/src/utils/permissions.test.ts +0 -125
  163. package/src/utils/permissions.ts +0 -129
  164. package/src/utils/post-process.test.ts +0 -1094
  165. package/src/utils/post-process.ts +0 -1604
  166. package/src/utils/resolve-addon-package.ts +0 -106
  167. package/src/utils/resolve-core-type.test.ts +0 -142
  168. package/src/utils/resolve-core-type.ts +0 -35
  169. package/src/utils/resolve-deploy-target.test.ts +0 -135
  170. package/src/utils/resolve-deploy-target.ts +0 -65
  171. package/src/utils/resolve-function-meta.ts +0 -25
  172. package/src/utils/resolve-function-types.ts +0 -42
  173. package/src/utils/resolve-identifier.ts +0 -46
  174. package/src/utils/resolve-ref-contract.ts +0 -71
  175. package/src/utils/resolve-versions.test.ts +0 -499
  176. package/src/utils/resolve-versions.ts +0 -228
  177. package/src/utils/schema-cache.test.ts +0 -124
  178. package/src/utils/schema-generator.test.ts +0 -272
  179. package/src/utils/schema-generator.ts +0 -780
  180. package/src/utils/secret-alias-services.test.ts +0 -146
  181. package/src/utils/secret-alias-services.ts +0 -58
  182. package/src/utils/serialize-inspector-state.ts +0 -742
  183. package/src/utils/serialize-mcp-json.ts +0 -150
  184. package/src/utils/serialize-middleware-groups-meta.ts +0 -36
  185. package/src/utils/serialize-openapi-json.ts +0 -277
  186. package/src/utils/serialize-permissions-groups-meta.ts +0 -7
  187. package/src/utils/single-declaration.ts +0 -57
  188. package/src/utils/type-utils.ts +0 -145
  189. package/src/utils/validate-auth-sessionless.ts +0 -30
  190. package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
  191. package/src/utils/validate-exposed-functions-gated.ts +0 -103
  192. package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
  193. package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
  194. package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
  195. package/src/utils/workflow/derive-workflow-plan.ts +0 -117
  196. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
  197. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
  198. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
  199. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
  200. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
  201. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
  202. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
  203. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
  204. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
  205. package/src/utils/workflow/dsl/index.ts +0 -11
  206. package/src/utils/workflow/dsl/patterns.ts +0 -440
  207. package/src/utils/workflow/dsl/validation.ts +0 -203
  208. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
  209. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
  210. package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
  211. package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
  212. package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
  213. package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
  214. package/src/utils/workflow/graph/index.ts +0 -28
  215. package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
  216. package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
  217. package/src/visit.ts +0 -181
  218. package/tsconfig.json +0 -19
  219. 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
- }