@synapsor/runner 1.6.1 → 1.6.3

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 (53) hide show
  1. package/CHANGELOG.md +98 -3
  2. package/README.md +33 -19
  3. package/SECURITY.md +10 -2
  4. package/THREAT_MODEL.md +27 -4
  5. package/dist/authoring.mjs +53 -3
  6. package/dist/cli.d.ts +19 -2
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/local-ui.d.ts +85 -1
  9. package/dist/local-ui.d.ts.map +1 -1
  10. package/dist/runner.mjs +33542 -20382
  11. package/dist/runtime.mjs +3581 -237
  12. package/dist/shadow.mjs +2423 -65
  13. package/docs/README.md +22 -2
  14. package/docs/agent-guided-setup.md +239 -0
  15. package/docs/approval-roles-and-operator-identity.md +353 -0
  16. package/docs/auto-boundary-and-scoped-explore.md +32 -6
  17. package/docs/capability-authoring.md +19 -0
  18. package/docs/current-scope.md +20 -1
  19. package/docs/cursor-plugin.md +2 -2
  20. package/docs/dsl-json-parity.md +75 -0
  21. package/docs/dsl-reference.md +10 -0
  22. package/docs/fresh-developer-usability.md +155 -80
  23. package/docs/getting-started-own-database.md +5 -4
  24. package/docs/guarded-crud-writeback.md +19 -0
  25. package/docs/guided-onboarding.md +331 -0
  26. package/docs/human-attention-notifications.md +367 -0
  27. package/docs/limitations.md +19 -3
  28. package/docs/local-mode.md +16 -2
  29. package/docs/migrating-to-synapsor-spec.md +38 -0
  30. package/docs/production.md +39 -0
  31. package/docs/release-notes.md +87 -4
  32. package/docs/runner-config-reference.md +132 -0
  33. package/docs/running-a-runner-fleet.md +16 -1
  34. package/docs/security-boundary.md +24 -0
  35. package/docs/store-lifecycle.md +20 -1
  36. package/docs/supervised-automatic-apply.md +267 -0
  37. package/docs/troubleshooting-first-run.md +35 -0
  38. package/examples/app-owned-writeback/command-handler.mjs +0 -0
  39. package/examples/fitflow-guided-onboarding/README.md +43 -0
  40. package/examples/fitflow-guided-onboarding/app/page.tsx +8 -0
  41. package/examples/fitflow-guided-onboarding/docker-compose.yml +16 -0
  42. package/examples/fitflow-guided-onboarding/package.json +18 -0
  43. package/examples/fitflow-guided-onboarding/prisma/schema.prisma +98 -0
  44. package/examples/fitflow-guided-onboarding/seed/postgres.sql +401 -0
  45. package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +0 -0
  46. package/examples/operator-oidc/issuer.mjs +188 -0
  47. package/examples/reference-support-billing-app/scripts/run-demo.sh +0 -0
  48. package/examples/support-billing-agent/scripts/run-demo.sh +0 -0
  49. package/examples/support-billing-agent/scripts/run-evaluation.sh +0 -0
  50. package/examples/support-plan-credit/README.md +6 -2
  51. package/package.json +11 -9
  52. package/schemas/schema-candidate-review.schema.json +42 -0
  53. package/schemas/synapsor.runner.schema.json +227 -4
