@alvera-ai/platform-sdk 0.17.0 → 0.18.0-next.g49897f5

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 (84) hide show
  1. package/.agent/AGENTS.md +91 -144
  2. package/.agent/account_management.md +2 -2
  3. package/.agent/action_logs.md +4 -4
  4. package/.agent/advanced_migrations.md +202 -0
  5. package/.agent/ai_agents.md +28 -21
  6. package/.agent/ai_sandbox.md +49 -39
  7. package/.agent/connected_apps.md +3 -3
  8. package/.agent/cookbook/_fixtures/README.md +1 -1
  9. package/.agent/cookbook/_fixtures/{foundation → organic-marketing}/_lead_submissions_foundation_generic_table.liquid +1 -1
  10. package/.agent/cookbook/_fixtures/organic-marketing/_lead_submissions_foundation_legal_entity.liquid +80 -0
  11. package/.agent/cookbook/_fixtures/{foundation → organic-marketing}/_lead_submissions_foundation_mdm.liquid +2 -1
  12. package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_generic_table.liquid +57 -0
  13. package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_legal_entity.liquid +30 -0
  14. package/.agent/cookbook/_fixtures/payments-compliance/_compliance_screenings_mdm.liquid +44 -0
  15. package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_generic_table.liquid +57 -0
  16. package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_legal_entity.liquid +36 -0
  17. package/.agent/cookbook/_fixtures/payments-compliance/_payment_accounts_mdm.liquid +41 -0
  18. package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_generic_table.liquid +70 -0
  19. package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_legal_entity.liquid +52 -0
  20. package/.agent/cookbook/_fixtures/primary-care-feedback/_cahps_appointments_mdm.liquid +42 -0
  21. package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_generic_table.liquid +38 -0
  22. package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_legal_entity.liquid +48 -0
  23. package/.agent/cookbook/_fixtures/subscription-saas/_customers_subscription_mdm.liquid +49 -0
  24. package/.agent/cookbook/organic-marketing.md +2801 -0
  25. package/.agent/cookbook/payments-compliance.md +2180 -0
  26. package/.agent/cookbook/primary-care.md +2175 -0
  27. package/.agent/cookbook/subscription-saas.md +2403 -0
  28. package/.agent/data_activation_clients.md +65 -52
  29. package/.agent/datalakes.md +407 -171
  30. package/.agent/errors.md +3 -3
  31. package/.agent/generic_tables.md +183 -62
  32. package/.agent/interoperability_contracts.md +57 -22
  33. package/.agent/mdm.md +136 -153
  34. package/.agent/messages.md +36 -34
  35. package/.agent/mock-services.md +13 -12
  36. package/.agent/mutations.md +2 -2
  37. package/.agent/templates.md +14 -13
  38. package/.agent/tools.md +63 -21
  39. package/.agent/type_naming.md +13 -13
  40. package/.agent/workflows.md +99 -53
  41. package/README.md +2 -2
  42. package/dist/bin/platform-sdk.mjs +33 -47
  43. package/dist/bin/platform-sdk.mjs.map +1 -1
  44. package/dist/index.d.mts +619 -385
  45. package/dist/index.d.mts.map +1 -1
  46. package/dist/index.mjs +535 -59
  47. package/dist/index.mjs.map +1 -1
  48. package/package.json +4 -3
  49. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +0 -88
  50. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +0 -47
  51. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +0 -24
  52. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +0 -38
  53. package/.agent/cookbook/_fixtures/payments/_compliance_screenings_payments_compliance_screening.liquid +0 -59
  54. package/.agent/cookbook/_fixtures/payments/_compliance_screenings_payments_mdm.liquid +0 -36
  55. package/.agent/cookbook/_fixtures/payments/_payment_accounts_payments_mdm.liquid +0 -30
  56. package/.agent/cookbook/_fixtures/payments/_payment_accounts_payments_payment_account.liquid +0 -55
  57. package/.agent/cookbook/_fixtures/subscription/_customers_subscription_mdm.liquid +0 -20
  58. package/.agent/cookbook/_setup/foundation.md +0 -359
  59. package/.agent/cookbook/_setup/healthcare.md +0 -361
  60. package/.agent/cookbook/_setup/payments.md +0 -365
  61. package/.agent/cookbook/_setup/subscription.md +0 -364
  62. package/.agent/cookbook/action-status-updaters.md +0 -278
  63. package/.agent/cookbook/ai-agent-invoke.md +0 -279
  64. package/.agent/cookbook/appointment-review-sms-workflow.md +0 -801
  65. package/.agent/cookbook/birthday-greeting-sms-trigger.md +0 -696
  66. package/.agent/cookbook/bulk-ingest.md +0 -302
  67. package/.agent/cookbook/contact-us-triage-with-llm.md +0 -663
  68. package/.agent/cookbook/dunning-sms-for-delinquent.md +0 -659
  69. package/.agent/cookbook/generic-tables.md +0 -244
  70. package/.agent/cookbook/invite-team.md +0 -200
  71. package/.agent/cookbook/kyc-notification-on-account-activation.md +0 -661
  72. package/.agent/cookbook/marketing-campaign-send.md +0 -1044
  73. package/.agent/cookbook/paginated-restapi-poller.md +0 -383
  74. package/.agent/cookbook/rest-fetch.md +0 -273
  75. package/.agent/cookbook/sanctions-screening-with-agent-review.md +0 -773
  76. package/.agent/cookbook/score-leads-with-llm-categorization.md +0 -665
  77. package/.agent/cookbook/system-templates.md +0 -165
  78. package/.agent/cookbook/talk-to-data.md +0 -178
  79. package/.agent/cookbook/triage-prospects-by-priority.md +0 -571
  80. package/.agent/cookbook/welcome-sms-for-customers.md +0 -647
  81. /package/.agent/cookbook/_fixtures/{healthcare → primary-care-feedback}/memorandum-of-association-01.png +0 -0
  82. /package/.agent/cookbook/_fixtures/{healthcare → primary-care-feedback}/sample_two_page.pdf +0 -0
  83. /package/.agent/cookbook/_fixtures/{subscription → subscription-saas}/_customers_subscription_customer.liquid +0 -0
  84. /package/.agent/cookbook/_fixtures/{subscription → subscription-saas}/stripe_customers_batch1.csv +0 -0
