@alvera-ai/platform-sdk 0.10.0-rc.2 → 0.10.0-rc.21

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 (55) hide show
  1. package/.agent/AGENTS.md +440 -0
  2. package/.agent/account_management.md +455 -0
  3. package/.agent/action_status_updaters.md +262 -0
  4. package/.agent/ai_agents.md +423 -0
  5. package/.agent/ai_sandbox.md +265 -0
  6. package/.agent/async.md +111 -0
  7. package/.agent/connected_apps.md +407 -0
  8. package/.agent/cookbook/_fixtures/README.md +99 -0
  9. package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_customer.liquid +32 -0
  10. package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_mdm.liquid +20 -0
  11. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_generic_table.liquid +33 -0
  12. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +88 -0
  13. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_mdm.liquid +48 -0
  14. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +47 -0
  15. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +24 -0
  16. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +38 -0
  17. package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid +59 -0
  18. package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_mdm.liquid +36 -0
  19. package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_mdm.liquid +30 -0
  20. package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_payment_account.liquid +55 -0
  21. package/.agent/cookbook/_setup/accounts_receivable.md +282 -0
  22. package/.agent/cookbook/_setup/foundation.md +277 -0
  23. package/.agent/cookbook/_setup/healthcare.md +279 -0
  24. package/.agent/cookbook/_setup/payment_risk.md +283 -0
  25. package/.agent/cookbook/appointment-review-sms-workflow.md +761 -0
  26. package/.agent/cookbook/birthday-greeting-sms-trigger.md +656 -0
  27. package/.agent/cookbook/contact-us-triage-with-llm.md +603 -0
  28. package/.agent/cookbook/dunning-sms-for-delinquent.md +619 -0
  29. package/.agent/cookbook/kyc-notification-on-account-activation.md +619 -0
  30. package/.agent/cookbook/sanctions-screening-with-agent-review.md +711 -0
  31. package/.agent/cookbook/score-leads-with-llm-categorization.md +602 -0
  32. package/.agent/cookbook/welcome-sms-for-customers.md +607 -0
  33. package/.agent/data_activation_clients.md +557 -0
  34. package/.agent/data_sources.md +234 -0
  35. package/.agent/datalakes.md +712 -0
  36. package/.agent/debugging.md +137 -0
  37. package/.agent/errors.md +196 -0
  38. package/.agent/generic_tables.md +351 -0
  39. package/.agent/interoperability_contracts.md +351 -0
  40. package/.agent/mdm.md +293 -0
  41. package/.agent/mutations.md +152 -0
  42. package/.agent/templates.md +98 -0
  43. package/.agent/tool-call-configs.md +90 -0
  44. package/.agent/tools.md +546 -0
  45. package/.agent/type_naming.md +131 -0
  46. package/.agent/workflows.md +601 -0
  47. package/README.md +46 -0
  48. package/dist/bin/platform-sdk.d.mts +1 -0
  49. package/dist/bin/platform-sdk.mjs +106 -0
  50. package/dist/bin/platform-sdk.mjs.map +1 -0
  51. package/dist/index.d.mts +1200 -43201
  52. package/dist/index.d.mts.map +1 -1
  53. package/dist/index.mjs +1859 -7319
  54. package/dist/index.mjs.map +1 -1
  55. package/package.json +19 -10
