@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,137 @@
1
+ # Debugging
2
+
3
+ Tracing HTTP traffic through the SDK is opt-in via the
4
+ `ApiDebugConfig` callback interface. Wire it at client
5
+ construction time and the SDK's HTTP interceptor will log
6
+ every request + response through your callback.
7
+
8
+ **Safety first**: enabling debug logging without redaction
9
+ will leak credentials in the request bodies the SDK sends to
10
+ the platform. The `ApiDebugConfig.redactStrings` array is the
11
+ load-bearing safety feature — pass every active secret literal
12
+ you've installed BEFORE the SDK fires its first request.
13
+
14
+ ## Wiring the debug interceptor
15
+
16
+ ```typescript
17
+ import {
18
+ createIsolatedPlatformApi,
19
+ type ApiDebugConfig,
20
+ } from '@alvera-ai/platform-sdk'
21
+
22
+ const debugConfig: ApiDebugConfig = {
23
+ log: (msg: string) => process.stderr.write(msg + '\n'),
24
+ redactStrings: getActiveSecretLiterals(),
25
+ // e.g. ['AKIA...real-key', 'twilio-real-token', ...]
26
+ }
27
+
28
+ const api = createIsolatedPlatformApi({
29
+ baseUrl,
30
+ sessionToken,
31
+ debug: debugConfig,
32
+ })
33
+ ```
34
+
35
+ When `debug` is wired:
36
+ - The SDK's HTTP interceptor logs each request + response
37
+ through the `log` callback. Consumer chooses the sink:
38
+ stderr, Pino, OpenTelemetry, etc.
39
+ - BEFORE logging, the SDK substitutes every string in
40
+ `redactStrings` with a fixed-length placeholder
41
+ (`********`).
42
+
43
+ When `debug` is omitted (the default), the SDK doesn't log
44
+ HTTP traffic at all — nothing to redact.
45
+
46
+ ## Safety: the load-bearing piece
47
+
48
+ The SDK never sees secret REFERENCES (`<%= name %>` Eta
49
+ syntax is a consumer-side convention used by some — CLI
50
+ manifests, for instance). By the time a request body reaches the SDK,
51
+ credentials are already substituted to their literal values.
52
+ If you enable debug logging without populating
53
+ `redactStrings`, those literals appear in plaintext in your
54
+ log output — including production logs if `debug` is wired
55
+ in non-dev environments.
56
+
57
+ The redaction is fixed-length (`********`), NOT length-
58
+ preserving. A length-preserving mask would still leak
59
+ length-as-signal (an AWS access key is 20 characters;
60
+ a 20-char placeholder says "this WAS a 20-char string").
61
+
62
+ ## The ApiDebugConfig contract
63
+
64
+ ```typescript
65
+ type ApiDebugConfig = {
66
+ log: (message: string) => void
67
+ redactStrings?: readonly string[]
68
+ }
69
+ ```
70
+
71
+ - `log` is the sink. The SDK emits formatted log messages
72
+ (request method + URL + body, response status + body) as
73
+ a single string per HTTP exchange.
74
+ - `redactStrings` is an array of LITERAL values the SDK
75
+ should scrub from log output. Empty / absent = no
76
+ redaction (use only when no secrets are in the request
77
+ bodies — rare).
78
+
79
+ The SDK's log output is plain strings, not structured. For
80
+ structured logging (per-field JSON, OTel spans, etc.),
81
+ consumers wrap the `log` callback with their own emitter.
82
+
83
+ ## What the SDK does NOT redact
84
+
85
+ - **Exception bodies thrown by the SDK** (the 422 envelope,
86
+ etc.) are NOT filtered through `redactStrings`. If your
87
+ error-rendering path includes the offending request body,
88
+ redact it yourself before rendering. See `errors.md` for
89
+ the throw mechanism.
90
+ - **Response bodies that legitimately contain secrets**.
91
+ Server-derived fields that are credentials (rare but
92
+ possible) come back un-redacted in the response; the
93
+ consumer is responsible for not logging them.
94
+ - **Anything OUTSIDE the SDK's HTTP path**. `redactStrings`
95
+ only filters what the SDK itself writes to `log`. Your own
96
+ log statements that touch request/response data must
97
+ redact themselves.
98
+
99
+ ## Defense in depth
100
+
101
+ `redactStrings` is one layer. Belt-and-suspenders practice:
102
+ - Avoid log statements that interpolate request bodies
103
+ OUTSIDE the SDK's debug pipeline.
104
+ - Use fixed-length placeholders in your own surfaces (CLI
105
+ tables, browser consoles, error pages, exception traces).
106
+ - Treat exception bodies as untrusted for log purposes —
107
+ parse what you need, don't dump the whole thing.
108
+ - Don't ship `debug` enabled in production unless the
109
+ redact list is provably complete.
110
+
111
+ ## Pattern: snapshotting the redact list
112
+
113
+ `redactStrings` is read at client construction time
114
+ (`createIsolatedPlatformApi(...)`). If your consumer's
115
+ secret store mutates during the process's lifetime (e.g. a
116
+ new credential gets installed mid-run), you need to
117
+ construct a NEW client to pick up the new redaction string —
118
+ the SDK doesn't observe the source array after construction.
119
+
120
+ Consumers that hold secrets in a mutable store typically
121
+ build the client lazily (per-request or per-handler) to
122
+ guarantee a fresh snapshot.
123
+
124
+ ## What's in the log line
125
+
126
+ A typical log message the SDK emits looks roughly like:
127
+
128
+ ```
129
+ [platform-sdk] POST /api/v1/tenants/acme/datalakes/main/tools
130
+ request body: {"name":"...","intent":"data_exchange",
131
+ "body":{"tool_body_type":"s3","access_key_id":"********",
132
+ "secret_access_key":"********",...}}
133
+ response 201: {"id":"...","slug":"...","name":"...",...}
134
+ ```
135
+
136
+ (Format is plain-string; exact prefix and field ordering are
137
+ SDK-internal and may evolve.)
@@ -0,0 +1,196 @@
1
+ # Errors
2
+
3
+ When you call an SDK method, two things can happen:
4
+
5
+ 1. **Success** — `{ data, meta? }` returned (typed).
6
+ 2. **Throw** — the SDK throws the server's response body verbatim after
7
+ the HTTP call returned a non-2xx status.
8
+
9
+ The SDK does **not** validate request bodies on the client. Validation is
10
+ server-authoritative: a malformed body is rejected by the platform and
11
+ surfaced to you as a thrown **422** body. There is no synchronous
12
+ client-side validation error to catch — every validation failure comes back
13
+ from the server in the standardized envelope below.
14
+
15
+ > Driving the platform through the `alvera` **CLI** instead of the SDK? Then
16
+ > `alvera plan` runs a local pre-flight check against the spec and reports
17
+ > errors *before* any HTTP call. That's a CLI feature (it validates the spec
18
+ > locally); it is not SDK runtime behavior. SDK callers get errors from the
19
+ > server.
20
+
21
+ ## The thrown value (post-HTTP)
22
+
23
+ When the platform returns a non-2xx status, **the SDK throws the parsed
24
+ response body verbatim** as the thrown value — not an `Error` subclass, just
25
+ the body object (decorated with an `_httpStatus` field so you can branch on
26
+ the status). Narrow it with a type guard:
27
+
28
+ ```typescript
29
+ try {
30
+ const { data } = await api.tools.create(
31
+ tenantSlug, datalakeSlug, body,
32
+ )
33
+ } catch (err) {
34
+ if (
35
+ typeof err === 'object' && err !== null &&
36
+ 'errors' in err && Array.isArray((err as any).errors)
37
+ ) {
38
+ const { errors } = err as {
39
+ errors: Array<{
40
+ source: { pointer: string }
41
+ title?: string
42
+ detail?: string
43
+ }>
44
+ }
45
+ for (const e of errors) {
46
+ // e.source.pointer is "/field_name" or "/embed/field"
47
+ }
48
+ return
49
+ }
50
+ throw err
51
+ }
52
+ ```
53
+
54
+ The thrown body is typed as `AlveraApiError` (exported from the package root)
55
+ for 422s, and the simpler `ErrorResponse` for some other statuses.
56
+
57
+ ## 422 envelope shape
58
+
59
+ The platform standardizes ALL 422 response bodies on a single shape —
60
+ regardless of which validation layer fired:
61
+
62
+ ```json
63
+ {
64
+ "errors": [
65
+ {
66
+ "source": { "pointer": "/<field_path>" },
67
+ "title": "<human-readable summary>",
68
+ "detail": "<specific reason this field failed>"
69
+ },
70
+ ...
71
+ ]
72
+ }
73
+ ```
74
+
75
+ - `errors` is always an ARRAY of error objects (never a field-keyed map).
76
+ - `source.pointer` is a JSON Pointer (`/field_name` for top-level fields;
77
+ `/nested/field` for embeds) naming the offending field on the request body.
78
+ - `detail` is the specific reason; `title` is a brief summary.
79
+ - Multiple errors may share the same `source.pointer` if more than one check
80
+ fired on the same field.
81
+
82
+ ## Two layers behind the standardized 422
83
+
84
+ The platform validates a request in two layers; both surface through the
85
+ same envelope above:
86
+
87
+ **Layer 1 — OpenAPI cast (structural)**
88
+
89
+ Runs at the plug layer (`OpenApiSpex`) before the controller. Catches:
90
+ missing required fields, wrong types, invalid enums, unknown keys, format
91
+ violations (UUID patterns, etc.). This is the *same* OpenAPI schema the SDK's
92
+ types are generated from, so a body that satisfies the TypeScript types
93
+ almost always clears layer 1.
94
+
95
+ **Layer 2 — server-side semantic validator (post-cast)**
96
+
97
+ Runs after the structural cast succeeds. Catches: uniqueness, cross-field
98
+ rules (e.g. `auth_method = "iam_role"` combined with conflicting credential
99
+ fields), foreign-key constraints, custom platform validations. Layer-2
100
+ errors are normalized into the same envelope shape as layer 1 — consumers
101
+ don't distinguish layers from the JSON.
102
+
103
+ Practical consequence: every validation error reaches you as a thrown 422
104
+ body. Read `source.pointer`, fix that field, resubmit.
105
+
106
+ ## Concrete examples
107
+
108
+ **Layer 1 — missing required fields** (POST `/agentic-workflows` with empty
109
+ body):
110
+
111
+ ```json
112
+ {
113
+ "errors": [
114
+ { "source": { "pointer": "/name" },
115
+ "detail": "Missing field: name" },
116
+ { "source": { "pointer": "/dataset_type" },
117
+ "detail": "Missing field: dataset_type" }
118
+ ]
119
+ }
120
+ ```
121
+
122
+ **Layer 1 — invalid enum value** (`dataset_type: "nonexistent_type"`):
123
+
124
+ ```json
125
+ {
126
+ "errors": [
127
+ { "source": { "pointer": "/dataset_type" },
128
+ "detail": "Invalid value: not in enum" }
129
+ ]
130
+ }
131
+ ```
132
+
133
+ **Layer 2 — semantic rule** (DELETE on a default-flagged data activation
134
+ client):
135
+
136
+ ```json
137
+ {
138
+ "errors": [
139
+ { "source": { "pointer": "/is_default" },
140
+ "detail": "Default data activation clients cannot be deleted" }
141
+ ]
142
+ }
143
+ ```
144
+
145
+ **Layer 2 — uniqueness** (POST `/admin/invites` for an already-invited
146
+ email):
147
+
148
+ ```json
149
+ {
150
+ "errors": [
151
+ { "source": { "pointer": "/email" },
152
+ "detail": "is already invited" }
153
+ ]
154
+ }
155
+ ```
156
+
157
+ ## HTTP status codes (under the hood)
158
+
159
+ The SDK abstracts HTTP responses behind typed methods. Consumers mostly see
160
+ typed data or thrown bodies; the thrown body carries `_httpStatus`, and raw
161
+ status is also visible via the SDK's `--debug`-style interceptor. The
162
+ platform maps:
163
+
164
+ | Code | When it fires |
165
+ |------|----------------------------------------------------------------|
166
+ | 200 | GET / PUT / DELETE successful |
167
+ | 201 | POST successful |
168
+ | 400 | Request malformed (rare; the SDK serializes before send) |
169
+ | 401 | Missing or invalid Bearer / `x-api-key` |
170
+ | 403 | Authenticated but role lacks permission |
171
+ | 404 | Resource doesn't exist OR cross-tenant access (404, not 403) |
172
+ | 422 | Validation failed; envelope above is the thrown body |
173
+ | 5xx | Platform-internal failure; safe to retry with backoff |
174
+
175
+ ## Retry semantics
176
+
177
+ Treat 5xx as infra noise — retry with exponential backoff. Treat 422 as a
178
+ real consumer-correctable error — fix the body and resubmit. **Never retry a
179
+ 422 with the same body.**
180
+
181
+ ## Mapping `source.pointer` back to your TypeScript type
182
+
183
+ A `source.pointer` like `/regulated_data_db_reader_user` maps directly to the
184
+ `regulated_data_db_reader_user` field on `DatalakeRequestWritable`. For
185
+ nested embeds, the pointer carries the embed path:
186
+ `/unregulated_cloud_storage/access_key_id`.
187
+
188
+ For polymorphic embeds, check the discriminator field's pointer first — if
189
+ `/tool_body_type` (or `/cloud_storage_type`, etc.) is in the errors array,
190
+ fix that BEFORE chasing other fields on the embed. The wrong discriminator
191
+ value cascades into spurious required-list failures on the wrong branch's
192
+ fields. See `mutations.md` for the discriminator naming convention.
193
+
194
+ Each per-resource MD names its `<Resource>RequestWritable` TypeScript type —
195
+ the corpus's resource pages are the source of truth for which fields exist on
196
+ which embed.
@@ -0,0 +1,351 @@
1
+ # Generic tables
2
+
3
+ A **generic table** is an operator-defined table whose schema the
4
+ caller authors at create time, distinct from the platform's industry-
5
+ built **system datasets** (see `datalakes.md` §5 `.systemDatasets`).
6
+ The SDK namespace is `api.genericTables`.
7
+
8
+ Generic tables are **Datalake-DB-resident**: creation triggers an
9
+ async schema-migration job that builds a physical Postgres table
10
+ in the datalake's schema. The parent datalake MUST be
11
+ `status: 'ready'` before the job can run end-to-end — see
12
+ `datalakes.md` §5 Lifecycle.
13
+
14
+ ## 1. Wire shape
15
+
16
+ ```typescript
17
+ import type {
18
+ GenericTableColumn,
19
+ GenericTableResponse,
20
+ } from '@alvera-ai/platform-sdk'
21
+
22
+ const { data: created } = await api.genericTables.create(
23
+ tenantSlug,
24
+ datalakeSlug,
25
+ {
26
+ title: 'Contact Us Submissions',
27
+ description: 'Inbound contact-form rows',
28
+ columns: [
29
+ {
30
+ name: 'submission_id',
31
+ title: 'Submission ID',
32
+ type: 'string',
33
+ description: 'Unique submission identifier',
34
+ is_unique: true,
35
+ privacy_requirement: 'none',
36
+ },
37
+ // ... more columns
38
+ ],
39
+ },
40
+ )
41
+ // created.id — server-derived UUID
42
+ // created.title === body.title
43
+ // created.name — server-derived: `alvera_custom_<slug>`
44
+ // created.status === 'new' | 'processing' (migration not yet done)
45
+ ```
46
+
47
+ The response returns immediately — the row exists in Platform-DB,
48
+ but the *physical* Postgres table does not yet. Consumers MUST
49
+ poll `.get(name)` until `status === 'deployed'` before treating
50
+ the table as queryable. See §5 Lifecycle.
51
+
52
+ ## 2. Rules the type cannot encode
53
+
54
+ ### `name` is server-derived from `title`
55
+
56
+ Caller supplies `title` (free-form, human-readable);
57
+ server slugifies it and prefixes `alvera_custom_`:
58
+
59
+ ```
60
+ title: 'Contact Us Submissions'
61
+ ↓ slugify + prefix
62
+ name: 'alvera_custom_contact_us_submissions'
63
+ ```
64
+
65
+ The `name` is what every downstream resource references —
66
+ `.get(name)`, `generic_table_id` foreign keys via the resolved
67
+ row, `datasets.metadataDetails(tenantSlug, datalakeSlug, 'generic_table', { genericTableId })`,
68
+ etc. Never pre-compute `name` client-side; read it back from
69
+ the create response. A server-side slugifier change would flip
70
+ every downstream binding, so specs assert the derived `name`
71
+ directly.
72
+
73
+ ### `columns` is operator-authored schema
74
+
75
+ Each column declares storage + semantics in one shot:
76
+
77
+ ```
78
+ name required string — snake_case identifier
79
+ (physical Postgres column name)
80
+ title required string — human label (UI / docs)
81
+ type required enum — see "Column types" below
82
+ is_array optional bool — column holds an array of `type` values
83
+ description required string — narrative; surfaces in catalogs
84
+ is_unique required bool — Postgres UNIQUE constraint at migration time
85
+ privacy_requirement required enum — 'none' | 'tokenize' | 'redact_only'
86
+ ```
87
+
88
+ #### Column types
89
+
90
+ ```
91
+ string free-form text
92
+ integer whole numbers
93
+ float decimal numbers
94
+ boolean true / false
95
+ date calendar date (no time component)
96
+ datetime timestamp with timezone
97
+ time time of day (no date component)
98
+ jsonb arbitrary JSON document
99
+ ```
100
+
101
+ Each type can be paired with `is_array: true` to model an array
102
+ column (`string[]`, `integer[]`, etc.). The wire enum lives in
103
+ the generated SDK as `GenericTableColumnTypeEnum` — consult it for
104
+ the live set since new types land independently of corpus
105
+ revisions.
106
+
107
+ #### Privacy × type interactions
108
+
109
+ `privacy_requirement` selects how the column flows between the
110
+ regulated and unregulated storage tiers (see `datalakes.md` §1
111
+ for the tier split):
112
+
113
+ - `none` — keeps the raw value in the regulated tier only;
114
+ the unregulated tier has no column.
115
+ - `tokenize` — stores the raw value in the regulated tier and an
116
+ opaque stable token in the unregulated tier. AI tools working
117
+ off the unregulated tier see the token, not the underlying
118
+ identity.
119
+ - `redact_only` — keeps the raw value in the regulated tier and
120
+ strips it from the unregulated tier entirely.
121
+
122
+ Two type-level restrictions the typed validator does not encode:
123
+
124
+ - **`type: 'jsonb'` rejects `privacy_requirement: 'tokenize'`.**
125
+ The platform can't tokenize an arbitrary JSON document — there
126
+ is no canonical "identity" to tokenize. Valid privacy values
127
+ for jsonb are `'none'` and `'redact_only'` only. Submitting
128
+ `{ type: 'jsonb', privacy_requirement: 'tokenize' }` returns
129
+ 422 on `/columns/N/privacy_requirement`.
130
+ - **Tokenize widens primitive types at the schema layer.** A
131
+ `{ type: 'integer', privacy_requirement: 'tokenize' }` column
132
+ surfaces in the unregulated tier as a string-typed token, not
133
+ an integer. This is a quirk of how the platform stores tokens
134
+ (one opaque string format across all source types) — your
135
+ unregulated-tier queries must treat the column as a string.
136
+ This affects `integer`, `float`, `boolean`, `is_array string`,
137
+ `date`, `datetime`, and `time`.
138
+
139
+ The privacy choice is **immutable post-create** — there is no
140
+ SDK mutation surface for column-level privacy changes.
141
+
142
+ **Privacy is operator-decided per column, not implied by name.**
143
+ A field called `message` can be `redact_only` in one tenant and
144
+ `none` in another; the SDK doesn't pattern-match column names
145
+ to default privacy levels. Pick deliberately at design time —
146
+ recreating the table is the only way to flip a column's
147
+ treatment later.
148
+
149
+ ### At least one column MUST be `is_unique: true`
150
+
151
+ The platform requires at least one unique column to identify
152
+ rows for the data activation upsert path. A body with zero
153
+ unique columns returns 422 on `/columns` with a message naming
154
+ the missing-unique-column rule. The unique column is what bound
155
+ data activation clients use as the upsert key.
156
+
157
+ ### Reserved column names
158
+
159
+ The platform reserves certain column names you cannot redeclare
160
+ in `columns[N].name`:
161
+
162
+ - **Infrastructure columns** (always reserved, every industry):
163
+ `id`, `inserted_at`, `updated_at`, `datalake_id`, plus the
164
+ unregulated/regulated tier-link fields the platform manages.
165
+ - **Industry-specific MDM FK columns** (reserved only inside
166
+ their owning industry's datalake):
167
+
168
+ ```
169
+ healthcare patient_id
170
+ core_banking party_id
171
+ accounts_receivable customer_id
172
+ (other domains as they land)
173
+ ```
174
+
175
+ A `patient_id` column is REJECTED in a healthcare datalake's
176
+ generic table — the platform's MDM machinery owns that name —
177
+ but is ACCEPTED in a `core_banking` datalake's generic table
178
+ (it's just an opaque user-defined column there). Reserved-name
179
+ rejections surface as 422 on `/columns/N/name`.
180
+
181
+ ### Title cannot start with `alvera_system`
182
+
183
+ The `alvera_system_` prefix is reserved for system-curated
184
+ generic tables the platform manages internally. A `title` whose
185
+ slugified form would start with `alvera_system` is rejected at
186
+ create with 422 on `/title`. Pick a title that slugifies to
187
+ something else (e.g. avoid leading "Alvera System" in the title
188
+ text).
189
+
190
+ ### No update surface
191
+
192
+ `PUT /generic-tables/:name` is **not** an SDK affordance.
193
+ Generic tables are schema-immutable post-create — to change
194
+ columns, delete + recreate. This is a deliberate constraint:
195
+ mutating a deployed schema mid-ingestion would race the
196
+ data activation client.
197
+
198
+ ## 3. Field ownership
199
+
200
+ **Server-derived (Response-only).** Universal set from
201
+ `type_naming.md`, plus:
202
+
203
+ ```
204
+ name string — `alvera_custom_<slugified-title>`
205
+ status enum — 'new' | 'processing' | 'deployed' | 'failed'
206
+ ```
207
+
208
+ **Caller-supplied (round-trip).**
209
+
210
+ ```
211
+ title required string — human-readable label
212
+ description optional string — narrative
213
+ columns required array — column definitions (see §2)
214
+ ```
215
+
216
+ **Write-only (Request-only).** None.
217
+
218
+ ## 4. Error envelopes
219
+
220
+ Standard JSON:API envelopes per `errors.md`. Common rejections:
221
+
222
+ | `source.pointer` | Cause |
223
+ |----------------------------------|----------------------------------------------------|
224
+ | `/title` | empty; generates a name colliding with existing; or starts with `alvera_system` prefix |
225
+ | `/columns` | empty array; duplicate `column.name`; or zero columns marked `is_unique: true` |
226
+ | `/columns/0/name` | reserved column name (infrastructure column or the industry's MDM FK; see §2) |
227
+ | `/columns/0/type` | value not in supported type enum (see §2 column types) |
228
+ | `/columns/0/privacy_requirement` | value not in enum, OR `tokenize` on a `jsonb` column |
229
+
230
+ All of these come back as a 422 `AlveraApiError` thrown from the
231
+ SDK on submit — structural cases (Layer 1) and semantic conflicts
232
+ like name collision / deployed-name reuse (Layer 2). The SDK does
233
+ not validate client-side; see `errors.md`.
234
+
235
+ ## 5. Lifecycle
236
+
237
+ Two-phase create, parallel to datalakes (see `datalakes.md` §5):
238
+
239
+ ```
240
+ .create ──→ status: 'new' or 'processing'
241
+ │
242
+ │ (async migration job — see async.md)
243
+ ▼
244
+ status: 'deployed' ◄── safe to bind / query
245
+ ```
246
+
247
+ ### Create + poll
248
+
249
+ ```typescript
250
+ const { data: created } = await api.genericTables.create(
251
+ tenantSlug, datalakeSlug, body,
252
+ )
253
+ // created.status === 'new' (or 'processing' if worker raced)
254
+
255
+ while (true) {
256
+ const { data: row } = await api.genericTables.get(
257
+ tenantSlug, datalakeSlug, created.name,
258
+ )
259
+ if (row.status === 'deployed') break
260
+ if (row.status === 'failed') throw new Error('migration failed')
261
+ await new Promise((r) => setTimeout(r, 1_000))
262
+ }
263
+ ```
264
+
265
+ `.get(name)` distinguishes "row never created" (404) from
266
+ "deployment in progress" (200 with status sub-`deployed`) —
267
+ two distinct signals on the same endpoint. Use the 404 to
268
+ short-circuit retry loops; use the 200+status to drive the
269
+ poll above.
270
+
271
+ ### Read shapes
272
+
273
+ ```
274
+ .list(tenantSlug, datalakeSlug) Paged: { data, meta }
275
+ .get(tenantSlug, datalakeSlug, name) One row, by name
276
+ (NOT slug — generic
277
+ tables expose `name`)
278
+ ```
279
+
280
+ Plus the polymorphic dataset surface for per-table markdown:
281
+
282
+ ```typescript
283
+ const { data: md } = await api.datasets.metadataDetails(
284
+ tenantSlug,
285
+ datalakeSlug,
286
+ 'generic_table',
287
+ { genericTableId },
288
+ )
289
+ // md: string — agent-facing markdown describing the table's schema
290
+ ```
291
+
292
+ For the whole-domain catalog (every system + generic table type
293
+ registered to this datalake) use the sibling
294
+ `api.datasets.metadata(tenantSlug, datalakeSlug)` — same shape as
295
+ `tools.metadata` / `dataSources.metadata`.
296
+
297
+ See `datalakes.md` §5 `.systemDatasets` for the system-dataset
298
+ sibling of this verb; the `'generic_table'` kind is the entry
299
+ point for generic-table metadata.
300
+
301
+ ### Delete
302
+
303
+ `DELETE` removes the row and drops the physical table. Tables
304
+ bound to active data activation clients, workflows, or
305
+ interoperability contracts reject with a 422 naming the
306
+ dependency. Detach dependents first.
307
+
308
+ ## 6. Gotchas
309
+
310
+ 1. **`.get` takes `name`, not `slug`.** Generic tables expose
311
+ `name` as the canonical reference; the `slug` field — if
312
+ present on `GenericTableResponse` — is incidental and
313
+ currently unused by downstream resources. Stash `data.name`
314
+ from create; pass it to `.get` and to every downstream
315
+ `generic_table` binding.
316
+
317
+ 2. **`status` reaches `deployed` asynchronously.** A bare
318
+ `.create` response with `status: 'new'` is NOT yet bindable.
319
+ The poll loop above is non-optional for any spec that
320
+ creates a generic table and immediately uses it. A 60-second
321
+ timeout floor survives a saturated worker queue on a busy box;
322
+ bump for automated test environments with slower migrations.
323
+
324
+ 3. **Column `privacy_requirement` is immutable post-create.**
325
+ `tokenize` columns surface as an opaque stable token in the
326
+ unregulated tier; `redact_only` strips the value from that
327
+ tier entirely; `none` keeps the raw value only in the
328
+ regulated tier. There is no update surface to flip this —
329
+ recreate the table to change.
330
+
331
+ 4. **System-dataset enumeration EXCLUDES generic tables.**
332
+ `api.datalakes.systemDatasets(...)` returns only the
333
+ industry-built name list; operator-defined generic
334
+ tables surface only via `api.genericTables.list(...)`.
335
+ The literal string `"generic_table"` is also absent from
336
+ the system-datasets enum — see `datalakes.md` §5.
337
+
338
+ 5. **Title-collision returns at create time.** Two generic tables
339
+ cannot share a `title` that slugifies to the same `name`
340
+ within one datalake. The 422 surfaces on POST, well before
341
+ the migration job runs.
342
+
343
+ 6. **Generic tables can carry agentic write-back columns.**
344
+ They are not exclusively ingestion targets. Columns the
345
+ data activation client fills at ingestion (e.g.
346
+ `submission_id`, `name`, `email`) sit alongside columns
347
+ reserved for downstream resources to update (e.g. a reply
348
+ column written by a workflow, or a scoring column written
349
+ by an agent). Plan the column set for the full lifecycle,
350
+ not just the inbound shape — adding columns later requires
351
+ delete + recreate.