package/.agent/AGENTS.md CHANGED
@@ -88,7 +88,7 @@ corpus page documents the surface you already know:
88
88
  | Host | The platform itself | This corpus + `account_management.md` |
89
89
  | Client | Data Activation Client | `data_activation_clients.md` |
90
90
  | Server | Agentic Workflow | `workflows.md` |
91
- | Resources | Datasets (per data domain) | `generic_tables.md`, `mdm.md` |
91
+ | Resources | Datasets + generic tables | `generic_tables.md`, `mdm.md` |
92
92
  | Prompts | Interoperability Contracts | `interoperability_contracts.md`, `templates.md` |
93
93
  | Sampling | Dataset event triggers | `data_activation_clients.md` §6.4 logs |
94
94
  | Tools | Tool Calls (polymorphic body)| `tools.md` |
@@ -231,17 +231,16 @@ What changes between them is only *where you put the calls* (a component
231
231
  handler vs. a test body), never the calls themselves. The cookbooks under
232
232
  `cookbook/` read as test-style sequences because that is how they are
233
233
  validated, but every step is a plain `api.*` call you can lift into an app
234
- unchanged. The per-capability docs (e.g. `bulk-ingest.md`, `ai-agent-invoke.md`)
235
- show one capability at a time; the cookbooks weave capabilities into a
236
- business outcome.
234
+ unchanged. Capabilities are not documented separately — each lives as
235
+ numbered steps inside the cookbook whose use case motivates it.
237
236
 
238
237
  ## Resources
239
238
 