@@ -0,0 +1,279 @@
1
+ ---
2
+ title: Healthcare industry bootstrap setup
3
+ summary: Auth as root, sign up + confirm a fresh industry-admin user, create a healthcare tenant + datalake. Shared by every healthcare scenario cookbook.
4
+ industry: healthcare
5
+ slug: healthcare
6
+ vitest_source:
7
+ - integration-tests/tests/healthcare/bootstrap.test.ts
8
+ status: green
9
+ ---
10
+
11
+ # Problem
12
+
13
+ Every healthcare scenario cookbook needs the same starting point:
14
+ a fresh tenant on the platform with a healthcare-domain datalake
15
+ attached, accessed by an industry-admin user. Authoring this
16
+ prelude inline inside every scenario cookbook would duplicate the
17
+ auth + tenant + datalake chain across two cookbooks (and six more
18
+ across the other industries). The validator auto-discovers this
19
+ file from each cookbook's `industry: healthcare` front-matter and
20
+ inlines its numbered steps ahead of the scenario's own, so the
21
+ generated bun:test spec is hermetic without the cookbook author
22
+ having to copy-paste this prelude.
23
+
24
+ # Composition
25
+
26
+ | Resource provisioned | Owner |
27
+ |-------------------------------|-------------|
28
+ | Root session | setup |
29
+ | Industry-admin user (`sarah`) | setup |
30
+ | Healthcare tenant | setup |
31
+ | Healthcare datalake | setup |
32
+
33
+ # Walkthrough
34
+
35
+ ## 001 — root admin signs in and provisions a fresh industry-admin user
36
+
37
+ Authenticate as the platform's root admin (`admin@dev.local` /
38
+ `devpassword` in local dev). Then sign up a per-run industry-admin
39
+ user (`sarah`) under a unique email derived from `runSuffix`, and
40
+ confirm them so they can sign in. Both the signup and the confirm
41
+ endpoints require root scope.
42
+
43
+ ```typescript
44
+ ctx.rootSession = await createSession({
45
+ baseUrl: process.env.ALVERA_BASE_URL!,
46
+ email: process.env.ALVERA_ROOT_EMAIL!,
47
+ password: process.env.ALVERA_ROOT_PASSWORD!,
48
+ })
49
+ ctx.rootApi = createIsolatedPlatformApi({
50
+ baseUrl: process.env.ALVERA_BASE_URL!,
51
+ sessionToken: ctx.rootSession.sessionToken,
52
+ })
53
+
54
+ ctx.sarahEmail = `cookbook-sarah-${runSuffix}@dev.local`
55
+ ctx.sarahPassword = 'CookbookPass1!'
56
+
57
+ const signUpResp = await ctx.rootApi.auth.signUp({
58
+ email: ctx.sarahEmail,
59
+ password: ctx.sarahPassword,
60
+ first_name: 'Cookbook',
61
+ last_name: 'Sarah',
62
+ })
63
+ ctx.sarahUserId = signUpResp.data.id
64
+ await ctx.rootApi.admin.confirmUser(ctx.sarahUserId)
65
+ ```
66
+
67
+ ## 002 — sarah signs in (tenantless) and creates the healthcare tenant
68
+
69
+ Sarah signs in for the first time without a tenant scope (no
70
+ tenant exists yet for her), then immediately creates a fresh
71
+ healthcare tenant. The tenant's server-derived `slug` is captured
72
+ into the closure-scoped `tenantSlug` slot for downstream steps.
73
+
74
+ ```typescript
75
+ ctx.sarahTenantlessSession = await createSession({
76
+ baseUrl: process.env.ALVERA_BASE_URL!,
77
+ email: ctx.sarahEmail,
78
+ password: ctx.sarahPassword,
79
+ })
80
+ ctx.sarahTenantlessApi = createIsolatedPlatformApi({
81
+ baseUrl: process.env.ALVERA_BASE_URL!,
82
+ sessionToken: ctx.sarahTenantlessSession.sessionToken,
83
+ })
84
+
85
+ const tenantResp = await ctx.sarahTenantlessApi.tenants.create({
86
+ name: `Cookbook Healthcare ${runSuffix}`,
87
+ })
88
+ tenantSlug = tenantResp.data.slug!
89
+ ```
90
+
91
+ ## 003 — sarah signs in tenant-scoped — the canonical client
92
+
93
+ Re-mint sarah's session with the new tenant slug. This bearer is
94
+ the canonical tenant-scoped client every subsequent step uses; it
95
+ is assigned to the closure-scoped `api` slot so the scenario
96
+ cookbook's steps inherit it via inlining.
97
+
98
+ ```typescript
99
+ const sarahTenantSession = await createSession({
100
+ baseUrl: process.env.ALVERA_BASE_URL!,
101
+ email: ctx.sarahEmail,
102
+ password: ctx.sarahPassword,
103
+ tenantSlug,
104
+ })
105
+ api = createIsolatedPlatformApi({
106
+ baseUrl: process.env.ALVERA_BASE_URL!,
107
+ sessionToken: sarahTenantSession.sessionToken,
108
+ })
109
+ ```
110
+
111
+ ## 004 — sarah creates the healthcare datalake
112
+
113
+ Provision a healthcare-domain datalake on the new tenant. The DB
114
+ schemas are scoped to the cookbook run via `runSuffix` so parallel
115
+ cookbook runs do not collide on schema names. Local-dev defaults
116
+ (`postgres` on `localhost:5432`, `alvera_dev_healthcare`) match
117
+ the seeded `dev.exs` setup; LocalStack S3 (`localhost:4566`)
118
+ serves the unregulated + regulated buckets. The datalake's
119
+ server-derived `slug` is captured into `datalakeSlug`.
120
+
121
+ ```typescript
122
+ const DB_HOST = 'localhost'
123
+ const DB_PORT = 5432
124
+ const DB_USER = 'postgres'
125
+ const DB_PASS = 'postgres'
126
+ const DB_NAME = 'alvera_dev_healthcare'
127
+ const UNREG_SCHEMA = `cookbook_${runSuffix}_unreg`
128
+ const REG_SCHEMA = `cookbook_${runSuffix}_reg`
129
+
130
+ const S3 = {
131
+ cloud_storage_type: 'aws' as const,
132
+ region: 'us-east-1',
133
+ access_key_id: 'test',
134
+ secret_access_key: 'test',
135
+ endpoint: 'http://localhost:4566',
136
+ }
137
+
138
+ const datalakeResp = await api.datalakes.create(tenantSlug, {
139
+ name: `Cookbook Healthcare Datalake ${runSuffix}`,
140
+ description: 'Healthcare datalake provisioned by cookbook doctest.',
141
+ data_domain: 'healthcare',
142
+ timezone: 'America/New_York',
143
+ pool_size: 5,
144
+
145
+ unregulated_db_writer_host: DB_HOST,
146
+ unregulated_db_writer_port: DB_PORT,
147
+ unregulated_db_writer_name: DB_NAME,
148
+ unregulated_db_writer_schema: UNREG_SCHEMA,
149
+ unregulated_db_writer_auth_method: 'password',
150
+ unregulated_db_writer_user: DB_USER,
151
+ unregulated_db_writer_pass: DB_PASS,
152
+ unregulated_db_writer_enable_ssl: false,
153
+ unregulated_db_reader_host: DB_HOST,
154
+ unregulated_db_reader_port: DB_PORT,
155
+ unregulated_db_reader_name: DB_NAME,
156
+ unregulated_db_reader_schema: UNREG_SCHEMA,
157
+ unregulated_db_reader_auth_method: 'password',
158
+ unregulated_db_reader_user: DB_USER,
159
+ unregulated_db_reader_pass: DB_PASS,
160
+ unregulated_db_reader_enable_ssl: false,
161
+
162
+ regulated_data_db_writer_host: DB_HOST,
163
+ regulated_data_db_writer_port: DB_PORT,
164
+ regulated_data_db_writer_name: DB_NAME,
165
+ regulated_data_db_writer_schema: REG_SCHEMA,
166
+ regulated_data_db_writer_auth_method: 'password',
167
+ regulated_data_db_writer_user: DB_USER,
168
+ regulated_data_db_writer_pass: DB_PASS,
169
+ regulated_data_db_writer_enable_ssl: false,
170
+ regulated_data_db_reader_host: DB_HOST,
171
+ regulated_data_db_reader_port: DB_PORT,
172
+ regulated_data_db_reader_name: DB_NAME,
173
+ regulated_data_db_reader_schema: REG_SCHEMA,
174
+ regulated_data_db_reader_auth_method: 'password',
175
+ regulated_data_db_reader_user: DB_USER,
176
+ regulated_data_db_reader_pass: DB_PASS,
177
+ regulated_data_db_reader_enable_ssl: false,
178
+
179
+ unregulated_cloud_storage: { ...S3, bucket: 'healthcare-lake-unregulated' },
180
+ regulated_cloud_storage: { ...S3, bucket: 'healthcare-lake-regulated' },
181
+ })
182
+ datalakeSlug = datalakeResp.data.slug!
183
+ // `tools.create` and a few other endpoints accept the datalake by
184
+ // UUID in the request body rather than only the slug in the URL,
185
+ // so keep it on `ctx` for downstream steps that need it.
186
+ ctx.datalakeId = datalakeResp.data.id!
187
+ ```
188
+
189
+ ## 005 — enqueue the datalake migrations
190
+
191
+ `datalakes.create` persists the datalake row in `:new` status but
192
+ does not itself run the schema migrations. Migration is a
193
+ separately-triggered async job so the operator controls when the
194
+ (potentially slow) per-industry DDL runs. `datalakes.migrate`
195
+ enqueues the `DatalakeMigrationWorker` Oban job and returns
196
+ immediately with `status: 'enqueued'` plus the Oban `job_id`. The
197
+ poll in §006 then waits for that worker to finish — without this
198
+ call the datalake would sit at `:new` forever.
199
+
200
+ ```typescript
201
+ const migrateResp = await api.datalakes.migrate(tenantSlug, datalakeSlug)
202
+ if (migrateResp.data.status !== 'enqueued') {
203
+ throw new Error(
204
+ `datalake migration not enqueued (status: ${migrateResp.data.status})`,
205
+ )
206
+ }
207
+ ```
208
+
209
+ ## 006 — poll the datalake until status is ready
210
+
211
+ Datalake migrations are async — the §005 migrate call enqueued a
212
+ `DatalakeMigrationWorker` Oban job that runs the per-industry
213
+ schema migrations (the regulated FHIR R4 patient + appointment
214
+ tables and their join tables), deploys PostgREST roles, and
215
+ provisions the service-account bot user. Downstream resource
216
+ creates (tools, workflows) only need `datalakeId` to be persisted
217
+ (which it is the moment §004 returns), but any scenario step that
218
+ ingests into the regulated/unregulated DBs or runs a dataset
219
+ search requires `status: 'ready'`. Polling here makes every
220
+ healthcare scenario cookbook deterministic regardless of how long
221
+ the cold migration takes on a given host; the loop exits the
222
+ moment the datalake reaches `:ready`, so the 5-minute cap is paid
223
+ only in failure mode.
224
+
225
+ ```typescript
226
+ const READY_TIMEOUT_MS = 5 * 60_000
227
+ const READY_POLL_MS = 5_000
228
+ const deadline = Date.now() + READY_TIMEOUT_MS
229
+ let datalakeStatus: string | undefined
230
+ while (Date.now() < deadline) {
231
+ const { data } = await api.datalakes.get(tenantSlug, ctx.datalakeId)
232
+ datalakeStatus = data.status
233
+ if (datalakeStatus === 'ready') break
234
+ await new Promise((r) => setTimeout(r, READY_POLL_MS))
235
+ }
236
+ if (datalakeStatus !== 'ready') {
237
+ throw new Error(
238
+ `datalake did not reach :ready within ${READY_TIMEOUT_MS}ms (last status: ${datalakeStatus})`,
239
+ )
240
+ }
241
+ ```
242
+
243
+ # Rollback
244
+
245
+ The cookbook doctest harness does not currently tear down the
246
+ created tenant / datalake / user — each run mints fresh names via
247
+ `runSuffix` so reruns do not collide, and the seeded local DB is
248
+ cheap to reset (`mix ecto.reset` on the platform). When the
249
+ validator graduates to continuous integration at Milestone 18
250
+ (running against the mockoon-backed mock env at `ALVERA_ENV=mock`),
251
+ no real state is created at all.
252
+
253
+ # Outcome
254
+
255
+ After this setup runs, the closure-scoped slots are populated as:
256
+
257
+ - `api` — a tenant-scoped `PlatformApi` client authenticated as
258
+ the industry-admin user
259
+ - `tenantSlug` — server-derived slug of the fresh healthcare
260
+ tenant
261
+ - `datalakeSlug` — server-derived slug of the fresh healthcare
262
+ datalake
263
+ - `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
264
+ credentials (kept on `ctx` because no scenario cookbook needs
265
+ to re-authenticate by default)
266
+
267
+ Scenario cookbooks under `industry: healthcare` start at their own
268
+ `§001` with these slots already populated.
269
+
270
+ # See also
271
+
272
+ - `.agent/datalakes.md` — datalake create body shape (regulated +
273
+ unregulated tier configuration)
274
+ - `.agent/AGENTS.md` § SDK auth + client construction — root vs
275
+ tenantless vs tenant-scoped session scopes
276
+ - `integration-tests/tests/healthcare/bootstrap.test.ts` — the
277
+ green test these snippets are lifted from (the §7b sibling
278
+ datalake creation and the §10–§13 sanity probes are out of
279
+ scope for this lean cookbook setup)
@@ -0,0 +1,283 @@
1
+ ---
2
+ title: Payment-risk industry bootstrap setup
3
+ summary: Auth as root, sign up + confirm a fresh industry-admin user, create a payment-risk tenant + datalake. Shared by every payment-risk scenario cookbook.
4
+ industry: payment_risk
5
+ slug: payment_risk
6
+ vitest_source:
7
+ - integration-tests/tests/payment_risk/bootstrap.test.ts
8
+ status: green
9
+ ---
10
+
11
+ # Problem
12
+
13
+ Every payment-risk scenario cookbook needs the same starting
14
+ point: a fresh tenant on the platform with a payment-risk-domain
15
+ datalake attached, accessed by an industry-admin user. Authoring
16
+ this prelude inline inside every scenario cookbook would
17
+ duplicate the auth + tenant + datalake chain across two cookbooks
18
+ (and six more across the other industries). The validator
19
+ auto-discovers this file from each cookbook's
20
+ `industry: payment_risk` front-matter and inlines its numbered
21
+ steps ahead of the scenario's own, so the generated bun:test spec
22
+ is hermetic without the cookbook author having to copy-paste this
23
+ prelude.
24
+
25
+ # Composition
26
+
27
+ | Resource provisioned | Owner |
28
+ |-------------------------------|-------------|
29
+ | Root session | setup |
30
+ | Industry-admin user (`sarah`) | setup |
31
+ | Payment-risk tenant | setup |
32
+ | Payment-risk datalake | setup |
33
+
34
+ # Walkthrough
35
+
36
+ ## 001 — root admin signs in and provisions a fresh industry-admin user
37
+
38
+ Authenticate as the platform's root admin (`admin@dev.local` /
39
+ `devpassword` in local dev). Then sign up a per-run industry-admin
40
+ user (`sarah`) under a unique email derived from `runSuffix`, and
41
+ confirm them so they can sign in. Both the signup and the confirm
42
+ endpoints require root scope.
43
+
44
+ ```typescript
45
+ ctx.rootSession = await createSession({
46
+ baseUrl: process.env.ALVERA_BASE_URL!,
47
+ email: process.env.ALVERA_ROOT_EMAIL!,
48
+ password: process.env.ALVERA_ROOT_PASSWORD!,
49
+ })
50
+ ctx.rootApi = createIsolatedPlatformApi({
51
+ baseUrl: process.env.ALVERA_BASE_URL!,
52
+ sessionToken: ctx.rootSession.sessionToken,
53
+ })
54
+
55
+ ctx.sarahEmail = `cookbook-sarah-${runSuffix}@dev.local`
56
+ ctx.sarahPassword = 'CookbookPass1!'
57
+
58
+ const signUpResp = await ctx.rootApi.auth.signUp({
59
+ email: ctx.sarahEmail,
60
+ password: ctx.sarahPassword,
61
+ first_name: 'Cookbook',
62
+ last_name: 'Sarah',
63
+ })
64
+ ctx.sarahUserId = signUpResp.data.id
65
+ await ctx.rootApi.admin.confirmUser(ctx.sarahUserId)
66
+ ```
67
+
68
+ ## 002 — sarah signs in (tenantless) and creates the payment-risk tenant
69
+
70
+ Sarah signs in for the first time without a tenant scope (no
71
+ tenant exists yet for her), then immediately creates a fresh
72
+ payment-risk tenant. The tenant's server-derived `slug` is
73
+ captured into the closure-scoped `tenantSlug` slot for downstream
74
+ steps.
75
+
76
+ ```typescript
77
+ ctx.sarahTenantlessSession = await createSession({
78
+ baseUrl: process.env.ALVERA_BASE_URL!,
79
+ email: ctx.sarahEmail,
80
+ password: ctx.sarahPassword,
81
+ })
82
+ ctx.sarahTenantlessApi = createIsolatedPlatformApi({
83
+ baseUrl: process.env.ALVERA_BASE_URL!,
84
+ sessionToken: ctx.sarahTenantlessSession.sessionToken,
85
+ })
86
+
87
+ const tenantResp = await ctx.sarahTenantlessApi.tenants.create({
88
+ name: `Cookbook Payment Risk ${runSuffix}`,
89
+ })
90
+ tenantSlug = tenantResp.data.slug!
91
+ ```
92
+
93
+ ## 003 — sarah signs in tenant-scoped — the canonical client
94
+
95
+ Re-mint sarah's session with the new tenant slug. This bearer is
96
+ the canonical tenant-scoped client every subsequent step uses; it
97
+ is assigned to the closure-scoped `api` slot so the scenario
98
+ cookbook's steps inherit it via inlining.
99
+
100
+ ```typescript
101
+ const sarahTenantSession = await createSession({
102
+ baseUrl: process.env.ALVERA_BASE_URL!,
103
+ email: ctx.sarahEmail,
104
+ password: ctx.sarahPassword,
105
+ tenantSlug,
106
+ })
107
+ api = createIsolatedPlatformApi({
108
+ baseUrl: process.env.ALVERA_BASE_URL!,
109
+ sessionToken: sarahTenantSession.sessionToken,
110
+ })
111
+ ```
112
+
113
+ ## 004 — sarah creates the payment-risk datalake
114
+
115
+ Provision a payment-risk-domain datalake on the new tenant. The
116
+ DB schemas are scoped to the cookbook run via `runSuffix` so
117
+ parallel cookbook runs do not collide on schema names. Local-dev
118
+ defaults (`postgres` on `localhost:5432`, `alvera_dev_payment_risk`)
119
+ match the seeded `dev.exs` setup; LocalStack S3
120
+ (`localhost:4566`) serves the unregulated + regulated buckets.
121
+ The datalake's server-derived `slug` is captured into
122
+ `datalakeSlug`.
123
+
124
+ ```typescript
125
+ const DB_HOST = 'localhost'
126
+ const DB_PORT = 5432
127
+ const DB_USER = 'postgres'
128
+ const DB_PASS = 'postgres'
129
+ const DB_NAME = 'alvera_dev_payment_risk'
130
+ const UNREG_SCHEMA = `cookbook_${runSuffix}_unreg`
131
+ const REG_SCHEMA = `cookbook_${runSuffix}_reg`
132
+
133
+ const S3 = {
134
+ cloud_storage_type: 'aws' as const,
135
+ region: 'us-east-1',
136
+ access_key_id: 'test',
137
+ secret_access_key: 'test',
138
+ endpoint: 'http://localhost:4566',
139
+ }
140
+
141
+ const datalakeResp = await api.datalakes.create(tenantSlug, {
142
+ name: `Cookbook Payment Risk Datalake ${runSuffix}`,
143
+ description: 'Payment-risk datalake provisioned by cookbook doctest.',
144
+ data_domain: 'payment_risk',
145
+ timezone: 'America/New_York',
146
+ pool_size: 5,
147
+
148
+ unregulated_db_writer_host: DB_HOST,
149
+ unregulated_db_writer_port: DB_PORT,
150
+ unregulated_db_writer_name: DB_NAME,
151
+ unregulated_db_writer_schema: UNREG_SCHEMA,
152
+ unregulated_db_writer_auth_method: 'password',
153
+ unregulated_db_writer_user: DB_USER,
154
+ unregulated_db_writer_pass: DB_PASS,
155
+ unregulated_db_writer_enable_ssl: false,
156
+ unregulated_db_reader_host: DB_HOST,
157
+ unregulated_db_reader_port: DB_PORT,
158
+ unregulated_db_reader_name: DB_NAME,
159
+ unregulated_db_reader_schema: UNREG_SCHEMA,
160
+ unregulated_db_reader_auth_method: 'password',
161
+ unregulated_db_reader_user: DB_USER,
162
+ unregulated_db_reader_pass: DB_PASS,
163
+ unregulated_db_reader_enable_ssl: false,
164
+
165
+ regulated_data_db_writer_host: DB_HOST,
166
+ regulated_data_db_writer_port: DB_PORT,
167
+ regulated_data_db_writer_name: DB_NAME,
168
+ regulated_data_db_writer_schema: REG_SCHEMA,
169
+ regulated_data_db_writer_auth_method: 'password',
170
+ regulated_data_db_writer_user: DB_USER,
171
+ regulated_data_db_writer_pass: DB_PASS,
172
+ regulated_data_db_writer_enable_ssl: false,
173
+ regulated_data_db_reader_host: DB_HOST,
174
+ regulated_data_db_reader_port: DB_PORT,
175
+ regulated_data_db_reader_name: DB_NAME,
176
+ regulated_data_db_reader_schema: REG_SCHEMA,
177
+ regulated_data_db_reader_auth_method: 'password',
178
+ regulated_data_db_reader_user: DB_USER,
179
+ regulated_data_db_reader_pass: DB_PASS,
180
+ regulated_data_db_reader_enable_ssl: false,
181
+
182
+ unregulated_cloud_storage: { ...S3, bucket: 'payment-risk-lake-unregulated' },
183
+ regulated_cloud_storage: { ...S3, bucket: 'payment-risk-lake-regulated' },
184
+ })
185
+ datalakeSlug = datalakeResp.data.slug!
186
+ // `tools.create` and a few other endpoints accept the datalake by
187
+ // UUID in the request body rather than only the slug in the URL,
188
+ // so keep it on `ctx` for downstream steps that need it.
189
+ ctx.datalakeId = datalakeResp.data.id!
190
+ ```
191
+
192
+ ## 005 — enqueue the datalake migrations
193
+
194
+ `datalakes.create` persists the datalake row in `:new` status but
195
+ does not itself run the schema migrations. Migration is a
196
+ separately-triggered async job so the operator controls when the
197
+ (potentially slow) per-industry DDL runs. `datalakes.migrate`
198
+ enqueues the `DatalakeMigrationWorker` Oban job and returns
199
+ immediately with `status: 'enqueued'` plus the Oban `job_id`. The
200
+ poll in §006 then waits for that worker to finish — without this
201
+ call the datalake would sit at `:new` forever.
202
+
203
+ ```typescript
204
+ const migrateResp = await api.datalakes.migrate(tenantSlug, datalakeSlug)
205
+ if (migrateResp.data.status !== 'enqueued') {
206
+ throw new Error(
207
+ `datalake migration not enqueued (status: ${migrateResp.data.status})`,
208
+ )
209
+ }
210
+ ```
211
+
212
+ ## 006 — poll the datalake until status is ready
213
+
214
+ Datalake migrations are async — the §005 migrate call enqueued a
215
+ `DatalakeMigrationWorker` Oban job that runs the per-industry
216
+ schema migrations (the regulated payment-account, compliance-
217
+ screening, and legal-entity tables and their join tables),
218
+ deploys PostgREST roles, and provisions the service-account bot
219
+ user. Downstream resource creates (tools, workflows) only need
220
+ `datalakeId` to be persisted (which it is the moment §004
221
+ returns), but any scenario step that ingests into the
222
+ regulated/unregulated DBs or runs a dataset search requires
223
+ `status: 'ready'`. Polling here makes every payment-risk scenario
224
+ cookbook deterministic regardless of how long the cold migration
225
+ takes on a given host; the loop exits the moment the datalake
226
+ reaches `:ready`, so the 5-minute cap is paid only in failure
227
+ mode.
228
+
229
+ ```typescript
230
+ const READY_TIMEOUT_MS = 5 * 60_000
231
+ const READY_POLL_MS = 5_000
232
+ const deadline = Date.now() + READY_TIMEOUT_MS
233
+ let datalakeStatus: string | undefined
234
+ while (Date.now() < deadline) {
235
+ const { data } = await api.datalakes.get(tenantSlug, ctx.datalakeId)
236
+ datalakeStatus = data.status
237
+ if (datalakeStatus === 'ready') break
238
+ await new Promise((r) => setTimeout(r, READY_POLL_MS))
239
+ }
240
+ if (datalakeStatus !== 'ready') {
241
+ throw new Error(
242
+ `datalake did not reach :ready within ${READY_TIMEOUT_MS}ms (last status: ${datalakeStatus})`,
243
+ )
244
+ }
245
+ ```
246
+
247
+ # Rollback
248
+
249
+ The cookbook doctest harness does not currently tear down the
250
+ created tenant / datalake / user — each run mints fresh names via
251
+ `runSuffix` so reruns do not collide, and the seeded local DB is
252
+ cheap to reset (`mix ecto.reset` on the platform). When the
253
+ validator graduates to continuous integration at Milestone 18
254
+ (running against the mockoon-backed mock env at `ALVERA_ENV=mock`),
255
+ no real state is created at all.
256
+
257
+ # Outcome
258
+
259
+ After this setup runs, the closure-scoped slots are populated as:
260
+
261
+ - `api` — a tenant-scoped `PlatformApi` client authenticated as
262
+ the industry-admin user
263
+ - `tenantSlug` — server-derived slug of the fresh payment-risk
264
+ tenant
265
+ - `datalakeSlug` — server-derived slug of the fresh payment-risk
266
+ datalake
267
+ - `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
268
+ credentials (kept on `ctx` because no scenario cookbook needs
269
+ to re-authenticate by default)
270
+
271
+ Scenario cookbooks under `industry: payment_risk` start at their
272
+ own `§001` with these slots already populated.
273
+
274
+ # See also
275
+
276
+ - `.agent/datalakes.md` — datalake create body shape (regulated +
277
+ unregulated tier configuration)
278
+ - `.agent/AGENTS.md` § SDK auth + client construction — root vs
279
+ tenantless vs tenant-scoped session scopes
280
+ - `integration-tests/tests/payment_risk/bootstrap.test.ts` — the
281
+ green test these snippets are lifted from (the sibling datalake
282
+ creation and the sanity probes are out of scope for this lean
283
+ cookbook setup)