@@ -0,0 +1,267 @@
1
+ # Operator-Supervised Automatic Apply
2
+
3
+ Synapsor separates two decisions that are often incorrectly combined:
4
+
5
+ 1. Who or what may approve a proposal?
6
+ 2. Who or what may execute an approved proposal?
7
+
8
+ The model answers neither question. It supplies a bounded request through a
9
+ reviewed capability. Human-reviewed policy may approve that exact immutable
10
+ proposal, and a separately operated Runner worker may apply it only after
11
+ revalidating the complete guarded-write boundary.
12
+
13
+ Supervised automatic apply is additive and disabled by default. Upgrading does
14
+ not start a worker, change existing `AUTO APPROVE` behavior, or apply an
15
+ already-approved proposal.
16
+
17
+ ## The Four Supported Modes
18
+
19
+ | Approval | Execution | Behavior |
20
+ | --- | --- | --- |
21
+ | Human | Manual | Default: a qualified operator approves and explicitly runs `apply`. |
22
+ | Human | Supervised worker | A qualified operator approves; the trusted worker may apply. |
23
+ | Policy auto-approval | Manual | Reviewed policy approves; an operator still runs `apply`. This is the legacy `AUTO APPROVE` behavior. |
24
+ | Policy auto-approval | Supervised worker | Reviewed policy approves; an independently enabled worker may guardedly apply. |
25
+
26
+ The default remains human approval plus manual apply.
27
+
28
+ ## Dual Opt-In
29
+
30
+ Automatic execution requires both:
31
+
32
+ 1. Contract permission, included in the canonical contract digest.
33
+ 2. Deployment permission for the exact capability and exact active digest.
34
+
35
+ Public DSL (Domain-Specific Language):
36
+
37
+ ```sql
38
+ CREATE CAPABILITY billing.propose_small_credit
39
+ ...
40
+ AUTO APPROVE WHEN amount_cents <= 2500
41
+ LIMIT 20 PER DAY
42
+ ALLOW SUPERVISED WORKER APPLY;
43
+ ```
44
+
45
+ The final clause compiles to:
46
+
47
+ ```json
48
+ {
49
+ "execution": {
50
+ "supervised_worker": "allowed"
51
+ }
52
+ }
53
+ ```
54
+
55
+ That permission alone applies nothing. The operator must separately configure
56
+ an exact deployment allowlist:
57
+
58
+ ```json
59
+ {
60
+ "supervised_worker": {
61
+ "enabled": true,
62
+ "profile": "production",
63
+ "capabilities": [
64
+ {
65
+ "capability": "billing.propose_small_credit",
66
+ "contract_digest": "sha256:REPLACE_WITH_THE_ACTIVE_DIGEST",
67
+ "mode": "supervised_worker",
68
+ "concurrency": 1,
69
+ "queue_limit": 100,
70
+ "lease_seconds": 60,
71
+ "max_attempts": 5,
72
+ "proposal_ttl_seconds": 3600,
73
+ "rate_limit": {
74
+ "executions": 20,
75
+ "window_seconds": 60
76
+ },
77
+ "write_url_env": "BILLING_POSTGRES_WRITE_URL",
78
+ "worker_identity": "billing_worker",
79
+ "control_role": "runner_operator",
80
+ "require_least_privilege_writer": true,
81
+ "writer_posture_fingerprint": "sha256:REPLACE_WITH_REVIEWED_POSTURE_DIGEST"
82
+ }
83
+ ]
84
+ }
85
+ }
86
+ ```
87
+
88
+ Use environment-variable names in config, never database URLs or credentials.
89
+ Production also requires verified `signed_key` or `jwt_oidc` operator identity
90
+ for worker controls.
91
+
92
+ If either opt-in is absent, stale, revoked, or mismatched, the proposal remains
93
+ unapplied. Enabling the worker globally does not enable another capability or a
94
+ new contract version.
95
+
96
+ ## Initial Eligible Write Shapes
97
+
98
+ The hardened supervised path is intentionally narrower than manual apply. It
99
+ accepts only direct-SQL, single-row proposal capabilities whose complete effect
100
+ Runner can validate:
101
+
102
+ - `UPDATE` with trusted tenant scope and an exact non-weak conflict/version
103
+ guard;
104
+ - `INSERT` with trusted tenant scope and a reviewed deterministic
105
+ deduplication key.
106
+
107
+ It currently refuses:
108
+
109
+ - `DELETE`;
110
+ - reversible actions;
111
+ - bounded-set and other multi-row writes;
112
+ - app-owned executors or external effects;
113
+ - arbitrary or model-generated SQL;
114
+ - a missing trusted tenant boundary;
115
+ - an unavailable receipt authority;
116
+ - a shared, privileged, owner, `BYPASSRLS`, or otherwise unverifiable writer
117
+ when hardened posture is required.
118
+
119
+ These operations remain available through their documented manual review/apply
120
+ or app-owned executor paths. The narrower worker eligibility does not remove
121
+ existing Runner write capabilities.
122
+
123
+ ## What The Worker Rechecks
124
+
125
+ Immediately before leasing and applying, Runner rechecks:
126
+
127
+ - proposal state, immutable hash, version, approval, expiry, and exact active
128
+ contract digest;
129
+ - contract permission, deployment allowlist, worker identity, control state,
130
+ profile, generation lock, and current approval-policy snapshot;
131
+ - trusted tenant and principal scope;
132
+ - target row and required supporting-evidence freshness;
133
+ - current source version and exact reviewed before-state;
134
+ - operation, allowed columns, bounds, transitions, row count, and expected
135
+ effect;
136
+ - approval and execution count/value limits under concurrency;
137
+ - writer credential separation, least-privilege posture, grants, and
138
+ PostgreSQL RLS posture where configured;
139
+ - operation identity, deduplication, receipt authority, and active lease.
140
+
141
+ A policy-approved proposal does not gain permanent authority. If the target row
142
+ or mandatory supporting evidence changes before execution, the worker fails
143
+ closed with the established conflict/freshness outcome. A refreshed diff is a
144
+ new proposal with a new hash and needs its own approval.
145
+
146
+ Manual and supervised execution converge on the same guarded apply
147
+ implementation. The queue is durable work coordination, not a second mutation
148
+ engine.
149
+
150
+ ## Run And Operate The Worker
151
+
152
+ Validate first:
153
+
154
+ ```bash
155
+ synapsor-runner config validate --config ./synapsor.runner.json
156
+ synapsor-runner doctor --config ./synapsor.runner.json
157
+ ```
158
+
159
+ Run one supervised claim:
160
+
161
+ ```bash
162
+ synapsor-runner worker run --supervised --once --yes \
163
+ --worker-id billing_worker \
164
+ --config ./synapsor.runner.json
165
+ ```
166
+
167
+ Run continuously:
168
+
169
+ ```bash
170
+ synapsor-runner worker run --supervised --yes \
171
+ --worker-id billing_worker \
172
+ --config ./synapsor.runner.json
173
+ ```
174
+
175
+ Inspect without copying proposal IDs:
176
+
177
+ ```bash
178
+ synapsor-runner worker status --config ./synapsor.runner.json
179
+ synapsor-runner attention show --config ./synapsor.runner.json
180
+ ```
181
+
182
+ Operator controls:
183
+
184
+ ```bash
185
+ synapsor-runner worker pause --yes --config ./synapsor.runner.json
186
+ synapsor-runner worker resume --yes --config ./synapsor.runner.json
187
+ synapsor-runner worker drain --yes --config ./synapsor.runner.json
188
+
189
+ synapsor-runner worker disable billing.propose_small_credit \
190
+ --digest sha256:EXACT_DIGEST --yes --config ./synapsor.runner.json
191
+ synapsor-runner worker enable billing.propose_small_credit \
192
+ --digest sha256:EXACT_DIGEST --yes --config ./synapsor.runner.json
193
+ synapsor-runner worker revoke billing.propose_small_credit \
194
+ --digest sha256:EXACT_DIGEST --yes --config ./synapsor.runner.json
195
+ ```
196
+
197
+ `revoke` is terminal for that digest. Pause, drain, disable, and revoke stop new
198
+ leasing but preserve queued proposals and durable history. They do not interrupt
199
+ a transaction that already committed.
200
+
201
+ Dead-letter recovery remains explicit and identity-verified:
202
+
203
+ ```bash
204
+ synapsor-runner worker dead-letter list --config ./synapsor.runner.json
205
+ synapsor-runner worker dead-letter show wrp_... --config ./synapsor.runner.json
206
+ synapsor-runner worker dead-letter requeue wrp_... --retry-budget 3 --yes \
207
+ --config ./synapsor.runner.json --identity alice --identity-key ./alice.pem
208
+ ```
209
+
210
+ Use the secured Workbench for the same queue, pause/drain, exact-digest,
211
+ dead-letter, and reconciliation controls.
212
+
213
+ ## Retry And Crash Boundary
214
+
215
+ Runner may retry only a classified transient failure with a proven non-commit
216
+ outcome. Version conflict, stale authority, scope failure, limit failure,
217
+ validation failure, and receipt mismatch do not retry automatically.
218
+
219
+ Delivery may repeat; mutation must not. Worker leases are fenced, and source
220
+ operation identity plus receipts prevent duplicate effects.
221
+
222
+ When the database outcome is unknown, Runner does not guess:
223
+
224
+ - source-receipt mode resolves a known committed operation from the source-side
225
+ receipt;
226
+ - genuinely ambiguous runner-ledger outcomes enter
227
+ `reconciliation_required`;
228
+ - automatic retry stops until a verified operator inspects and resolves the
229
+ established reconciliation record.
230
+
231
+ This is safe retry and explicit ambiguity handling, not a claim of distributed
232
+ exactly-once execution.
233
+
234
+ ## Optional Supervision-Health Gate
235
+
236
+ An exact capability policy may name `required_attention_sinks`:
237
+
238
+ ```json
239
+ {
240
+ "required_attention_sinks": ["operations"]
241
+ }
242
+ ```
243
+
244
+ When present, the worker leaves eligible work queued until every named,
245
+ enabled sink has a healthy delivery record. Sink health never approves a
246
+ proposal and never bypasses either execution opt-in. This gate is off unless a
247
+ capability explicitly names required sinks.
248
+
249
+ See [Human Attention And Notifications](human-attention-notifications.md) for
250
+ quiet defaults, signed webhooks, routing, and the Workbench inbox.
251
+
252
+ ## Accurate Model-Facing Claim
253
+
254
+ Without supervised execution:
255
+
256
+ > This call creates a proposal and does not change the source database.
257
+
258
+ With exact supervised execution enabled:
259
+
260
+ > This call creates a proposal. If it satisfies the reviewed automatic-approval
261
+ > policy, a separately trusted Runner worker may automatically apply it without
262
+ > a per-request human click. The model cannot approve, apply, start the worker,
263
+ > or change that policy.
264
+
265
+ The model can therefore cause a bounded request to enter a pre-authorized
266
+ production pipeline. It still cannot choose the authority, approve the
267
+ proposal, control the worker, or commit a mutation directly.
@@ -12,6 +12,41 @@ Use JSON for automation:
12
12
  npx -y -p @synapsor/runner synapsor-runner doctor --first-run --json