240
239
  ```
241
240
  INFRASTRUCTURE (stood up before data flows)
242
241
  ──────────────
243
- datalakes.md Storage layer; regulated +
244
- unregulated tiers.
242
+ datalakes.md Storage layer; the raw lake and
243
+ its tokenized + redacted copies.
245
244
  data_sources.md External ingestion endpoints.
246
245
  tools.md Authenticated connections for
247
246
  action execution.
@@ -260,6 +259,10 @@ business outcome.
260
259
  DATA ACTIVATION (how data flows in)
261
260
  ───────────────
262
261
  generic_tables.md Schema-on-write tables.
262
+ advanced_migrations.md Hand-written DDL for one lake —
263
+ indexes, foreign keys, anything
264
+ past the CREATE TABLE / ADD COLUMN
265
+ the platform writes for itself.
263
266
  interoperability_contracts.md Liquid template mappings.
264
267
  data_activation_clients.md Ingestion pipelines — both the
265
268
  binding row (CRUD) and the
@@ -299,6 +302,7 @@ wire name (snake_case):
299
302
  api.connectedApps connected_apps.md
300
303
  api.mdm mdm.md
301
304
  api.genericTables generic_tables.md
305
+ api.advancedMigrations advanced_migrations.md
302
306
  api.interoperabilityContracts interoperability_contracts.md
303
307
  api.dataActivationClients data_activation_clients.md
304
308
  api.workflows workflows.md
@@ -323,12 +327,17 @@ at runtime.
323
327
 
324
328
  Each cookbook structure:
325
329
 
326
- - **Front matter** — `title`, `summary`, `industry`, `slug`,
330
+ - **Front matter** — `title`, `summary`, `use_case`, `slug`,
327
331
  `vitest_source` (list of integration-test files the snippets
328
- are lifted from, anchor file first), `status`. The `industry:`
329
- field also drives automatic discovery of the per-industry
330
- bootstrap setup file at `_setup/<industry>.md` — see
331
- "Industry-derived setup files" below.
332
+ are lifted from, anchor file first), `status`. `use_case` is one
333
+ of the four surfaces — `organic-marketing`,
334
+ `subscription-saas`, `payments-compliance`,
335
+ `primary-care-feedback` — and there is exactly one cookbook per use
336
+ case. Three are at `cookbook/<use-case>.md`; the fourth is at
337
+ `cookbook/primary-care.md`, because `use_case` tracks the integration
338
+ suite it is lifted from (`integration-tests/tests/primary-care-feedback/`)
339
+ and the filename does not. Read `use_case` from the front matter rather
340
+ than deriving it from the filename.
332
341
  - **Problem** — the business-outcome statement in domain terms,
333
342
  sourced from the anchor vitest's behaviour (not from
334
343
  customer-narrative documentation).
@@ -349,44 +358,33 @@ Each cookbook structure:
349
358
  - **See also** — links to relevant per-resource reference MDs
350
359
  and to the anchor + ancillary vitest files.
351
360
 
352
- ### Industry-derived setup files
353
-
354
- Cookbook authoring uses per-industry bootstrap setup files
355
- under `.agent/cookbook/_setup/<industry>.md` to DRY out the
356
- auth + tenant + datalake + dataset-seeding steps every
357
- scenario in an industry shares. The convention matches the
358
- markdown-doctest ecosystem pattern — setup belongs to a
359
- scope (the industry), and any cookbook in that scope
360
- inherits the setup by being there. No per-cookbook opt-in
361
- field is required.
362
-
363
- - Setup files live under `_setup/`. The leading underscore on
364
- the directory marks them as fragments (not standalone
365
- scenarios); the validator's discovery walk skips them and
366
- the corpus index does not list them under "Available
367
- cookbooks."
368
- - The validator reads each scenario cookbook's existing
369
- `industry:` front-matter field and auto-discovers the
370
- setup file at `_setup/<industry>.md`. A cookbook in an
371
- industry that has no setup file gets nothing inlined; the
372
- validator does not fail if the setup file is absent.
373
- Cookbook authors write no additional front-matter for
374
- setup inclusion — the relationship is implicit, by
375
- convention.
376
- - The validator inlines the setup file's numbered `it()`
377
- blocks BEFORE the scenario's own numbered `it()` blocks
378
- inside the same `describe(...)`. Each `it()` label is
379
- prefixed with its source slug so failure output
380
- unambiguously points at the file to open (e.g.
381
- `_setup/foundation §001 — auth` versus
382
- `birthday-greeting-sms-trigger §001 — create workflow`).
383
- Cookbook authors keep clean local §001-§00N numbering
384
- inside their own markdown.
385
- - An agent reading a scenario cookbook discovers the matching
386
- setup file at the predictable conventional path
387
- `_setup/<industry>.md` — one extra file open at a
388
- fixed-by-convention location, not via cookbook-specific
389
- metadata the agent has to learn.
361
+ ### Every cookbook is self-reliant, and repeats what it needs
362
+
363
+ A cookbook stands up **its own** tenant, datalake and resources in
364
+ its own numbered steps. There is no shared setup file, no setup
365
+ group and no bootstrap phase: everything the walk depends on is
366
+ in the walk.
367
+
368
+ That means the auth + tenant + datalake preamble is **duplicated
369
+ across all four cookbooks, deliberately.** This corpus is read by
370
+ someone — or something — that opens one file and works through it,
371
+ and for that reader a shared setup is not a saving. It is a second
372
+ file to find, a second convention to learn, and a hidden dependency
373
+ on steps they never see. Four documents repeating a preamble is the
374
+ cheaper trade, and it is the trade this corpus makes. Do not
375
+ re-factor it out.
376
+
377
+ Two consequences worth stating plainly:
378
+
379
+ - **Write for idempotence, not just for a clean run.** Use stable
380
+ names and create-or-reuse: look the resource up, create it only
381
+ if it is absent, and assert the end state either way. A step that
382
+ mints a fresh random name works once and litters forever —
383
+ nothing in the product reclaims an abandoned datalake. Ask of
384
+ every creating step: what does this do on run two?
385
+ - **Label numbering is local and clean.** Each cookbook numbers its
386
+ own steps `§001…§00N` from the top, and a failure names the file
387
+ and the step directly.
390
388
 
391
389
  ### Vendored fixtures (Liquid templates, CSV bodies)
392
390
 
@@ -417,7 +415,7 @@ repo's own integration tests use.
417
415
  const LE_TEMPLATE = fs.readFileSync(
418
416
  path.join(
419
417
  process.env.COOKBOOK_FIXTURES_DIR!,
420
- 'foundation/_lead_submissions_foundation_legal_entity.liquid',
418
+ 'organic-marketing/_lead_submissions_foundation_legal_entity.liquid',
421
419
  ),
422
420
  'utf8',
423
421
  )
@@ -439,109 +437,58 @@ repo's own integration tests use.
439
437
  template plus the cookbooks that depend on it. No need to
440
438
  read cookbook code first to know what fixtures exist.
441
439
 
442
- ### Two kinds of doc here: business cookbooks vs. capability docs
440
+ ### The four cookbooks
443
441
 
444
- The `cookbook/` directory holds two kinds of recipe, both validated by
445
- `make validate-cookbook`:
442
+ `cookbook/` holds **four files, one per use case**, and nothing else that
443
+ runs. Each is self-reliant: it stands up its own tenant, datalake and
444
+ resources in its own numbered steps and shares nothing with its siblings.
445
+ A reader reads one file end to end and needs no second one. The
446
+ duplicated bootstrap across them is deliberate — four repeated preambles
447
+ are cheaper than one shared setup every reader must go and understand
448
+ first.
446
449
 
447
- - **Business cookbooks** (`<use-case>.md`) — one real business outcome each,
448
- told as a numbered API-call sequence (e.g. `welcome-sms-for-customers`,
449
- `dunning-sms-for-delinquent`). Read these to see how resources compose into
450
- an outcome.
451
- - **Capability docs** (`<capability>.md`) — one platform capability each,
452
- shown as the minimal call sequence that proves it (e.g. `bulk-ingest`,
453
- `ai-agent-invoke`, `generic-tables`, `invite-team`). Read these to learn one
454
- capability in isolation; cookbooks weave them into outcomes.
450
+ There is no separate per-capability file. Bulk upload, REST fetch, team
451
+ invitations, action status updaters, paginated pollers, text-to-SQL and
452
+ agent file-invocation each live as numbered steps inside the cookbook
453
+ whose use case motivates them, because a capability shown without a
454
+ reason to use it is the thing readers skip.
455
455
 
456
- ### Available cookbooks
457
-
458
- The ten business-cookbook scenarios. Each is anchored to a green
459
- end-to-end vitest scenario in the platform's integration-tests suite and is
460
- verified at dev time by `make validate-cookbook` at the platform-sdk repo root.
461
- This index is mirrored into the managed block `alvera llm-export` writes to
462
- consumer `AGENTS.md` files (`buildManagedBlock` in both the SDK and CLI
463
- packages — kept in sync by hand; the llm-export test suites pin the shipped
464
- copy, so a dropped entry breaks a test).
456
+ All four are verified at dev time by `make validate-cookbook` at the
457
+ repo root; every numbered step is one test.
465
458
 
466
459
  <!-- BEGIN:cookbook-index -->
467
460
 
468
- **Healthcare**
469
-
470
- - [appointment-review-sms-workflow](./cookbook/appointment-review-sms-workflow.md)
471
- — Send a patient review-request SMS after a fulfilled
472
- appointment, deep-linked to a connected-app feedback form.
473
- - [contact-us-triage-with-llm](./cookbook/contact-us-triage-with-llm.md)
474
- — Triage inbound contact-us messages into three priority
475
- buckets (appointment / job-application / spam) via an LLM
476
- agent, route each to a tailored SMS action.
477
-
478
- **Subscription**
479
-
480
- - [welcome-sms-for-customers](./cookbook/welcome-sms-for-customers.md)
481
- — Send a welcome SMS to newly contracted customers with a
482
- self-serve billing-portal link.
483
- - [dunning-sms-for-delinquent](./cookbook/dunning-sms-for-delinquent.md)
484
- — Send a payment-reminder SMS to delinquent customers
485
- (filtered on phone-on-file and tax-id-verified) with a
486
- pay-invoice link.
487
- - [triage-prospects-by-priority](./cookbook/triage-prospects-by-priority.md)
488
- — Triage inbound AR customers into priority bands
489
- (high / medium / low) via an LLM agent, route each band to a
490
- tailored SMS action.
491
-
492
- **Payments**
493
-
494
- - [kyc-notification-on-account-activation](./cookbook/kyc-notification-on-account-activation.md)
495
- — Send a KYC-notification SMS when a payment account
496
- transitions to active status.
497
- - [sanctions-screening-with-agent-review](./cookbook/sanctions-screening-with-agent-review.md)
498
- — Disambiguate gray-zone sanctions screenings via an LLM
499
- agent, route confirmed-clean and confirmed-block outcomes to
500
- distinct SMS actions.
501
-
502
- **Foundation**
503
-
504
- - [birthday-greeting-sms-trigger](./cookbook/birthday-greeting-sms-trigger.md)
505
- — Send a happy-birthday SMS on each contact's next birthday
506
- using a pure-Liquid trigger (year-roll math).
507
- - [score-leads-with-llm-categorization](./cookbook/score-leads-with-llm-categorization.md)
508
- — Score inbound leads into four bands
509
- (hot / warm / cold / spam) via an LLM agent, route each band
510
- to a tailored SMS action.
511
- - [marketing-campaign-send](./cookbook/marketing-campaign-send.md)
512
- — Send an A/B marketing campaign across SMS and email: the
513
- suppression and reachability gates and the A/B split all live
514
- in the workflow, each send carries a per-recipient short link,
515
- and the reply re-attaches to the customer who sent it.
461
+ - [organic-marketing](./cookbook/organic-marketing.md) — score inbound
462
+ leads into four bands with an LLM agent, schedule a birthday greeting a
463
+ year out with a pure-Liquid trigger, and run an A/B campaign whose
464
+ suppression gates and split live in the workflow. Ends with delivery
465
+ reconciliation and a natural-language read of the lake. One raw
466
+ datalake.
467
+ - [payments-compliance](./cookbook/payments-compliance.md) —
468
+ disambiguate gray-zone sanctions screenings with an LLM agent that
469
+ never sees who was screened, and fire a KYC notification on account
470
+ activation. Ends by reading one row through raw, tokenized and redacted
471
+ to show what each reader gets. Three datalakes.
472
+ - [subscription-saas](./cookbook/subscription-saas.md) — one customers
473
+ table and three workflows over it: a welcome SMS gated on
474
+ reachability, a dunning reminder gated on reachability AND KYC, and an
475
+ LLM agent banding accounts by priority. Then the same table filled
476
+ three more ways — inline, bulk CSV, REST fetch — and a teammate invited
477
+ onto the tenant. One raw datalake.
478
+ - [primary-care](./cookbook/primary-care.md) — a review request that
479
+ only reaches patients whose visit actually happened, an LLM agent
480
+ triaging inbound messages off the tokenized projection, and a vision
481
+ agent reading a scanned document. Three datalakes.
516
482
 
517
483
  <!-- END:cookbook-index -->
518
484
 
519
- ### Capability docs
520
-
521
- One platform capability each — the minimal call sequence that proves it,
522
- anchored to a green vitest scenario. Read the matching reference MD for the
523
- full wire shape.
524
-
525
- - [bulk-ingest](./cookbook/bulk-ingest.md) — load a whole file of records in
526
- one upload (presigned link → PUT → `ingestFile` → search by batch).
527
- - [rest-fetch](./cookbook/rest-fetch.md) — pull records from a third-party REST
528
- API on demand (Bearer + OAuth2), no file upload.
529
- - [ai-agent-invoke](./cookbook/ai-agent-invoke.md) — read an uploaded
530
- document/image and pull structured JSON out of it (`aiAgents.invoke` with
531
- files). The correct file-vision path — the agent reads the file; the DAC
532
- ingests it.
533
- - [generic-tables](./cookbook/generic-tables.md) — stand up a generic table the
534
- built-in datasets don't model; deploy → ingest → read back via executeSql.
535
- - [action-status-updaters](./cookbook/action-status-updaters.md) — reconcile
536
- the delivery status of messages you send, on a schedule.
537
- - [system-templates](./cookbook/system-templates.md) — discover the platform's
538
- built-in row-mapping Liquid templates.
539
- - [invite-team](./cookbook/invite-team.md) — invite a teammate into your tenant
540
- (root / tenantless / tenant-scoped sessions in one flow).
541
- - [talk-to-data](./cookbook/talk-to-data.md) — turn a datalake conversational:
542
- natural language → reviewable SQL (`datalakes.textToSql`, data-free by
543
- construction) → read-only execution returning a `{ data, meta }` row page or a
544
- CSV export (`datalakes.executeSql`).
485
+ This index is mirrored into the managed block `alvera llm-export` writes
486
+ to consumer `AGENTS.md` files — `buildManagedBlock`, duplicated in both
487
+ the SDK and CLI packages and kept in sync by hand. **Both suites now
488
+ assert every cookbook named in the block exists on disk, and that every
489
+ cookbook on disk is named.** They previously pinned names against the
490
+ block string alone, which is how the index went on advertising four
491
+ deleted files without a single test failing.
545
492
 
546
493
  ## Utility namespaces
547
494
 
@@ -86,7 +86,7 @@ platform-admin side door (a root Bearer):
86
86
  ```typescript
