@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,282 @@
1
+ ---
2
+ title: Accounts-receivable industry bootstrap setup
3
+ summary: Auth as root, sign up + confirm a fresh industry-admin user, create an accounts-receivable tenant + datalake. Shared by every accounts-receivable scenario cookbook.
4
+ industry: accounts_receivable
5
+ slug: accounts_receivable
6
+ vitest_source:
7
+ - integration-tests/tests/accounts_receivable/bootstrap.test.ts
8
+ status: green
9
+ ---
10
+
11
+ # Problem
12
+
13
+ Every accounts-receivable scenario cookbook needs the same
14
+ starting point: a fresh tenant on the platform with an
15
+ accounts-receivable-domain datalake attached, accessed by an
16
+ industry-admin user. Authoring this prelude inline inside every
17
+ scenario cookbook would duplicate the auth + tenant + datalake
18
+ chain across two cookbooks (and six more across the other
19
+ industries). The validator auto-discovers this file from each
20
+ cookbook's `industry: accounts_receivable` front-matter and
21
+ inlines its numbered steps ahead of the scenario's own, so the
22
+ generated bun:test spec is hermetic without the cookbook author
23
+ having to copy-paste this prelude.
24
+
25
+ # Composition
26
+
27
+ | Resource provisioned | Owner |
28
+ |-------------------------------|-------------|
29
+ | Root session | setup |
30
+ | Industry-admin user (`sarah`) | setup |
31
+ | Accounts-receivable tenant | setup |
32
+ | Accounts-receivable 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 accounts-receivable 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
+ accounts-receivable 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 Accounts Receivable ${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 accounts-receivable datalake
114
+
115
+ Provision an accounts-receivable-domain datalake on the new
116
+ tenant. The DB schemas are scoped to the cookbook run via
117
+ `runSuffix` so parallel cookbook runs do not collide on schema
118
+ names. Local-dev defaults (`postgres` on `localhost:5432`,
119
+ `alvera_dev_accounts_receivable`) match the seeded `dev.exs`
120
+ setup; LocalStack S3 (`localhost:4566`) serves the unregulated +
121
+ regulated buckets. The datalake's server-derived `slug` is
122
+ captured into `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_accounts_receivable'
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 Accounts Receivable Datalake ${runSuffix}`,
143
+ description: 'Accounts-receivable datalake provisioned by cookbook doctest.',
144
+ data_domain: 'accounts_receivable',
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: 'accounts-receivable-lake-unregulated' },
183
+ regulated_cloud_storage: { ...S3, bucket: 'accounts-receivable-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 customer + invoice tables and
217
+ their join tables), deploys PostgREST roles, and provisions the
218
+ service-account bot user. Downstream resource creates (tools,
219
+ workflows) only need `datalakeId` to be persisted (which it is
220
+ the moment §004 returns), but any scenario step that ingests into
221
+ the regulated/unregulated DBs or runs a dataset search requires
222
+ `status: 'ready'`. Polling here makes every accounts-receivable
223
+ scenario cookbook deterministic regardless of how long the cold
224
+ migration takes on a given host; the loop exits the moment the
225
+ datalake reaches `:ready`, so the 5-minute cap is paid only in
226
+ failure mode.
227
+
228
+ ```typescript
229
+ const READY_TIMEOUT_MS = 5 * 60_000
230
+ const READY_POLL_MS = 5_000
231
+ const deadline = Date.now() + READY_TIMEOUT_MS
232
+ let datalakeStatus: string | undefined
233
+ while (Date.now() < deadline) {
234
+ const { data } = await api.datalakes.get(tenantSlug, ctx.datalakeId)
235
+ datalakeStatus = data.status
236
+ if (datalakeStatus === 'ready') break
237
+ await new Promise((r) => setTimeout(r, READY_POLL_MS))
238
+ }
239
+ if (datalakeStatus !== 'ready') {
240
+ throw new Error(
241
+ `datalake did not reach :ready within ${READY_TIMEOUT_MS}ms (last status: ${datalakeStatus})`,
242
+ )
243
+ }
244
+ ```
245
+
246
+ # Rollback
247
+
248
+ The cookbook doctest harness does not currently tear down the
249
+ created tenant / datalake / user — each run mints fresh names via
250
+ `runSuffix` so reruns do not collide, and the seeded local DB is
251
+ cheap to reset (`mix ecto.reset` on the platform). When the
252
+ validator graduates to continuous integration at Milestone 18
253
+ (running against the mockoon-backed mock env at `ALVERA_ENV=mock`),
254
+ no real state is created at all.
255
+
256
+ # Outcome
257
+
258
+ After this setup runs, the closure-scoped slots are populated as:
259
+
260
+ - `api` — a tenant-scoped `PlatformApi` client authenticated as
261
+ the industry-admin user
262
+ - `tenantSlug` — server-derived slug of the fresh
263
+ accounts-receivable tenant
264
+ - `datalakeSlug` — server-derived slug of the fresh
265
+ accounts-receivable datalake
266
+ - `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
267
+ credentials (kept on `ctx` because no scenario cookbook needs
268
+ to re-authenticate by default)
269
+
270
+ Scenario cookbooks under `industry: accounts_receivable` start at
271
+ their own `§001` with these slots already populated.
272
+
273
+ # See also
274
+
275
+ - `.agent/datalakes.md` — datalake create body shape (regulated +
276
+ unregulated tier configuration)
277
+ - `.agent/AGENTS.md` § SDK auth + client construction — root vs
278
+ tenantless vs tenant-scoped session scopes
279
+ - `integration-tests/tests/accounts_receivable/bootstrap.test.ts` —
280
+ the green test these snippets are lifted from (the sibling
281
+ datalake creation and the sanity probes are out of scope for
282
+ this lean cookbook setup)
@@ -0,0 +1,277 @@
1
+ ---
2
+ title: Foundation industry bootstrap setup
3
+ summary: Auth as root, sign up + confirm a fresh industry-admin user, create a foundation tenant + datalake. Shared by every foundation scenario cookbook.
4
+ industry: foundation
5
+ slug: foundation
6
+ vitest_source:
7
+ - integration-tests/tests/foundation/bootstrap.test.ts
8
+ status: green
9
+ ---
10
+
11
+ # Problem
12
+
13
+ Every foundation scenario cookbook needs the same starting point:
14
+ a fresh tenant on the platform with a foundation-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 four more
18
+ across the other industries). The validator auto-discovers this
19
+ file from each cookbook's `industry: foundation` 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
+ | Foundation tenant | setup |
31
+ | Foundation 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 foundation 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
+ foundation 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 Foundation ${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 foundation datalake
112
+
113
+ Provision a foundation-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_foundation`) 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_foundation'
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 Foundation Datalake ${runSuffix}`,
140
+ description: 'Foundation datalake provisioned by cookbook doctest.',
141
+ data_domain: 'foundation',
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: 'foundation-lake-unregulated' },
180
+ regulated_cloud_storage: { ...S3, bucket: 'foundation-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, deploys PostgREST roles, and provisions the
214
+ service-account bot user. Downstream resource creates (tools,
215
+ workflows) only need `datalakeId` to be persisted (which it is
216
+ the moment §004 returns), but any scenario step that touches
217
+ the regulated/unregulated DBs or the dataset infrastructure
218
+ requires `status: 'ready'`. Polling here makes every foundation
219
+ scenario cookbook deterministic regardless of how long the cold
220
+ migration takes on a given host; the loop exits the moment the
221
+ datalake reaches `:ready`, so the 5-minute cap is paid only in
222
+ failure mode.
223
+
224
+ ```typescript
225
+ const READY_TIMEOUT_MS = 5 * 60_000
226
+ const READY_POLL_MS = 5_000
227
+ const deadline = Date.now() + READY_TIMEOUT_MS
228
+ let datalakeStatus: string | undefined
229
+ while (Date.now() < deadline) {
230
+ const { data } = await api.datalakes.get(tenantSlug, ctx.datalakeId)
231
+ datalakeStatus = data.status
232
+ if (datalakeStatus === 'ready') break
233
+ await new Promise((r) => setTimeout(r, READY_POLL_MS))
234
+ }
235
+ if (datalakeStatus !== 'ready') {
236
+ throw new Error(
237
+ `datalake did not reach :ready within ${READY_TIMEOUT_MS}ms (last status: ${datalakeStatus})`,
238
+ )
239
+ }
240
+ ```
241
+
242
+ # Rollback
243
+
244
+ The cookbook doctest harness does not currently tear down the
245
+ created tenant / datalake / user — each run mints fresh names via
246
+ `runSuffix` so reruns do not collide, and the seeded local DB is
247
+ cheap to reset (`mix ecto.reset` on the platform). When the
248
+ validator graduates to continuous integration at Milestone 18
249
+ (running against the mockoon-backed mock env at `ALVERA_ENV=mock`),
250
+ no real state is created at all.
251
+
252
+ # Outcome
253
+
254
+ After this setup runs, the closure-scoped slots are populated as:
255
+
256
+ - `api` — a tenant-scoped `PlatformApi` client authenticated as
257
+ the industry-admin user
258
+ - `tenantSlug` — server-derived slug of the fresh foundation
259
+ tenant
260
+ - `datalakeSlug` — server-derived slug of the fresh foundation
261
+ datalake
262
+ - `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
263
+ credentials (kept on `ctx` because no scenario cookbook needs
264
+ to re-authenticate by default)
265
+
266
+ Scenario cookbooks under `industry: foundation` start at their own
267
+ `§001` with these slots already populated.
268
+
269
+ # See also
270
+
271
+ - `.agent/datalakes.md` — datalake create body shape (regulated +
272
+ unregulated tier configuration)
273
+ - `.agent/AGENTS.md` § SDK auth + client construction — root vs
274
+ tenantless vs tenant-scoped session scopes
275
+ - `integration-tests/tests/foundation/bootstrap.test.ts` — the
276
+ green test these snippets are lifted from (the §7b sibling
277
+ datalake creation is out of scope for this lean cookbook setup)