13
13
  ```
14
14
 
15
+ ## Guided Recovery Contract
16
+
17
+ Runner 1.6.3 failures should tell you what failed, why the boundary stopped,
18
+ what state remains, and one next action. Do not delete the project or add
19
+ `--force` merely to recover.
20
+
21
+ | Failure | State preserved | One next action |
22
+ | --- | --- | --- |
23
+ | Database connection or metadata inspection failed | Existing project/review files and source rows | Fix the exported URL/network without printing the credential, then rerun `npx -y @synapsor/runner@latest start --from-env DATABASE_URL`. |
24
+ | Read role is writable, owner, superuser, `BYPASSRLS`, or unverifiable | Disabled metadata draft; no source-row Explore | Supply a verifiably SELECT-only non-owner staging role, then rerun the same `start` command. |
25
+ | Schema or project choice is ambiguous | Existing files; no authority activation | Rerun with the exact reviewed schema, for example `npx -y @synapsor/runner@latest start --from-env DATABASE_URL --schema public`. |
26
+ | Tenant or principal scope is unresolved | Conservative blocked resource decisions | Open the same Workbench URL and resolve the highlighted scope exception. |
27
+ | Sensitive field remains unresolved | Field stays kept out; active tools unchanged | Open Workbench **Exceptions** and record one reviewed field decision. |
28
+ | Row identifier is missing/composite/ambiguous | Resource remains blocked | Select a source-proven single-column primary/unique identity or keep the resource blocked. |
29
+ | Trusted context environment is missing | Boundary and ledger remain intact; query did not run | Export the named tenant/principal variable locally, then rerun the displayed Try action. |
30
+ | Generated output already exists | Existing files are not overwritten | Rerun the original `start` command and choose **Resume existing review**. |
31
+ | Generation lock is stale | Existing active named capability and review history | Run `synapsor-runner boundary diff --json`, then choose **Rescan and review changes**. |
32
+ | Config JSON is malformed | Config and source database are unchanged | Correct the reported file/line/column, then run `synapsor-runner config validate --config ./synapsor.runner.json --json`. |
33
+ | Config mode is missing/invalid | Config and source database are unchanged | Set `mode` to `read_only`, `shadow`, `review`, or `cloud`, then rerun `synapsor-runner config validate --json`. |
34
+ | Config contains an unknown field | Config and source database are unchanged | Remove or correct the reported JSON path, then rerun `synapsor-runner config validate --json`. |
35
+ | Workbench port is occupied | Review files, ledger, and source database | Rerun `synapsor-runner ui --open`; the default selects a free loopback port. |
36
+ | Writeback setup fails | Config and reviewed plan; transactional setup rolls back | Rerun `synapsor-runner writeback setup --profile staging --json` and review the reported prerequisite. |
37
+ | Writer role/setup URL is missing | No DDL or grant was applied | Rerun the preview with `--writer-role <role> --setup-url-env <ADMIN_URL_ENV>`. |
38
+ | No supported write candidate exists | Read boundary remains active; no write authority | Use **Add a safe action** on a writable base table with a proven identity/version field, or retain read-only mode. |
39
+ | MCP client installation is unavailable | Project and reviewed authority; client config unchanged | Run `synapsor-runner mcp config --absolute-paths` and use the generic stdio snippet manually. |
40
+ | Supervised work remains queued | Approved proposal and queue state; source unchanged | Run `synapsor-runner worker status --json`, then follow its exact digest, posture, policy, freshness, limit, pause, or required-sink finding. |
41
+ | Required attention sink is unhealthy | Approved proposal remains queued; no source mutation | Repair the operator-owned sink, run `synapsor-runner notifications test --sink <id>`, then dispatch and let the worker revalidate from the beginning. |
42
+ | Notification delivery is dead-lettered | Authoritative event and proposal state are preserved | Repair/test the sink, then run `synapsor-runner notifications replay latest --yes --reason "sink repaired"` with the configured verified operator identity; this resends only the redacted event. |
43
+ | Apply outcome is UNKNOWN | Intent, receipt evidence, and queue state are preserved | Open the latest critical attention item and use the verified reconciliation flow; never rerun the mutation blindly. |
44
+
45
+ For machine output, a process-level failure emits one redacted JSON object to
46
+ stdout and diagnostics to stderr. `recovery.source_database_changed` is `null`
47
+ when a generic exception cannot establish an operation-specific mutation
48
+ outcome; inspect the durable receipt/reconciliation state rather than guessing.
49
+
15
50
  ## Fresh Start Did Not Enter Auto Boundary
16
51
 
17
52
  Auto Boundary is the default only for a fresh interactive `start --from-env`
File without changes
@@ -0,0 +1,43 @@
1
+ # FitFlow Guided Onboarding Fixture
2
+
3
+ FitFlow is a synthetic multi-tenant fitness application used to verify the
4
+ Runner 1.6.3 first-time developer journey.
5
+
6
+ Its live PostgreSQL schema contains 41 relations: six seeded core relations,
7
+ 34 realistic surrounding application subsystems, and one readiness marker.
8
+ The broader catalog proves that review by exception can narrow a real
9
+ application-sized schema to a small active agent pack without blind bulk
10
+ approval.
11
+
12
+ The fixture deliberately separates two reviewed authority packs:
13
+
14
+ - `organization_analytics`: organization-scoped, aggregate-only analysis over
15
+ check-ins and locations;
16
+ - `trainer_members`: exact member reads and proposal-first member updates,
17
+ restricted to the assigned trainer.
18
+
19
+ The source includes planted fields that must never become visible by default:
20
+ `payment_method`, `home_address`, and `medical_waiver_notes`.
21
+
22
+ The PostgreSQL fixture provides separate roles:
23
+
24
+ - `fitflow_analytics_reader`: read-only, organization-scoped analytics;
25
+ - `fitflow_trainer_reader`: read-only, organization and trainer scoped;
26
+ - `fitflow_writer`: guarded member updates under the same trusted scope;
27
+ - `fitflow_setup`: development/staging receipt-table setup only.
28
+
29
+ Start the database:
30
+
31
+ ```bash
32
+ docker compose up -d --wait postgres
33
+ ```
34
+
35
+ Use the organization analytics role for the first onboarding journey:
36
+
37
+ ```bash
38
+ export DATABASE_URL='postgresql://fitflow_analytics_reader:fitflow_analytics_reader_password@127.0.0.1:55463/fitflow'
39
+ export SYNAPSOR_TENANT_ID='org-fitflow'
40
+ npx -y @synapsor/runner@latest start --from-env DATABASE_URL
41
+ ```
42
+
43
+ Use only synthetic data. The fixture is not a production deployment template.
@@ -0,0 +1,8 @@
1
+ export default function FitFlowOverview() {
2
+ return (
3
+ <main>
4
+ <h1>FitFlow operations</h1>
5
+ <p>Synthetic member, trainer, class, and check-in data for Synapsor onboarding tests.</p>
6
+ </main>
7
+ );
8
+ }
@@ -0,0 +1,16 @@
1
+ services:
2
+ postgres:
3
+ image: postgres:16
4
+ environment:
5
+ POSTGRES_DB: fitflow
6
+ POSTGRES_USER: fitflow_admin
7
+ POSTGRES_PASSWORD: fitflow_admin_password
8
+ ports:
9
+ - "127.0.0.1:55463:5432"
10
+ healthcheck:
11
+ test: ["CMD-SHELL", "test \"$(psql -U fitflow_admin -d fitflow -tAc \"SELECT pg_postmaster_start_time() > initialized_at FROM public.synapsor_fixture_ready LIMIT 1\" 2>/dev/null | xargs)\" = t"]
12
+ interval: 1s
13
+ timeout: 3s
14
+ retries: 40
15
+ volumes:
16
+ - ./seed/postgres.sql:/docker-entrypoint-initdb.d/001-schema.sql:ro
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "synapsor-fitflow-guided-onboarding",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev"
7
+ },
8
+ "dependencies": {
9
+ "@prisma/client": "^6.0.0",
10
+ "next": "^15.0.0",
11
+ "react": "^19.0.0",
12
+ "react-dom": "^19.0.0"
13
+ },
14
+ "devDependencies": {
15
+ "prisma": "^6.0.0",
16
+ "typescript": "^5.8.0"
17
+ }
18
+ }
@@ -0,0 +1,98 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "postgresql"
7
+ url = env("DATABASE_URL")
8
+ }
9
+
10
+ model Organization {
11
+ id String @id
12
+ name String
13
+ locations Location[]
14
+ trainers Trainer[]
15
+ members Member[]
16
+ classes Class[]
17
+ checkIns CheckIn[]
18
+
19
+ @@map("organizations")
20
+ }
21
+
22
+ model Location {
23
+ id String @id
24
+ organizationId String @map("organization_id")
25
+ name String
26
+ region String
27
+ organization Organization @relation(fields: [organizationId], references: [id])
28
+ members Member[]
29
+ classes Class[]
30
+ checkIns CheckIn[]
31
+
32
+ @@unique([organizationId, name])
33
+ @@map("locations")
34
+ }
35
+
36
+ model Trainer {
37
+ id String @id
38
+ organizationId String @map("organization_id")
39
+ displayName String @map("display_name")
40
+ organization Organization @relation(fields: [organizationId], references: [id])
41
+ members Member[]
42
+ classes Class[]
43
+
44
+ @@map("trainers")
45
+ }
46
+
47
+ model Member {
48
+ id String @id
49
+ organizationId String @map("organization_id")
50
+ locationId String @map("location_id")
51
+ assignedTrainerId String @map("assigned_trainer_id")
52
+ membershipStatus String @map("membership_status")
53
+ membershipTier String @map("membership_tier")
54
+ loyaltyBalance Int @map("loyalty_balance")
55
+ version Int
56
+ paymentMethod String @map("payment_method")
57
+ homeAddress String @map("home_address")
58
+ medicalWaiverNotes String @map("medical_waiver_notes")
59
+ organization Organization @relation(fields: [organizationId], references: [id])
60
+ location Location @relation(fields: [locationId], references: [id])
61
+ assignedTrainer Trainer @relation(fields: [assignedTrainerId], references: [id])
62
+ checkIns CheckIn[]
63
+
64
+ @@index([organizationId, assignedTrainerId])
65
+ @@map("members")
66
+ }
67
+
68
+ model Class {
69
+ id String @id
70
+ organizationId String @map("organization_id")
71
+ locationId String @map("location_id")
72
+ trainerId String @map("trainer_id")
73
+ classType String @map("class_type")
74
+ startsAt DateTime @map("starts_at") @db.Timestamptz(6)
75
+ organization Organization @relation(fields: [organizationId], references: [id])
76
+ location Location @relation(fields: [locationId], references: [id])
77
+ trainer Trainer @relation(fields: [trainerId], references: [id])
78
+ checkIns CheckIn[]
79
+
80
+ @@map("classes")
81
+ }
82
+
83
+ model CheckIn {
84
+ id String @id
85
+ organizationId String @map("organization_id")
86
+ locationId String @map("location_id")
87
+ memberId String @map("member_id")
88
+ classId String @map("class_id")
89
+ outcome String
90
+ checkedInAt DateTime @map("checked_in_at") @db.Timestamptz(6)
91
+ organization Organization @relation(fields: [organizationId], references: [id])
92
+ location Location @relation(fields: [locationId], references: [id])
93
+ member Member @relation(fields: [memberId], references: [id])
94
+ fitnessClass Class @relation(fields: [classId], references: [id])
95
+
96
+ @@index([organizationId, checkedInAt])
97
+ @@map("check_ins")
98
+ }