87
87
  const { data: minted } = await rootApi.admin.createTenantApiKey(tenantSlug, {
88
88
  name: 'Bootstrap Key',
89
- data_access_mode: 'unregulated',
89
+ data_access_mode: 'raw',
90
90
  })
91
91
  // minted.api_key — the publishable key value (thread into createSession)
92
92
  ```
@@ -329,7 +329,7 @@ What you SUBMIT when creating an invitation. The
329
329
  | Invitation `role` | Result on accept |
330
330
  |-------------------|----------------------------------------|
331
331
  | `'member'` | Standard tenant user |
332
- | `'researcher'` | Tokenized-only access; blocked from regulated schema |
332
+ | `'researcher'` | Ceiling of `tokenized`; cannot read the raw lake |
333
333
  | `'admin'` | Full tenant management |
334
334
 
335
335
  ```typescript
@@ -39,7 +39,7 @@ batch_id — the run this outcome belongs to; THE
39
39
  decision_key — which action fired
40
40
  context_key — companion string key; like decision_key, it is
41
41
  SQL-lane only (neither is Flop-filterable, §5)
42
- mdm_subject_id, subject_name, session_id
42
+ legal_entity_id, subject_name, session_id
43
43
  action_type ('sms'|'email'|'voice'|'data_exchange'), channel,
