@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,406 +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 { ErrorCode } from '../error-codes.js'
8
- import type { InspectorLogger } from '../types.js'
9
-
10
- const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
11
- ({
12
- debug: () => {},
13
- info: () => {},
14
- warn: () => {},
15
- error: () => {},
16
- diagnostic: ({ code, message }) => {
17
- criticals.push({ code, message })
18
- },
19
- critical: (code: ErrorCode, message: string) => {
20
- criticals.push({ code, message })
21
- },
22
- hasCriticalErrors: () => criticals.length > 0,
23
- }) satisfies InspectorLogger
24
-
25
- const inspectSource = async (source: string) => {
26
- const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-personas-'))
27
- const file = join(rootDir, 'personas.ts')
28
- await writeFile(file, source)
29
- const criticals: Array<{ code: ErrorCode; message: string }> = []
30
- try {
31
- const state = await inspect(makeLogger(criticals), [file], { rootDir })
32
- return { state, criticals }
33
- } finally {
34
- await rm(rootDir, { recursive: true, force: true })
35
- }
36
- }
37
-
38
- /** Inspects several files at once, keyed by basename. */
39
- const inspectSources = async (sources: Record<string, string>) => {
40
- const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-personas-multi-'))
41
- const files: Record<string, string> = {}
42
- for (const [name, source] of Object.entries(sources)) {
43
- files[name] = join(rootDir, name)
44
- await writeFile(files[name]!, source)
45
- }
46
- const criticals: Array<{ code: ErrorCode; message: string }> = []
47
- try {
48
- const state = await inspect(makeLogger(criticals), Object.values(files), {
49
- rootDir,
50
- })
51
- return { state, criticals, files }
52
- } finally {
53
- await rm(rootDir, { recursive: true, force: true })
54
- }
55
- }
56
-
57
- /** The declared world every persona below is checked against. */
58
- const withRoles = (...lines: string[]) =>
59
- [
60
- "import { defineScope } from '@pikku/core/scope'",
61
- "import { defineSystemRole } from '@pikku/core/role'",
62
- "import { definePersonas } from '@pikku/core/persona'",
63
- 'defineScope({ catalogue: { scopes: { read: {} } }, admin: {} })',
64
- 'defineSystemRole({',
65
- " buyer: { scopes: ['catalogue:read'] },",
66
- " admin: { scopes: ['admin'] },",
67
- '})',
68
- ...lines,
69
- ].join('\n')
70
-
71
- describe('addPersonas inspector', () => {
72
- test('extracts a whole person', async () => {
73
- const { state, criticals } = await inspectSource(
74
- withRoles(
75
- 'definePersonas({',
76
- ' susan: {',
77
- " name: 'Susan',",
78
- " jobTitle: 'Buys for a small café',",
79
- " roles: ['buyer'],",
80
- " personality: 'Hunts cheap deals.',",
81
- " goals: ['Get the weekly order in under five minutes'],",
82
- " tags: ['commerce'],",
83
- " disposition: 'careless',",
84
- ' tuning: { repeatRate: 0.3 },',
85
- " fixtures: ['seedCatalogue'],",
86
- ' account: {},',
87
- ' },',
88
- '})'
89
- )
90
- )
91
-
92
- assert.deepEqual(criticals, [])
93
- assert.equal(state.personas.definitions.length, 1)
94
- const susan = state.personas.definitions[0]!
95
- assert.equal(susan.id, 'susan')
96
- assert.equal(susan.name, 'Susan')
97
- assert.equal(susan.jobTitle, 'Buys for a small café')
98
- assert.deepEqual(susan.roles, ['buyer'])
99
- assert.equal(susan.personality, 'Hunts cheap deals.')
100
- assert.deepEqual(susan.goals, [
101
- 'Get the weekly order in under five minutes',
102
- ])
103
- assert.deepEqual(susan.tags, ['commerce'])
104
- assert.equal(susan.disposition, 'careless')
105
- assert.deepEqual(susan.tuning, { repeatRate: 0.3 })
106
- assert.deepEqual(susan.fixtures, ['seedCatalogue'])
107
- assert.deepEqual(susan.account, {})
108
- assert.equal(susan.runnable, true)
109
- })
110
-
111
- test('the app a person signs into survives into the meta', async () => {
112
- const { state, criticals } = await inspectSource(
113
- withRoles(
114
- 'definePersonas({',
115
- ' susan: {',
116
- " name: 'Susan',",
117
- " app: 'storefront',",
118
- ' },',
119
- '})'
120
- )
121
- )
122
-
123
- assert.deepEqual(criticals, [])
124
- assert.equal(state.personas.definitions[0]!.app, 'storefront')
125
- })
126
-
127
- test('a person who names no app has none, rather than a guessed one', async () => {
128
- const { state } = await inspectSource(
129
- withRoles('definePersonas({', " susan: { name: 'Susan' },", '})')
130
- )
131
-
132
- assert.equal(state.personas.definitions[0]!.app, undefined)
133
- })
134
-
135
- test('extracts a declared avatar', async () => {
136
- const { state } = await inspectSource(
137
- withRoles(
138
- 'definePersonas({',
139
- ' susan: {',
140
- " name: 'Susan',",
141
- " avatarUrl: '/avatars/susan.png',",
142
- ' },',
143
- '})'
144
- )
145
- )
146
- assert.equal(state.personas.definitions[0]!.avatarUrl, '/avatars/susan.png')
147
- })
148
-
149
- // The console draws a colour-and-icon avatar from the persona's id when none
150
- // is declared, so an absent field has to stay absent rather than become an
151
- // empty string the renderer would treat as a broken image.
152
- test('leaves the avatar absent when none is declared', async () => {
153
- const { state } = await inspectSource(
154
- withRoles('definePersonas({', " susan: { name: 'Susan' },", '})')
155
- )
156
- assert.equal(state.personas.definitions[0]!.avatarUrl, undefined)
157
- })
158
-
159
- test('extracts the environments a persona declares', async () => {
160
- const { state } = await inspectSource(
161
- withRoles(
162
- 'definePersonas({',
163
- ' robin: {',
164
- " name: 'Robin',",
165
- " disposition: 'accountable',",
166
- " environments: ['staging', 'prod'],",
167
- ' },',
168
- '})'
169
- )
170
- )
171
- assert.deepEqual(state.personas.definitions[0]!.environments, [
172
- 'staging',
173
- 'prod',
174
- ])
175
- })
176
-
177
- // "Everywhere but production" is a fact about the config, and the meta file
178
- // outlives whichever environments were configured the day it was written —
179
- // so the absence is preserved rather than resolved into a list here.
180
- test('leaves environments absent when none are declared', async () => {
181
- const { state } = await inspectSource(
182
- withRoles('definePersonas({', " susan: { name: 'Susan' },", '})')
183
- )
184
- assert.equal(state.personas.definitions[0]!.environments, undefined)
185
- })
186
-
187
- test('the declaration key is the id', async () => {
188
- const { state } = await inspectSource(
189
- withRoles(
190
- 'definePersonas({',
191
- " susan: { name: 'Susan' },",
192
- " mallory: { name: 'Mallory' },",
193
- '})'
194
- )
195
- )
196
- assert.deepEqual(
197
- state.personas.definitions.map((p) => p.id),
198
- ['susan', 'mallory']
199
- )
200
- })
201
-
202
- test('a persona without a name is refused', async () => {
203
- const { state, criticals } = await inspectSource(
204
- withRoles('definePersonas({', " susan: { roles: ['buyer'] },", '})')
205
- )
206
- assert.equal(state.personas.definitions.length, 0)
207
- assert.ok(criticals.some((c) => /needs a literal 'name'/.test(c.message)))
208
- })
209
-
210
- test('an unknown disposition is refused rather than defaulted', async () => {
211
- const { state, criticals } = await inspectSource(
212
- withRoles(
213
- 'definePersonas({',
214
- " susan: { name: 'Susan', disposition: 'grumpy' },",
215
- '})'
216
- )
217
- )
218
- assert.equal(state.personas.definitions.length, 0)
219
- assert.ok(
220
- criticals.some((c) => /unknown disposition 'grumpy'/.test(c.message))
221
- )
222
- })
223
-
224
- // `repeatRate: 18` meaning 18% would silently double every call, and the run
225
- // would read as a product bug.
226
- test('a tuning dial outside its range is refused', async () => {
227
- const { state, criticals } = await inspectSource(
228
- withRoles(
229
- 'definePersonas({',
230
- " susan: { name: 'Susan', tuning: { repeatRate: 18 } },",
231
- '})'
232
- )
233
- )
234
- assert.equal(state.personas.definitions.length, 0)
235
- assert.ok(
236
- criticals.some((c) =>
237
- /repeatRate must be between 0 and 1/.test(c.message)
238
- )
239
- )
240
- })
241
-
242
- test('a linked account is extracted alongside the primary one', async () => {
243
- const { state, criticals } = await inspectSource(
244
- withRoles(
245
- 'definePersonas({',
246
- ' yasser: {',
247
- " name: 'Yasser',",
248
- ' account: {},',
249
- " linkedAccounts: { google: { provider: 'google' } },",
250
- ' },',
251
- '})'
252
- )
253
- )
254
- assert.deepEqual(criticals, [])
255
- const yasser = state.personas.definitions[0]!
256
- assert.deepEqual(yasser.account, {})
257
- assert.deepEqual(yasser.linkedAccounts, { google: { provider: 'google' } })
258
- // Their primary login is email+password, so they can still be driven.
259
- assert.equal(yasser.runnable, true)
260
- })
261
-
262
- // Driving a consent screen needs a human — refused at the CLI rather than
263
- // failing somewhere inside a browser.
264
- test('a persona whose own login is a provider is not runnable', async () => {
265
- const { state } = await inspectSource(
266
- withRoles(
267
- 'definePersonas({',
268
- " yasser: { name: 'Yasser', account: { provider: 'google' } },",
269
- '})'
270
- )
271
- )
272
- assert.equal(state.personas.definitions[0]!.runnable, false)
273
- })
274
-
275
- test('a person who is only ever acted upon says so', async () => {
276
- const { state } = await inspectSource(
277
- withRoles(
278
- 'definePersonas({',
279
- " target: { name: 'Target', account: {}, runnable: false },",
280
- '})'
281
- )
282
- )
283
- assert.equal(state.personas.definitions[0]!.runnable, false)
284
- })
285
-
286
- // One call site per codebase, so there is one place to read the cast from
287
- // and one place to add to. The keyed form above is how you declare more.
288
- test('a second definePersonas in the same file is refused', async () => {
289
- const { state, criticals } = await inspectSource(
290
- withRoles(
291
- "definePersonas({ susan: { name: 'Susan' } })",
292
- "definePersonas({ dave: { name: 'Dave' } })"
293
- )
294
- )
295
-
296
- const hit = criticals.find(
297
- (c) => c.code === ErrorCode.DUPLICATE_PERSONAS_DEFINITION
298
- )
299
- assert.ok(
300
- hit,
301
- `expected DUPLICATE_PERSONAS_DEFINITION, got ${JSON.stringify(criticals)}`
302
- )
303
- assert.deepEqual(
304
- state.personas.definitions.map((p) => p.id),
305
- ['susan']
306
- )
307
- })
308
-
309
- test('a second definePersonas in another file is refused', async () => {
310
- const { criticals, files } = await inspectSources({
311
- 'personas.ts': withRoles("definePersonas({ susan: { name: 'Susan' } })"),
312
- 'more-personas.ts': [
313
- "import { definePersonas } from '@pikku/core/persona'",
314
- "definePersonas({ dave: { name: 'Dave' } })",
315
- ].join('\n'),
316
- })
317
-
318
- const hit = criticals.find(
319
- (c) => c.code === ErrorCode.DUPLICATE_PERSONAS_DEFINITION
320
- )
321
- assert.ok(
322
- hit,
323
- `expected DUPLICATE_PERSONAS_DEFINITION, got ${JSON.stringify(criticals)}`
324
- )
325
- assert.ok(
326
- hit!.message.includes(files['personas.ts']!) &&
327
- hit!.message.includes(files['more-personas.ts']!),
328
- `expected both files named, got ${hit!.message}`
329
- )
330
- })
331
-
332
- // The CLI scaffolds personas of its own, and nobody adds a persona to a file
333
- // the next codegen run overwrites — so a generated declaration neither takes
334
- // the single slot nor collides with the app's own.
335
- test('a generated declaration neither claims the slot nor conflicts', async () => {
336
- const { state, criticals } = await inspectSources({
337
- 'pikku-personas.gen.ts': [
338
- "import { definePersonas } from '@pikku/core/persona'",
339
- "definePersonas({ dave: { name: 'Dave' } })",
340
- ].join('\n'),
341
- 'personas.ts': withRoles("definePersonas({ susan: { name: 'Susan' } })"),
342
- })
343
-
344
- assert.deepEqual(
345
- criticals.filter(
346
- (c) => c.code === ErrorCode.DUPLICATE_PERSONAS_DEFINITION
347
- ),
348
- [],
349
- `expected no duplicate critical, got ${JSON.stringify(criticals)}`
350
- )
351
- assert.deepEqual(state.personas.definitions.map((d) => d.id).sort(), [
352
- 'dave',
353
- 'susan',
354
- ])
355
- })
356
-
357
- // A half-read roles array typechecks, runs, and grants less than the source
358
- // says — so an unreadable one reads as absent.
359
- test('a computed roles array is not half-extracted', async () => {
360
- const { state } = await inspectSource(
361
- withRoles(
362
- 'const r = "buyer"',
363
- 'definePersonas({',
364
- " susan: { name: 'Susan', roles: ['buyer', r] },",
365
- '})'
366
- )
367
- )
368
- assert.deepEqual(state.personas.definitions[0]!.roles, [])
369
- })
370
- })
371
-
372
- describe('validatePersonaRoles', () => {
373
- test('a declared system role is accepted', async () => {
374
- const { criticals } = await inspectSource(
375
- withRoles(
376
- 'definePersonas({',
377
- " susan: { name: 'Susan', roles: ['buyer', 'admin'] },",
378
- '})'
379
- )
380
- )
381
- assert.deepEqual(criticals, [])
382
- })
383
-
384
- // The failure this prevents: an admin deletes 'invoicing-clerk' from the
385
- // console, and Susan goes on claiming to test it while 403ing everywhere.
386
- test('a role that is not declared fails the build', async () => {
387
- const { criticals } = await inspectSource(
388
- withRoles(
389
- 'definePersonas({',
390
- " susan: { name: 'Susan', roles: ['invoicing-clerk'] },",
391
- '})'
392
- )
393
- )
394
- const critical = criticals.find((c) => /invoicing-clerk/.test(c.message))
395
- assert.ok(critical, `expected a critical, got ${JSON.stringify(criticals)}`)
396
- assert.match(critical!.message, /may only name a system role/)
397
- assert.match(critical!.message, /admin, buyer/)
398
- })
399
-
400
- test('a persona with no roles at all is fine', async () => {
401
- const { criticals } = await inspectSource(
402
- withRoles('definePersonas({', " susan: { name: 'Susan' },", '})')
403
- )
404
- assert.deepEqual(criticals, [])
405
- })
406
- })
@@ -1,287 +0,0 @@
1
- import * as ts from 'typescript'
2
- import { DISPOSITIONS } from '@pikku/core/virtual-user'
3
- import { isRunnablePersona } from '@pikku/core/persona'
4
- import type { PersonaMeta, PersonaAccountMeta } from '@pikku/core/persona'
5
- import { ErrorCode } from '../error-codes.js'
6
- import { claimSingleDeclaration } from '../utils/single-declaration.js'
7
- import type { AddWiring, InspectorLogger } from '../types.js'
8
- import {
9
- booleanProperty,
10
- getProperty,
11
- numberProperty,
12
- stringArrayProperty,
13
- stringProperty,
14
- unwrapAs,
15
- } from '../utils/literal-properties.js'
16
-
17
- type Tuning = NonNullable<PersonaMeta['tuning']>
18
-
19
- /** A dial and the range outside which it is a mistake rather than a choice. */
20
- const RATES: [keyof Tuning, number, number][] = [
21
- ['temperature', 0, 2],
22
- ['repeatRate', 0, 1],
23
- ['reReadRate', 0, 1],
24
- ]
25
-
26
- /**
27
- * `{ moves, temperature, repeatRate, … }` — a disposition's dials, overridden.
28
- *
29
- * The ranges are checked here rather than at run time because these are the
30
- * kind of typo that does not throw: `repeatRate: 18` meaning 18% would silently
31
- * double every call the persona makes, and you would read the run as a product
32
- * bug.
33
- */
34
- const tuningProperty = (
35
- config: ts.ObjectLiteralExpression,
36
- onError: (message: string) => void
37
- ): Tuning | undefined => {
38
- const value = getProperty(config, 'tuning')
39
- if (!value || !ts.isObjectLiteralExpression(value)) {
40
- return undefined
41
- }
42
-
43
- const tuning: Tuning = {}
44
-
45
- const movesValue = getProperty(value, 'moves')
46
- if (movesValue && ts.isObjectLiteralExpression(movesValue)) {
47
- const moves: NonNullable<Tuning['moves']> = {}
48
- for (const move of ['continue', 'suspend', 'resume', 'abandon'] as const) {
49
- const weight = numberProperty(movesValue, move)
50
- if (weight === undefined) {
51
- continue
52
- }
53
- if (weight < 0) {
54
- onError(`tuning.moves.${move} must not be negative (got ${weight}).`)
55
- continue
56
- }
57
- moves[move] = weight
58
- }
59
- if (Object.keys(moves).length) {
60
- tuning.moves = moves
61
- }
62
- }
63
-
64
- for (const [name, min, max] of RATES) {
65
- const rate = numberProperty(value, name)
66
- if (rate === undefined) {
67
- continue
68
- }
69
- if (rate < min || rate > max) {
70
- onError(`tuning.${name} must be between ${min} and ${max} (got ${rate}).`)
71
- continue
72
- }
73
- Object.assign(tuning, { [name]: rate })
74
- }
75
-
76
- for (const flag of ['emptyMemory', 'readOnly', 'invertedOracle'] as const) {
77
- const declared = booleanProperty(value, flag)
78
- if (declared !== undefined) {
79
- tuning[flag] = declared
80
- }
81
- }
82
-
83
- const instructions = stringProperty(value, 'instructions')
84
- if (instructions !== undefined) {
85
- tuning.instructions = instructions
86
- }
87
-
88
- return Object.keys(tuning).length ? tuning : undefined
89
- }
90
-
91
- const accountProperty = (
92
- value: ts.Expression | undefined
93
- ): PersonaAccountMeta | undefined => {
94
- if (!value || !ts.isObjectLiteralExpression(value)) {
95
- return undefined
96
- }
97
- const provider = stringProperty(value, 'provider')
98
- return provider === undefined ? {} : { provider }
99
- }
100
-
101
- const linkedAccountsProperty = (
102
- config: ts.ObjectLiteralExpression,
103
- personaId: string,
104
- logger: InspectorLogger
105
- ): Record<string, PersonaAccountMeta> | undefined => {
106
- const value = getProperty(config, 'linkedAccounts')
107
- if (!value || !ts.isObjectLiteralExpression(value)) {
108
- return undefined
109
- }
110
-
111
- const accounts: Record<string, PersonaAccountMeta> = {}
112
- for (const prop of value.properties) {
113
- if (!ts.isPropertyAssignment(prop)) {
114
- continue
115
- }
116
- if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
117
- logger.critical(
118
- ErrorCode.NON_LITERAL_WIRE_NAME,
119
- `Persona '${personaId}' has a linked account whose key is not a literal.`
120
- )
121
- continue
122
- }
123
- const account = accountProperty(prop.initializer)
124
- if (!account) {
125
- logger.critical(
126
- ErrorCode.INVALID_VALUE,
127
- `Persona '${personaId}' linked account '${prop.name.text}' must be an object literal.`
128
- )
129
- continue
130
- }
131
- accounts[prop.name.text] = account
132
- }
133
-
134
- return Object.keys(accounts).length ? accounts : undefined
135
- }
136
-
137
- /**
138
- * Inspector for `definePersonas()` calls.
139
- *
140
- * A persona is entirely declaration — a person, their roles, what they want —
141
- * so there is no code body and nothing to resolve at runtime. What is read here
142
- * is the whole thing, and it is generated straight to `personas.gen.json` for
143
- * the CLI and the console to read without loading the app.
144
- *
145
- * `name` is the one field that cannot be defaulted. A persona without one is
146
- * not a person, and the console has nothing to show.
147
- */
148
- export const addPersonas: AddWiring = (logger, node, _checker, state) => {
149
- if (!ts.isCallExpression(node)) {
150
- return
151
- }
152
-
153
- const expression = node.expression
154
- if (!ts.isIdentifier(expression) || expression.text !== 'definePersonas') {
155
- return
156
- }
157
-
158
- const firstArg = node.arguments[0]
159
- if (!firstArg) {
160
- return
161
- }
162
-
163
- const unwrapped = unwrapAs(firstArg)
164
- if (!ts.isObjectLiteralExpression(unwrapped)) {
165
- logger.critical(
166
- ErrorCode.INVALID_VALUE,
167
- 'definePersonas() needs an inline object — personas are read statically, never evaluated.'
168
- )
169
- return
170
- }
171
-
172
- const sourceFile = node.getSourceFile().fileName
173
-
174
- if (
175
- !claimSingleDeclaration(
176
- logger,
177
- state.personas.files,
178
- ErrorCode.DUPLICATE_PERSONAS_DEFINITION,
179
- 'definePersonas',
180
- sourceFile
181
- )
182
- ) {
183
- return
184
- }
185
-
186
- const known = Object.keys(DISPOSITIONS)
187
-
188
- for (const prop of unwrapped.properties) {
189
- if (!ts.isPropertyAssignment(prop)) {
190
- continue
191
- }
192
-
193
- if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
194
- logger.critical(
195
- ErrorCode.NON_LITERAL_WIRE_NAME,
196
- 'A persona is declared with a key that is not a literal.'
197
- )
198
- continue
199
- }
200
- const id = prop.name.text
201
-
202
- if (!ts.isObjectLiteralExpression(prop.initializer)) {
203
- logger.critical(
204
- ErrorCode.INVALID_VALUE,
205
- `Persona '${id}' must be an object literal.`
206
- )
207
- continue
208
- }
209
- const config = prop.initializer
210
-
211
- const name = stringProperty(config, 'name')
212
- if (!name) {
213
- logger.critical(
214
- ErrorCode.INVALID_VALUE,
215
- `Persona '${id}' needs a literal 'name' — a persona is a person, and the console has nothing to show without one.`
216
- )
217
- continue
218
- }
219
-
220
- // The generated `definePersonas` types this as a union, so a bad value only
221
- // reaches here in source that does not compile. Caught anyway: a
222
- // disposition is the entire behaviour, and defaulting a typo to `realistic`
223
- // would run a different person than the one that was written.
224
- const disposition = stringProperty(config, 'disposition')
225
- if (disposition !== undefined && !known.includes(disposition)) {
226
- logger.critical(
227
- ErrorCode.INVALID_VALUE,
228
- `Persona '${id}' has an unknown disposition '${disposition}'. One of: ${known.join(', ')}`
229
- )
230
- continue
231
- }
232
-
233
- let tuningFailed = false
234
- const tuning = tuningProperty(config, (message) => {
235
- tuningFailed = true
236
- logger.critical(ErrorCode.INVALID_VALUE, `Persona '${id}': ${message}`)
237
- })
238
- if (tuningFailed) {
239
- continue
240
- }
241
-
242
- const account = accountProperty(getProperty(config, 'account'))
243
- const declaredRunnable = booleanProperty(config, 'runnable')
244
-
245
- const persona: PersonaMeta = {
246
- id,
247
- name,
248
- roles: stringArrayProperty(config, 'roles') ?? [],
249
- goals: stringArrayProperty(config, 'goals') ?? [],
250
- tags: stringArrayProperty(config, 'tags') ?? [],
251
- runnable: isRunnablePersona({
252
- ...(declaredRunnable !== undefined
253
- ? { runnable: declaredRunnable }
254
- : {}),
255
- ...(account ? { account } : {}),
256
- }),
257
- sourceFile,
258
- }
259
-
260
- const jobTitle = stringProperty(config, 'jobTitle')
261
- if (jobTitle !== undefined) persona.jobTitle = jobTitle
262
- const description = stringProperty(config, 'description')
263
- if (description !== undefined) persona.description = description
264
- const avatarUrl = stringProperty(config, 'avatarUrl')
265
- if (avatarUrl !== undefined) persona.avatarUrl = avatarUrl
266
- const personality = stringProperty(config, 'personality')
267
- if (personality !== undefined) persona.personality = personality
268
- const app = stringProperty(config, 'app')
269
- if (app !== undefined) persona.app = app
270
- if (disposition !== undefined) {
271
- persona.disposition = disposition as PersonaMeta['disposition']
272
- }
273
- if (tuning !== undefined) persona.tuning = tuning
274
- const fixtures = stringArrayProperty(config, 'fixtures')
275
- if (fixtures !== undefined) persona.fixtures = fixtures
276
- // Left absent rather than resolved here: "everywhere but production" is a
277
- // fact about the config, and the inspector has no business baking today's
278
- // environment list into a meta file that outlives it.
279
- const environments = stringArrayProperty(config, 'environments')
280
- if (environments !== undefined) persona.environments = environments
281
- if (account !== undefined) persona.account = account
282
- const linkedAccounts = linkedAccountsProperty(config, id, logger)
283
- if (linkedAccounts !== undefined) persona.linkedAccounts = linkedAccounts
284
-
285
- state.personas.definitions.push(persona)
286
- }
287
- }