44
44
  classification
45
45
  status — see §3
@@ -92,17 +92,17 @@ message.
92
92
  // dataset type is SINGULAR 'action_log' (the table is plural)
93
93
  const { data: search } = await api.datasets.createUserSearch(
94
94
  tenantSlug, datalakeSlug, 'action_log',
95
- { search_query: `ral.batch_id = '${batchId}'` },
95
+ { search_query: `al.batch_id = '${batchId}'` },
96
96
  )
97
97
  const { data: page } = await api.datasets.search(
98
98
  tenantSlug, datalakeSlug, 'action_log',
99
- { userSearchId: search.id, dataAccessMode: 'unregulated' },
99
+ { userSearchId: search.id, dataAccessMode: 'tokenized' },
100
100
  )
101
101
  ```
102
102
 
103
103
  Filterable columns (Flop, the full set): `id`, `inserted_at`,
104
104
  `action_type`, `channel`, `status`, `batch_id`, `classification`,
105
- `workflow_id`, `sent_at`, `external_id`, `mdm_subject_id`. Note
105
+ `workflow_id`, `sent_at`, `external_id`, `legal_entity_id`. Note
106
106
  there is **no `global_search`** here (unlike the message dataset) —
107
107
  `decision_key`/`context_key` are not in this set either; scope them
108
108
  via the `search_query` SQL fragment above.
@@ -0,0 +1,202 @@
1
+ # Advanced migrations
2
+
3
+ SQL a person wrote by hand for one datalake.
4
+
5
+ The platform writes its own DDL when a generic table is created or gains a
6
+ column — `CREATE TABLE` and `ALTER TABLE … ADD COLUMN`, and nothing else.
7
+ Everything past that is yours to write here: an index, a foreign key, a column
8
+ rename, a type change, a drop. The same migrator runs both.
9
+
10
+ **One name, two spellings.** Outwardly these are *advanced migrations*, which is
11
+ what the API path and this guide call them. The platform stores them in
12
+ `datalake_migrations`, and its own logs and internals use that name. Both are
13
+ correct; do not coin a third.
14
+
15
+ api.advancedMigrations → this guide
16
+ /datalakes/{slug}/advanced-migrations
17
+
18
+ ## 1. Wire shape
19
+
20
+ ```typescript
21
+ const { data: created } = await api.advancedMigrations.create(
22
+ tenantSlug,
23
+ datalakeSlug, // scope comes from the PATH — see below
24
+ {
25
+ name: 'AddEmailIndexToContactSubmissions',
26
+ up_statement:
27
+ 'CREATE INDEX IF NOT EXISTS idx_contact_submissions_email ' +
28
+ 'ON alvera_custom_contact_submissions (email)',
29
+ down_statement:
30
+ 'DROP INDEX IF EXISTS idx_contact_submissions_email',
31
+ },
32
+ )
33
+ // created.id — server-derived UUID
34
+ // created.version — server-assigned from a sequence; orders execution
35
+ // created.datalake_id — server-derived from the path
36
+ // created.checksum — server-computed; the drift fingerprint
37
+ ```
38
+
39
+ Three fields, all required, all strings:
40
+
41
+ ```
42
+ name required string — capitalized identifier, unique per datalake
43
+ up_statement required string — the DDL to run
44
+ down_statement required string — the DDL that reverses it
45
+ ```
46
+
47
+ **`datalake_id` is NOT a body field.** This is the one place the shape departs
48
+ from every other datalake-scoped kind. Elsewhere — tools, workflows, generic
49
+ tables — you send `datalake_id` in the body. Here it is `readOnly`: the scope
50
+ comes from `datalakeSlug` in the path, and the server fills the field in. Sending
51
+ it is not how you say which lake this belongs to.
52
+
53
+ **`version` is server-assigned and orders execution.** It comes from a sequence,
54
+ not from you, and it is the thing that decides what runs first — not the name,
55
+ and not the order you declared them in. Generated migrations carry a year-style
56
+ prefix in their names to read well beside yours; that prefix is cosmetic.
57
+
58
+ ## 2. Rules the type cannot encode
59
+
60
+ ### An applied migration is frozen — update and delete both refuse
61
+
62
+ Once a migration has run successfully, `update()` and `delete()` each answer
63
+ **409 Conflict**. The reasons differ and both are worth knowing.
64
+
65
+ **Update** is refused because a statement that has already executed cannot be
66
+ rewritten after the fact: the new text would no longer describe what happened.
67
+
68
+ **Delete** is refused because the run log cascades on delete, so removing an
69
+ applied migration would also erase the record of it having run.
70
+
71
+ So a migration has exactly two lives. Before it runs it is ordinary and
72
+ editable. After it runs it is history. **Changing applied DDL means writing a
73
+ NEW migration that alters what the old one built** — the same shape as changing
74
+ a generic table column's `privacy_requirement`, which is delete-and-recreate
75
+ rather than an edit.
76
+
77
+ "Applied" means precisely: a run log row exists for this migration with
78
+ direction `up` and status `success`. A failed run does not freeze it.
79
+
80
+ ### Write the statement to be safe on a second run
81
+
82
+ The platform tracks what it has applied and will not run an applied migration
83
+ twice, so you do not need `IF NOT EXISTS` for that. You need it for the other
84
+ case: a run that failed **partway**. The applied-migrations record stops a second
85
+ run; it does not undo a first one that got halfway.
86
+
87
+ ```sql
88
+ -- safe to meet again
89
+ CREATE INDEX IF NOT EXISTS idx_submissions_email ON … (email)
90
+
91
+ -- Postgres has no ADD CONSTRAINT IF NOT EXISTS, so guard it yourself
92
+ DO $$
93
+ BEGIN
94
+ IF NOT EXISTS (
95
+ SELECT 1 FROM pg_constraint WHERE conname = 'fk_submissions_lead'
96
+ ) THEN
97
+ ALTER TABLE alvera_custom_contact_submissions
98
+ ADD CONSTRAINT fk_submissions_lead
99
+ FOREIGN KEY (lead_id) REFERENCES alvera_custom_leads (id);
100
+ END IF;
101
+ END $$;
102
+ ```
103
+
104
+ The same applies to `down_statement`, which is likelier to be run against a
105
+ database that is not in the state you assumed.
106
+
107
+ ### The table names are the platform's, not yours
108
+
109
+ A generic table you declared as `contact_submissions` is physically
110
+ `alvera_custom_contact_submissions`. Read the name back from the created table
111
+ rather than composing it — see `generic_tables.md` — because your migration
112
+ statement has to name the physical table, and getting it wrong is a runtime
113
+ failure inside DDL rather than a validation error.
114
+
115
+ ### Ordering is real, and it is the version's job
116
+
117
+ Adding an index and then a foreign key that depends on it are two migrations,
118
+ and the sequence decides which runs first. Two migrations created in one session
119
+ run in creation order because the sequence is monotonic — but if you need B
120
+ after A, that is a fact about your two `version` values, not about how you
121
+ listed them.
122
+
123
+ ## 3. Field ownership
124
+
125
+ **Caller-authored (Request).** `name`, `up_statement`, `down_statement`. That is
126
+ the whole writable surface.
127
+
128
+ **Server-derived (Response-only).** `id`, `datalake_id`, `version`, `checksum`,
129
+ `inserted_at`, `updated_at`. None may be sent; see `type_naming.md` for why the
130
+ Writable types exclude them.
131
+
132
+ `name` is unique per datalake, so two lakes may each carry a
133
+ `AddEmailIndex` and neither collides.
134
+
135
+ ## 4. Error envelopes
136
+
137
+ Standard JSON:API envelope throughout — see `errors.md`.
138
+
139
+ | Status | When |
140
+ |---|---|
141
+ | 422 | a required field is missing, or `name` collides within this datalake |
142
+ | 409 | the migration has already been applied — `update` and `delete` both |
143
+ | 404 | the id belongs to a different datalake (deliberately not "403": its existence is not leaked) |
144
+
145
+ A 409 here is not a retryable condition. It is telling you the migration is
146
+ history and you want a new one.
147
+
148
+ ## 5. Lifecycle
149
+
150
+ ```
151
+ create → the row exists; nothing has run yet
152
+ migrate → the datalake's migrator runs it, in version order
153
+ (POST /datalakes/{slug}/migrate — asynchronous, 202)
154
+ applied → a run log row: direction up, status success
155
+ from here: update 409, delete 409
156
+ ```
157
+
158
+ Creating a migration does **not** run it. The datalake's migrate call does, and
159
+ it is asynchronous — it answers 202 as soon as the job is enqueued. Poll the
160
+ datalake for `status: ready` the way you would after creating one; see
161
+ `datalakes.md`.
162
+
163
+ ## 6. Gotchas
164
+
165
+ **A created migration that never ran is invisible in the database.** The row
166
+ exists, `list()` returns it, and nothing has happened to any table. Reading the
167
+ catalog is the only way to know whether it took effect — the migration record
168
+ says what you asked for, not what is true.
169
+
170
+ **Proving one worked means asking the database, not the API.** The response
171
+ carries `checksum`, `datalake_id`, `down_statement`, `id`, `inserted_at`,
172
+ `name`, `up_statement`, `updated_at` and `version` — no status, no applied
173
+ flag, and `version` is execution ORDER rather than proof of execution. So the
174
+ honest check is a query against the catalog for the object the statement was
175
+ supposed to create:
176
+
177
+ ```typescript
178
+ // an index
179
+ const { data: indexRows } = await api.datalakes.executeSql(tenantSlug, datalakeSlug, {
180
+ sql: `select 1 from pg_indexes
181
+ where tablename = 'alvera_custom_contact_submissions'
182
+ and indexname = 'idx_contact_submissions_email'`,
183
+ mode: 'raw',
184
+ })
185
+
186
+ // a foreign key
187
+ const { data: fkRows } = await api.datalakes.executeSql(tenantSlug, datalakeSlug, {
188
+ sql: `select 1 from information_schema.table_constraints
189
+ where constraint_type = 'FOREIGN KEY'
190
+ and table_name = 'alvera_custom_contact_submissions'
191
+ and constraint_name = 'fk_submissions_lead'`,
192
+ mode: 'raw',
193
+ })
194
+ ```
195
+
196
+ **`down_statement` is required and is rarely exercised.** Nothing makes you
197
+ prove it works, and it is the statement you will want on your worst day. Write
198
+ it as carefully as the `up`.
199
+
200
+ **A migration belongs to exactly one datalake.** There is no shared or
201
+ tenant-level migration. Three lakes needing the same index need three
202
+ migrations — which is also why `name` only has to be unique per lake.