@shipeasy/openapi 2.2.0 → 2.3.0

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 (43) hide show
  1. package/dist/{chunk-643YSMLU.js → chunk-LYNWK55D.js} +23 -1
  2. package/dist/chunk-LYNWK55D.js.map +1 -0
  3. package/dist/{chunk-MRW3DJ57.js → chunk-PRN4AEAM.js} +250 -100
  4. package/dist/chunk-PRN4AEAM.js.map +1 -0
  5. package/dist/{client-K0ShmsC7.d.ts → client-DO5HTb_L.d.ts} +690 -143
  6. package/dist/client.d.ts +1 -1
  7. package/dist/client.js +7 -1
  8. package/dist/custom.js +4 -4
  9. package/dist/custom.js.map +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +8 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/schemas.d.ts +686 -73
  14. package/dist/schemas.js +41 -5
  15. package/openapi.json +1486 -521
  16. package/openapi.yaml +1016 -350
  17. package/package.json +1 -1
  18. package/spec/components/errors.yaml +30 -1
  19. package/spec/components/schemas/common.yaml +6 -0
  20. package/spec/components/schemas/errors-api.yaml +66 -3
  21. package/spec/components/schemas/experiment-api-row.yaml +293 -0
  22. package/spec/components/schemas/experiments.yaml +54 -299
  23. package/spec/components/schemas/gates.yaml +19 -0
  24. package/spec/components/schemas/keys.yaml +2 -1
  25. package/spec/components/schemas/metrics.yaml +17 -17
  26. package/spec/components/schemas/ops.yaml +101 -0
  27. package/spec/components/schemas/projects.yaml +119 -1
  28. package/spec/components/schemas/universes.yaml +83 -1
  29. package/spec/openapi.yaml +14 -0
  30. package/spec/paths/alert-rules.yaml +1 -0
  31. package/spec/paths/configs.yaml +1 -0
  32. package/spec/paths/errors.yaml +96 -2
  33. package/spec/paths/events.yaml +1 -0
  34. package/spec/paths/experiments.yaml +8 -0
  35. package/spec/paths/gates.yaml +1 -0
  36. package/spec/paths/keys.yaml +1 -0
  37. package/spec/paths/killswitches.yaml +1 -0
  38. package/spec/paths/metrics.yaml +1 -0
  39. package/spec/paths/ops.yaml +111 -0
  40. package/spec/paths/projects.yaml +2 -2
  41. package/spec/paths/universes.yaml +1 -0
  42. package/dist/chunk-643YSMLU.js.map +0 -1
  43. package/dist/chunk-MRW3DJ57.js.map +0 -1
package/openapi.json CHANGED
@@ -98,6 +98,11 @@
98
98
  },
99
99
  "description": "Alert rules: the metric-threshold definitions the analysis cron evaluates each run.\n\n**What fires.** Each rule binds a `metricId`, a `comparator` (`gt`/`gte`/`lt`/`lte`), and a `threshold`. On every cron pass the cron aggregates the metric over the trailing `windowHours` and raises an alert at `severity` when `value comparator threshold` holds.\n\n**Immutable metric.** The bound metric (and its aggregation) is fixed at create time — there is no update path for `metricId`. Tune `threshold`/`comparator`/`windowHours`/`severity`/`name`/`enabled` instead, or delete + recreate to repoint the rule at a different metric.\n\n**Delivery.** `notify` optionally targets a Slack channel and/or email for this rule; `null` falls back to the project's default notification settings. Slack targets require a connected Slack connector."
100
100
  },
101
+ {
102
+ "name": "Comments",
103
+ "parent": "Ops",
104
+ "description": "Comments on a queue item — the discussion thread that hangs off any bug,\nfeature request, or auto-filed error/alert ticket. Append a comment or read\nthe thread; one level of threaded replies via `parentId`.\n\n**Mentions.** `@teammate` in a body raises an in-app notification for that\nperson; `@shipeasy` asks Jarvis (the AI agent) to read the item and reply.\nComments authored by Jarvis carry `authorType: system`."
105
+ },
101
106
  {
102
107
  "name": "Projects",
103
108
  "description": "Projects: the account-level container every other resource is scoped to.\n\n**Account-level, not bound-project-level.** Both operations resolve from the caller's credential rather than the `.shipeasy`-bound project — `current` reads the project the auth header maps to, and `upsert` find-or-creates under the session's owner. Neither touches the local `.shipeasy` binding; recording the result there is a consumer side-effect layered on top.\n\n**Idempotent upsert.** A project is keyed by `(owner_email, domain)`. Calling `upsert` again with the same domain returns the existing project with `created: false`, so it is safe to run on every install."
@@ -415,7 +420,8 @@
415
420
  "UNAUTHORIZED",
416
421
  "FORBIDDEN",
417
422
  "ALREADY_EXISTS",
418
- "VALIDATION"
423
+ "VALIDATION",
424
+ "PLAN_LIMIT"
419
425
  ],
420
426
  "x-cli": {
421
427
  "name": "create",
@@ -1136,7 +1142,7 @@
1136
1142
  "content": {
1137
1143
  "application/json": {
1138
1144
  "schema": {
1139
- "$ref": "#/components/schemas/GetExperimentResponse"
1145
+ "$ref": "#/components/schemas/ExperimentApiRow"
1140
1146
  },
1141
1147
  "example": {
1142
1148
  "id": "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
@@ -1324,7 +1330,8 @@
1324
1330
  "INVALID_TRANSITION",
1325
1331
  "GROUPS_WEIGHT_SUM",
1326
1332
  "REFERENCED_NOT_FOUND",
1327
- "VALIDATION"
1333
+ "VALIDATION",
1334
+ "EXPERIMENT_IMMUTABLE_FIELD"
1328
1335
  ],
1329
1336
  "x-cli": {
1330
1337
  "name": "update",
@@ -1504,7 +1511,13 @@
1504
1511
  "FORBIDDEN",
1505
1512
  "NOT_FOUND",
1506
1513
  "INVALID_TRANSITION",
1507
- "VALIDATION"
1514
+ "VALIDATION",
1515
+ "PLAN_LIMIT",
1516
+ "EXPERIMENT_NO_GOAL_METRIC",
1517
+ "EXPERIMENT_ARCHIVED_RESTART",
1518
+ "EXPERIMENT_NOT_RUNNING",
1519
+ "EXPERIMENT_RUNNING_ARCHIVE",
1520
+ "EXPERIMENT_RESTORE_INVALID"
1508
1521
  ],
1509
1522
  "x-cli": {
1510
1523
  "positional": [
@@ -1643,7 +1656,8 @@
1643
1656
  "NOT_FOUND",
1644
1657
  "EVENT_PENDING",
1645
1658
  "REFERENCED_NOT_FOUND",
1646
- "VALIDATION"
1659
+ "VALIDATION",
1660
+ "METRIC_UNKNOWN_ID"
1647
1661
  ],
1648
1662
  "x-cli": {
1649
1663
  "name": "set-metrics",
@@ -2223,7 +2237,8 @@
2223
2237
  "UNAUTHORIZED",
2224
2238
  "FORBIDDEN",
2225
2239
  "ALREADY_EXISTS",
2226
- "VALIDATION"
2240
+ "VALIDATION",
2241
+ "PLAN_LIMIT"
2227
2242
  ],
2228
2243
  "x-cli": {
2229
2244
  "name": "create",
@@ -3271,7 +3286,8 @@
3271
3286
  "UNAUTHORIZED",
3272
3287
  "FORBIDDEN",
3273
3288
  "ALREADY_EXISTS",
3274
- "VALIDATION"
3289
+ "VALIDATION",
3290
+ "PLAN_LIMIT"
3275
3291
  ],
3276
3292
  "x-cli": {
3277
3293
  "name": "create",
@@ -4030,7 +4046,8 @@
4030
4046
  "FORBIDDEN",
4031
4047
  "PLAN_REQUIRED",
4032
4048
  "ALREADY_EXISTS",
4033
- "VALIDATION"
4049
+ "VALIDATION",
4050
+ "PLAN_LIMIT"
4034
4051
  ],
4035
4052
  "x-cli": {
4036
4053
  "name": "create",
@@ -5194,7 +5211,8 @@
5194
5211
  "ALREADY_EXISTS",
5195
5212
  "EVENT_PENDING",
5196
5213
  "REFERENCED_NOT_FOUND",
5197
- "VALIDATION"
5214
+ "VALIDATION",
5215
+ "PLAN_LIMIT"
5198
5216
  ],
5199
5217
  "x-cli": {
5200
5218
  "name": "create",
@@ -5623,7 +5641,8 @@
5623
5641
  "UNAUTHORIZED",
5624
5642
  "FORBIDDEN",
5625
5643
  "ALREADY_EXISTS",
5626
- "VALIDATION"
5644
+ "VALIDATION",
5645
+ "PLAN_LIMIT"
5627
5646
  ],
5628
5647
  "x-cli": {
5629
5648
  "name": "create",
@@ -6470,6 +6489,156 @@
6470
6489
  }
6471
6490
  }
6472
6491
  },
6492
+ "/api/admin/ops/{handle}/comments": {
6493
+ "get": {
6494
+ "operationId": "listOpsComments",
6495
+ "summary": "List an item's comments",
6496
+ "description": "List the comment thread on a queue item, oldest first. Each comment\ncarries its author (a teammate email, or `system` for a comment authored by\nJarvis — the AI agent), its markdown body, and `parentId` for the single\nlevel of threaded replies. Removed comments are omitted.\n\n**Use case:** Read the discussion on an item before replying or acting on it.",
6497
+ "tags": [
6498
+ "Comments"
6499
+ ],
6500
+ "parameters": [
6501
+ {
6502
+ "$ref": "#/components/parameters/ProjectId"
6503
+ },
6504
+ {
6505
+ "name": "handle",
6506
+ "in": "path",
6507
+ "required": true,
6508
+ "description": "Per-project item number (e.g. `7`) or the full ops item id.",
6509
+ "schema": {
6510
+ "$ref": "#/components/schemas/ResourceId"
6511
+ }
6512
+ }
6513
+ ],
6514
+ "responses": {
6515
+ "200": {
6516
+ "description": "List an item's comments",
6517
+ "content": {
6518
+ "application/json": {
6519
+ "schema": {
6520
+ "$ref": "#/components/schemas/ListOpsCommentsResponse"
6521
+ }
6522
+ }
6523
+ }
6524
+ },
6525
+ "400": {
6526
+ "$ref": "#/components/responses/BadRequest"
6527
+ },
6528
+ "401": {
6529
+ "$ref": "#/components/responses/Unauthorized"
6530
+ },
6531
+ "403": {
6532
+ "$ref": "#/components/responses/Forbidden"
6533
+ },
6534
+ "404": {
6535
+ "$ref": "#/components/responses/NotFound"
6536
+ }
6537
+ },
6538
+ "x-error-codes": [
6539
+ "BAD_REQUEST",
6540
+ "UNAUTHORIZED",
6541
+ "FORBIDDEN",
6542
+ "NOT_FOUND"
6543
+ ],
6544
+ "x-cli": {
6545
+ "name": "list",
6546
+ "positional": [
6547
+ "handle"
6548
+ ]
6549
+ }
6550
+ },
6551
+ "post": {
6552
+ "operationId": "createOpsComment",
6553
+ "summary": "Comment on an item",
6554
+ "description": "Append a comment to a queue item's thread. The body is markdown (mentions\nlike `@teammate` notify that person; `@shipeasy` asks Jarvis, the AI agent,\nto reply). Pass `parentId` to reply under an existing top-level comment\n(one level of threading — a reply to a reply attaches to the same parent).\n\nCreate-only and append-only, so it is safe for restricted ops keys — the\nsame channel the ops loop and Jarvis use to leave a note on an item.",
6555
+ "tags": [
6556
+ "Comments"
6557
+ ],
6558
+ "parameters": [
6559
+ {
6560
+ "$ref": "#/components/parameters/ProjectId"
6561
+ },
6562
+ {
6563
+ "name": "handle",
6564
+ "in": "path",
6565
+ "required": true,
6566
+ "description": "Per-project item number (e.g. `7`) or the full ops item id.",
6567
+ "schema": {
6568
+ "$ref": "#/components/schemas/ResourceId"
6569
+ }
6570
+ }
6571
+ ],
6572
+ "requestBody": {
6573
+ "required": true,
6574
+ "content": {
6575
+ "application/json": {
6576
+ "schema": {
6577
+ "$ref": "#/components/schemas/CreateOpsCommentRequest"
6578
+ },
6579
+ "examples": {
6580
+ "comment": {
6581
+ "summary": "Leave a comment",
6582
+ "value": {
6583
+ "body": "Repro'd on iOS Safari 17 — the button overflows its container. cc @alice"
6584
+ }
6585
+ },
6586
+ "reply": {
6587
+ "summary": "Reply under an existing comment",
6588
+ "value": {
6589
+ "body": "Agreed, let's ship the fix behind @shipeasy's suggested flag.",
6590
+ "parentId": "cm_01j7w8a1b2c3d4e5f6g7h8i9j0"
6591
+ }
6592
+ }
6593
+ }
6594
+ }
6595
+ }
6596
+ },
6597
+ "responses": {
6598
+ "201": {
6599
+ "description": "Comment on an item",
6600
+ "content": {
6601
+ "application/json": {
6602
+ "schema": {
6603
+ "$ref": "#/components/schemas/CreateOpsCommentResponse"
6604
+ },
6605
+ "example": {
6606
+ "id": "cm_01j7w8a1b2c3d4e5f6g7h8i9j0"
6607
+ }
6608
+ }
6609
+ }
6610
+ },
6611
+ "400": {
6612
+ "$ref": "#/components/responses/BadRequest"
6613
+ },
6614
+ "401": {
6615
+ "$ref": "#/components/responses/Unauthorized"
6616
+ },
6617
+ "403": {
6618
+ "$ref": "#/components/responses/Forbidden"
6619
+ },
6620
+ "404": {
6621
+ "$ref": "#/components/responses/NotFound"
6622
+ },
6623
+ "422": {
6624
+ "$ref": "#/components/responses/UnprocessableEntity"
6625
+ }
6626
+ },
6627
+ "x-error-codes": [
6628
+ "BAD_REQUEST",
6629
+ "UNAUTHORIZED",
6630
+ "FORBIDDEN",
6631
+ "NOT_FOUND",
6632
+ "VALIDATION"
6633
+ ],
6634
+ "x-cli": {
6635
+ "name": "create",
6636
+ "positional": [
6637
+ "handle"
6638
+ ]
6639
+ }
6640
+ }
6641
+ },
6473
6642
  "/api/admin/notifications": {
6474
6643
  "post": {
6475
6644
  "operationId": "notifyOps",
@@ -6791,6 +6960,7 @@
6791
6960
  "UNAUTHORIZED",
6792
6961
  "FORBIDDEN",
6793
6962
  "ALREADY_EXISTS",
6963
+ "METRIC_NOT_FOUND",
6794
6964
  "REFERENCED_NOT_FOUND",
6795
6965
  "VALIDATION"
6796
6966
  ],
@@ -6996,7 +7166,7 @@
6996
7166
  "name": "Acme",
6997
7167
  "domain": "acme.com",
6998
7168
  "ownerEmail": "owner@acme.com",
6999
- "plan": "paid",
7169
+ "plan": "pro",
7000
7170
  "status": "active",
7001
7171
  "subscriptionStatus": "active",
7002
7172
  "billingInterval": "monthly",
@@ -7191,7 +7361,7 @@
7191
7361
  "name": "Acme Rockets",
7192
7362
  "domain": "acme.com",
7193
7363
  "ownerEmail": "owner@acme.com",
7194
- "plan": "paid",
7364
+ "plan": "pro",
7195
7365
  "status": "active",
7196
7366
  "subscriptionStatus": "active",
7197
7367
  "billingInterval": "monthly",
@@ -8167,7 +8337,7 @@
8167
8337
  "get": {
8168
8338
  "operationId": "getError",
8169
8339
  "summary": "Get a tracked error",
8170
- "description": "Returns a single tracked error by its id, including the latest occurrence's stack, extras, and consequence. Returns `404` if no such error exists in the project.\n\n**Use case:** Drill into one issue — fetch its full stack and `seenUrls` to investigate, or to render the detail panel behind a row in the errors list.",
8340
+ "description": "Returns a single tracked error by its id, including the latest occurrence's stack, extras, and consequence, plus `occurrences` — the sampled per-instance detail rows behind the issue (newest first; exhaustive while the issue is small, thinned at volume, capped at 100). Returns `404` if no such error exists in the project.\n\n**Use case:** Drill into one issue — fetch its full stack and `seenUrls` to investigate, or walk `occurrences` to see how the failing message/stack varies across instances.",
8171
8341
  "tags": [
8172
8342
  "Errors"
8173
8343
  ],
@@ -8216,7 +8386,33 @@
8216
8386
  "firstSeenAt": "2026-06-20T08:11:02.000Z",
8217
8387
  "lastSeenAt": "2026-06-28T14:55:31.000Z",
8218
8388
  "createdAt": "2026-06-20T08:11:02.000Z",
8219
- "updatedAt": "2026-06-28T14:55:31.000Z"
8389
+ "updatedAt": "2026-06-28T14:55:31.000Z",
8390
+ "occurrences": [
8391
+ {
8392
+ "id": "occ_01j7w7qq0v3k9d2m5rc8t1xh4n",
8393
+ "message": "Cannot read properties of undefined (reading 'id')",
8394
+ "stack": "TypeError: Cannot read properties of undefined (reading 'id')\n at GateEditor (/app/gates/[id]/page.tsx:42:18)",
8395
+ "url": "https://app.example.com/dashboard/gat_01j7/edit",
8396
+ "env": "prod",
8397
+ "side": "client",
8398
+ "sdkVersion": "6.0.0",
8399
+ "extrasJson": "{\"gateId\":\"gat_01j7\"}",
8400
+ "sampleRate": 100,
8401
+ "seenAt": "2026-06-28T14:55:31.000Z"
8402
+ },
8403
+ {
8404
+ "id": "occ_01j7w7pk8s6e3f9b0zad7y5w2q",
8405
+ "message": "Cannot read properties of undefined (reading 'id')",
8406
+ "stack": "TypeError: Cannot read properties of undefined (reading 'id')\n at GateEditor (/app/gates/[id]/page.tsx:42:18)",
8407
+ "url": "https://app.example.com/dashboard/gat_02c4/edit",
8408
+ "env": "prod",
8409
+ "side": "client",
8410
+ "sdkVersion": "6.0.0",
8411
+ "extrasJson": "{\"gateId\":\"gat_02c4\"}",
8412
+ "sampleRate": 10,
8413
+ "seenAt": "2026-06-24T09:12:05.000Z"
8414
+ }
8415
+ ]
8220
8416
  }
8221
8417
  }
8222
8418
  }
@@ -8437,6 +8633,96 @@
8437
8633
  }
8438
8634
  }
8439
8635
  },
8636
+ "/api/admin/errors/{id}/resolve": {
8637
+ "post": {
8638
+ "operationId": "resolveError",
8639
+ "summary": "Resolve a tracked error",
8640
+ "description": "Marks one tracked error `resolved` — the single-purpose \"close out\" action. Takes no body; it is `PATCH /api/admin/errors/{id}` pinned to `{ \"status\": \"resolved\" }`, exposed so tooling can close an error without being handed the full open/resolved/ignored status machine. A resolved error reopens automatically (ingestion-side) if it recurs, so resolving is always safe: a premature resolve un-does itself on the next occurrence. Returns the updated row; `404` if the error does not exist.\n\n**Use case:** Close out a tracked error from an agent or script once its fix has shipped — e.g. after a deploy, resolve every open issue the change addressed and let recurrence reopen anything that wasn't actually fixed.",
8641
+ "tags": [
8642
+ "Errors"
8643
+ ],
8644
+ "parameters": [
8645
+ {
8646
+ "$ref": "#/components/parameters/ProjectId"
8647
+ },
8648
+ {
8649
+ "name": "id",
8650
+ "in": "path",
8651
+ "required": true,
8652
+ "description": "Stable opaque error id (`err_…`).",
8653
+ "schema": {
8654
+ "$ref": "#/components/schemas/ResourceId"
8655
+ }
8656
+ }
8657
+ ],
8658
+ "responses": {
8659
+ "200": {
8660
+ "description": "Resolve a tracked error",
8661
+ "content": {
8662
+ "application/json": {
8663
+ "schema": {
8664
+ "$ref": "#/components/schemas/ErrorRecord"
8665
+ },
8666
+ "example": {
8667
+ "id": "err_01j7w7m9q4hxbf6npe6s9zr3vc",
8668
+ "projectId": "e976b15e-2f0d-4c6e-9b1a-3a7c1f2d8e90",
8669
+ "fingerprint": "9c1f4f1f2c0c4a5fa1c2b6d3e7c8e3a1",
8670
+ "causedByFingerprint": null,
8671
+ "message": "Cannot read properties of undefined (reading 'id')",
8672
+ "errorType": "TypeError",
8673
+ "stack": null,
8674
+ "source": "shipeasy",
8675
+ "url": "https://app.example.com/dashboard/gat_01j7/edit",
8676
+ "seenUrls": null,
8677
+ "subject": "gate editor",
8678
+ "outcome": "crash on load",
8679
+ "side": "client",
8680
+ "env": "prod",
8681
+ "kind": "uncaught",
8682
+ "lastExtrasJson": null,
8683
+ "sdkVersion": "6.0.0",
8684
+ "count": 137,
8685
+ "status": "resolved",
8686
+ "firstSeenAt": "2026-06-20T08:11:02.000Z",
8687
+ "lastSeenAt": "2026-06-28T14:55:31.000Z",
8688
+ "createdAt": "2026-06-20T08:11:02.000Z",
8689
+ "updatedAt": "2026-06-28T15:02:10.000Z"
8690
+ }
8691
+ }
8692
+ }
8693
+ },
8694
+ "400": {
8695
+ "$ref": "#/components/responses/BadRequest"
8696
+ },
8697
+ "401": {
8698
+ "$ref": "#/components/responses/Unauthorized"
8699
+ },
8700
+ "403": {
8701
+ "$ref": "#/components/responses/Forbidden"
8702
+ },
8703
+ "404": {
8704
+ "$ref": "#/components/responses/NotFound"
8705
+ },
8706
+ "409": {
8707
+ "$ref": "#/components/responses/Conflict"
8708
+ },
8709
+ "422": {
8710
+ "$ref": "#/components/responses/UnprocessableEntity"
8711
+ }
8712
+ },
8713
+ "x-error-codes": [
8714
+ "UNAUTHORIZED",
8715
+ "FORBIDDEN",
8716
+ "NOT_FOUND"
8717
+ ],
8718
+ "x-cli": {
8719
+ "name": "resolve",
8720
+ "positional": [
8721
+ "id"
8722
+ ]
8723
+ }
8724
+ }
8725
+ },
8440
8726
  "/api/admin/errors/{id}/series": {
8441
8727
  "post": {
8442
8728
  "operationId": "getErrorSeries",
@@ -9630,7 +9916,8 @@
9630
9916
  "x-error-codes": [
9631
9917
  "UNAUTHORIZED",
9632
9918
  "FORBIDDEN",
9633
- "VALIDATION"
9919
+ "VALIDATION",
9920
+ "PLAN_LIMIT"
9634
9921
  ],
9635
9922
  "x-cli": {
9636
9923
  "name": "create"
@@ -9877,6 +10164,14 @@
9877
10164
  ],
9878
10165
  "description": "Master switch. Returned as `1`/`0` from D1; treat truthy as enabled."
9879
10166
  },
10167
+ "type": {
10168
+ "type": "string",
10169
+ "enum": [
10170
+ "targeting",
10171
+ "holdout"
10172
+ ],
10173
+ "description": "Gate kind — `targeting` (normal flag) or `holdout` (restricted public % + whitelist)."
10174
+ },
9880
10175
  "rolloutPct": {
9881
10176
  "type": "integer",
9882
10177
  "minimum": 0,
@@ -10287,7 +10582,16 @@
10287
10582
  "REFERENCED_NOT_FOUND",
10288
10583
  "GROUPS_WEIGHT_SUM",
10289
10584
  "EVENT_PENDING",
10290
- "INTERNAL"
10585
+ "INTERNAL",
10586
+ "PLAN_LIMIT",
10587
+ "EXPERIMENT_NO_GOAL_METRIC",
10588
+ "EXPERIMENT_ARCHIVED_RESTART",
10589
+ "EXPERIMENT_RESTORE_INVALID",
10590
+ "EXPERIMENT_NOT_RUNNING",
10591
+ "EXPERIMENT_RUNNING_ARCHIVE",
10592
+ "EXPERIMENT_IMMUTABLE_FIELD",
10593
+ "METRIC_NOT_FOUND",
10594
+ "METRIC_UNKNOWN_ID"
10291
10595
  ],
10292
10596
  "x-enum-descriptions": {
10293
10597
  "BAD_REQUEST": "Malformed request (bad JSON, missing project scope).",
@@ -10304,12 +10608,21 @@
10304
10608
  "REFERENCED_NOT_FOUND": "A referenced entity (universe, metric, gate, event) does not exist.",
10305
10609
  "GROUPS_WEIGHT_SUM": "Experiment group weights must sum to 10000 (basis points).",
10306
10610
  "EVENT_PENDING": "The referenced event is still pending review and cannot back a metric yet.",
10307
- "INTERNAL": "Unexpected server error."
10611
+ "INTERNAL": "Unexpected server error.",
10612
+ "PLAN_LIMIT": "The action would exceed a plan quota (e.g. the tier's maximum experiments, metrics, or configs). Upgrade the plan or archive an existing resource.",
10613
+ "EXPERIMENT_NO_GOAL_METRIC": "The experiment has no goal metric set, which the requested action requires.",
10614
+ "EXPERIMENT_ARCHIVED_RESTART": "An archived experiment cannot be restarted.",
10615
+ "EXPERIMENT_RESTORE_INVALID": "The experiment cannot be restored from its current state.",
10616
+ "EXPERIMENT_NOT_RUNNING": "The action requires the experiment to be running.",
10617
+ "EXPERIMENT_RUNNING_ARCHIVE": "A running experiment cannot be archived — stop it first.",
10618
+ "EXPERIMENT_IMMUTABLE_FIELD": "A field that is immutable while the experiment is running was modified.",
10619
+ "METRIC_NOT_FOUND": "The referenced metric does not exist or is not visible to the caller.",
10620
+ "METRIC_UNKNOWN_ID": "The supplied metric id is malformed or not recognised."
10308
10621
  }
10309
10622
  },
10310
10623
  "Error": {
10311
10624
  "type": "object",
10312
- "description": "Uniform error envelope returned by every admin endpoint on a non-2xx status. `error` is the human-readable message; `code` is the stable machine code (present whenever the failure maps to a catalogued `ErrorCode`); `detail` carries extra context (validation paths, conflicting field); `instructions` is optional actionable guidance for resolving the error.",
10625
+ "description": "Uniform error envelope returned by every admin endpoint on a non-2xx status. `error` is the human-readable message; `code` is the stable machine code (present whenever the failure maps to a catalogued `ErrorCode`); `detail` carries extra context (validation paths, conflicting field); `fields` maps each failing input field to its own message; `instructions` is optional actionable guidance for resolving the error; `schema` echoes back the expected JSON Schema when a value failed schema validation.",
10313
10626
  "required": [
10314
10627
  "error"
10315
10628
  ],
@@ -10326,9 +10639,21 @@
10326
10639
  "type": "string",
10327
10640
  "description": "Extra context (validation details, conflicting field, referenced row)."
10328
10641
  },
10642
+ "fields": {
10643
+ "type": "object",
10644
+ "additionalProperties": {
10645
+ "type": "string"
10646
+ },
10647
+ "description": "Per-field validation messages, keyed by the submitted field's path (dot-notation for nested fields, e.g. `groups.0.weight`). Present when structural validation failed on one or more specific input fields, so a client form can attach each message to its own input instead of collapsing everything into one banner."
10648
+ },
10329
10649
  "instructions": {
10330
10650
  "type": "string",
10331
10651
  "description": "Actionable guidance for resolving this error — what to check or change next."
10652
+ },
10653
+ "schema": {
10654
+ "type": "object",
10655
+ "additionalProperties": true,
10656
+ "description": "The expected JSON Schema (draft 2020-12) the submitted value must satisfy. Returned only when a value failed schema validation (e.g. a config `value` that does not match its config's saved schema), so the caller can correct the value and retry without re-fetching."
10332
10657
  }
10333
10658
  }
10334
10659
  },
@@ -10354,7 +10679,16 @@
10354
10679
  "pattern": "^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?)?$",
10355
10680
  "description": "Stable gate key used by SDKs (`Shipeasy.checkGate(user, '<name>')`). Single segment or `folder.name`. Lowercase letters, digits, `_` or `-`; max 128 chars. Immutable after create — rename = delete + recreate."
10356
10681
  },
10357
- "enabled": {
10682
+ "type": {
10683
+ "default": "targeting",
10684
+ "type": "string",
10685
+ "enum": [
10686
+ "targeting",
10687
+ "holdout"
10688
+ ],
10689
+ "description": "Gate kind. `targeting` (default) is a normal flag with the full builder. `holdout` is a **restricted** flag — only a public rollout % and a whitelist are allowed; attribute rules and a gatekeeper stack are rejected. Used as an experiment's `holdout_gate`."
10690
+ },
10691
+ "enabled": {
10358
10692
  "default": true,
10359
10693
  "description": "Master switch. Defaults to `true`. Set `false` to create the gate disabled (evaluates to `false` regardless of rules/rollout); flip on via `POST /{id}/enable` or PATCH.",
10360
10694
  "type": "boolean"
@@ -10751,6 +11085,14 @@
10751
11085
  "UpdateGateRequest": {
10752
11086
  "type": "object",
10753
11087
  "properties": {
11088
+ "type": {
11089
+ "type": "string",
11090
+ "enum": [
11091
+ "targeting",
11092
+ "holdout"
11093
+ ],
11094
+ "description": "Gate kind. Switching to `holdout` requires the gate carry only a public rollout % + whitelist (attribute rules / stack are rejected)."
11095
+ },
10754
11096
  "rollout_pct": {
10755
11097
  "description": "New rollout in **basis points** (0–10000 = 0%–100%) — `100` here means **1%**. Use `rollout_percent` (0–100) below for percent. Omit both to leave unchanged.",
10756
11098
  "type": "integer",
@@ -11140,307 +11482,13 @@
11140
11482
  ],
11141
11483
  "additionalProperties": false
11142
11484
  },
11143
- "ListExperimentsResponse": {
11144
- "type": "object",
11145
- "properties": {
11146
- "data": {
11147
- "type": "array",
11148
- "items": {
11149
- "type": "object",
11150
- "properties": {
11151
- "id": {
11152
- "type": "string",
11153
- "description": "Stable opaque experiment id (`exp_…`)."
11154
- },
11155
- "name": {
11156
- "type": "string",
11157
- "maxLength": 128,
11158
- "pattern": "^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?)?$",
11159
- "description": "Stable experiment key. Single segment or `folder.name` (a-z, 0-9, `_`/`-`; max 128 chars). Used by SDKs as `Shipeasy.getExperiment(user, '<name>')`. Immutable after create."
11160
- },
11161
- "description": {
11162
- "anyOf": [
11163
- {
11164
- "type": "string"
11165
- },
11166
- {
11167
- "type": "null"
11168
- }
11169
- ]
11170
- },
11171
- "hypothesis": {
11172
- "anyOf": [
11173
- {
11174
- "type": "string"
11175
- },
11176
- {
11177
- "type": "null"
11178
- }
11179
- ]
11180
- },
11181
- "tag": {
11182
- "anyOf": [
11183
- {
11184
- "type": "string"
11185
- },
11186
- {
11187
- "type": "null"
11188
- }
11189
- ]
11190
- },
11191
- "ownerEmail": {
11192
- "anyOf": [
11193
- {
11194
- "type": "string"
11195
- },
11196
- {
11197
- "type": "null"
11198
- }
11199
- ]
11200
- },
11201
- "audience": {
11202
- "anyOf": [
11203
- {
11204
- "type": "string"
11205
- },
11206
- {
11207
- "type": "null"
11208
- }
11209
- ]
11210
- },
11211
- "bucketBy": {
11212
- "anyOf": [
11213
- {
11214
- "type": "string",
11215
- "minLength": 1,
11216
- "maxLength": 128,
11217
- "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
11218
- "description": "User-attribute name used as the bucketing key — e.g. `user_id` (default), `session_id`, `device_id`, or a custom attribute like `company_id` to keep a whole org on one variant. Defaults to `user_id` when omitted."
11219
- },
11220
- {
11221
- "type": "null"
11222
- }
11223
- ]
11224
- },
11225
- "folder": {
11226
- "anyOf": [
11227
- {
11228
- "type": "string"
11229
- },
11230
- {
11231
- "type": "null"
11232
- }
11233
- ]
11234
- },
11235
- "status": {
11236
- "type": "string",
11237
- "enum": [
11238
- "draft",
11239
- "running",
11240
- "stopped",
11241
- "archived"
11242
- ]
11243
- },
11244
- "universe": {
11245
- "type": "string",
11246
- "description": "Universe name this experiment draws from."
11247
- },
11248
- "targetingGate": {
11249
- "anyOf": [
11250
- {
11251
- "type": "string"
11252
- },
11253
- {
11254
- "type": "null"
11255
- }
11256
- ]
11257
- },
11258
- "allocationPct": {
11259
- "type": "integer",
11260
- "minimum": -9007199254740991,
11261
- "maximum": 9007199254740991,
11262
- "description": "Allocation in basis points (0–10000)."
11263
- },
11264
- "salt": {
11265
- "type": "string"
11266
- },
11267
- "params": {
11268
- "type": "object",
11269
- "additionalProperties": {
11270
- "type": "string",
11271
- "enum": [
11272
- "string",
11273
- "bool",
11274
- "number"
11275
- ]
11276
- }
11277
- },
11278
- "groups": {
11279
- "type": "array",
11280
- "items": {
11281
- "type": "object",
11282
- "properties": {
11283
- "name": {
11284
- "type": "string",
11285
- "minLength": 1,
11286
- "maxLength": 64,
11287
- "description": "Group label (e.g. `control`, `treatment_a`). Max 64 chars."
11288
- },
11289
- "weight": {
11290
- "type": "integer",
11291
- "minimum": 0,
11292
- "maximum": 10000,
11293
- "description": "Allocation weight in basis points (0–10000). The sum across all groups must equal exactly **10000** (100%)."
11294
- },
11295
- "params": {
11296
- "default": {},
11297
- "description": "Per-group parameter values delivered to the SDK when a caller is hashed into this group. Keys must match the experiment's `params` schema.",
11298
- "type": "object",
11299
- "additionalProperties": {}
11300
- }
11301
- },
11302
- "required": [
11303
- "name",
11304
- "weight"
11305
- ],
11306
- "additionalProperties": false,
11307
- "description": "One variant in the experiment. Weights across all groups must sum to 10000."
11308
- }
11309
- },
11310
- "significanceThreshold": {
11311
- "type": "number"
11312
- },
11313
- "minRuntimeDays": {
11314
- "type": "integer",
11315
- "minimum": -9007199254740991,
11316
- "maximum": 9007199254740991
11317
- },
11318
- "minSampleSize": {
11319
- "type": "integer",
11320
- "minimum": -9007199254740991,
11321
- "maximum": 9007199254740991
11322
- },
11323
- "sequentialTesting": {
11324
- "type": "boolean"
11325
- },
11326
- "startedAt": {
11327
- "anyOf": [
11328
- {
11329
- "type": "string"
11330
- },
11331
- {
11332
- "type": "null"
11333
- }
11334
- ],
11335
- "description": "ISO-8601 timestamp the experiment last transitioned to `running`, or `null`."
11336
- },
11337
- "stoppedAt": {
11338
- "anyOf": [
11339
- {
11340
- "type": "string"
11341
- },
11342
- {
11343
- "type": "null"
11344
- }
11345
- ]
11346
- },
11347
- "updatedAt": {
11348
- "type": "string"
11349
- }
11350
- },
11351
- "required": [
11352
- "id",
11353
- "name",
11354
- "description",
11355
- "hypothesis",
11356
- "tag",
11357
- "ownerEmail",
11358
- "audience",
11359
- "bucketBy",
11360
- "folder",
11361
- "status",
11362
- "universe",
11363
- "targetingGate",
11364
- "allocationPct",
11365
- "salt",
11366
- "params",
11367
- "groups",
11368
- "significanceThreshold",
11369
- "minRuntimeDays",
11370
- "minSampleSize",
11371
- "sequentialTesting",
11372
- "startedAt",
11373
- "updatedAt"
11374
- ],
11375
- "additionalProperties": false
11376
- }
11377
- },
11378
- "next_cursor": {
11379
- "anyOf": [
11380
- {
11381
- "type": "string"
11382
- },
11383
- {
11384
- "type": "null"
11385
- }
11386
- ]
11387
- }
11388
- },
11389
- "required": [
11390
- "data",
11391
- "next_cursor"
11392
- ],
11393
- "additionalProperties": false
11394
- },
11395
- "MetricName": {
11396
- "type": "string",
11397
- "maxLength": 128,
11398
- "pattern": "^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?)?$",
11399
- "description": "Stable metric key. Single segment or `folder.name`; lowercase letters, digits, `_`/`-`; max 128 chars."
11400
- },
11401
- "ExperimentInlineMetric": {
11485
+ "ExperimentApiRow": {
11402
11486
  "type": "object",
11403
11487
  "properties": {
11404
- "name": {
11405
- "$ref": "#/components/schemas/MetricName"
11406
- },
11407
- "query": {
11408
- "type": "string",
11409
- "minLength": 1,
11410
- "maxLength": 4096,
11411
- "description": "Metric DSL string. Provide this OR `event`."
11412
- },
11413
- "event": {
11414
- "type": "string",
11415
- "minLength": 1,
11416
- "maxLength": 256,
11417
- "description": "Event name to build the metric from server-side. Auto-created if missing."
11418
- },
11419
- "aggregation": {
11488
+ "id": {
11420
11489
  "type": "string",
11421
- "enum": [
11422
- "count_users",
11423
- "count_events",
11424
- "retention_7d",
11425
- "retention_30d",
11426
- "sum",
11427
- "avg"
11428
- ],
11429
- "description": "Reducer for the `event` form. Defaults to `count_users`."
11490
+ "description": "Stable opaque experiment id (`exp_…`)."
11430
11491
  },
11431
- "value": {
11432
- "type": "string",
11433
- "minLength": 1,
11434
- "maxLength": 256,
11435
- "description": "Numeric event property for `sum`/`avg` (with `event`)."
11436
- }
11437
- },
11438
- "additionalProperties": false,
11439
- "description": "Inline metric — a DSL `query`, or an `event` (+ `aggregation`/`value`) the server compiles into one."
11440
- },
11441
- "CreateExperimentRequest": {
11442
- "type": "object",
11443
- "properties": {
11444
11492
  "name": {
11445
11493
  "type": "string",
11446
11494
  "maxLength": 128,
@@ -11448,12 +11496,9 @@
11448
11496
  "description": "Stable experiment key. Single segment or `folder.name` (a-z, 0-9, `_`/`-`; max 128 chars). Used by SDKs as `Shipeasy.getExperiment(user, '<name>')`. Immutable after create."
11449
11497
  },
11450
11498
  "description": {
11451
- "default": null,
11452
- "description": "Free-form description. Max 2000 chars, markdown rendered in the dashboard.",
11453
11499
  "anyOf": [
11454
11500
  {
11455
- "type": "string",
11456
- "maxLength": 2000
11501
+ "type": "string"
11457
11502
  },
11458
11503
  {
11459
11504
  "type": "null"
@@ -11461,12 +11506,9 @@
11461
11506
  ]
11462
11507
  },
11463
11508
  "hypothesis": {
11464
- "default": null,
11465
- "description": "Hypothesis statement shown in the editor. Display-only.",
11466
11509
  "anyOf": [
11467
11510
  {
11468
- "type": "string",
11469
- "maxLength": 4000
11511
+ "type": "string"
11470
11512
  },
11471
11513
  {
11472
11514
  "type": "null"
@@ -11474,25 +11516,19 @@
11474
11516
  ]
11475
11517
  },
11476
11518
  "tag": {
11477
- "default": null,
11478
- "description": "Short tag chip rendered next to the name. Display-only.",
11479
11519
  "anyOf": [
11480
11520
  {
11481
- "type": "string",
11482
- "maxLength": 64
11521
+ "type": "string"
11483
11522
  },
11484
11523
  {
11485
11524
  "type": "null"
11486
11525
  }
11487
11526
  ]
11488
11527
  },
11489
- "owner_email": {
11490
- "default": null,
11491
- "description": "Owner email. Display-only.",
11528
+ "ownerEmail": {
11492
11529
  "anyOf": [
11493
11530
  {
11494
- "type": "string",
11495
- "maxLength": 254
11531
+ "type": "string"
11496
11532
  },
11497
11533
  {
11498
11534
  "type": "null"
@@ -11500,20 +11536,16 @@
11500
11536
  ]
11501
11537
  },
11502
11538
  "audience": {
11503
- "default": null,
11504
- "description": "Audience label shown in the editor. Display-only.",
11505
11539
  "anyOf": [
11506
11540
  {
11507
- "type": "string",
11508
- "maxLength": 256
11541
+ "type": "string"
11509
11542
  },
11510
11543
  {
11511
11544
  "type": "null"
11512
11545
  }
11513
11546
  ]
11514
11547
  },
11515
- "bucket_by": {
11516
- "default": null,
11548
+ "bucketBy": {
11517
11549
  "anyOf": [
11518
11550
  {
11519
11551
  "type": "string",
@@ -11528,16 +11560,29 @@
11528
11560
  ]
11529
11561
  },
11530
11562
  "folder": {
11531
- "$ref": "#/components/schemas/Folder"
11563
+ "anyOf": [
11564
+ {
11565
+ "type": "string"
11566
+ },
11567
+ {
11568
+ "type": "null"
11569
+ }
11570
+ ]
11571
+ },
11572
+ "status": {
11573
+ "type": "string",
11574
+ "enum": [
11575
+ "draft",
11576
+ "running",
11577
+ "stopped",
11578
+ "archived"
11579
+ ]
11532
11580
  },
11533
11581
  "universe": {
11534
11582
  "type": "string",
11535
- "minLength": 1,
11536
- "description": "Name of an existing universe in the project. Returns `422` if the universe doesn't exist."
11583
+ "description": "Universe name this experiment draws from."
11537
11584
  },
11538
- "targeting_gate": {
11539
- "default": null,
11540
- "description": "Optional gate name. Only callers that pass the gate are enrolled in the experiment.",
11585
+ "targetingGate": {
11541
11586
  "anyOf": [
11542
11587
  {
11543
11588
  "type": "string"
@@ -11547,27 +11592,64 @@
11547
11592
  }
11548
11593
  ]
11549
11594
  },
11550
- "allocation_pct": {
11551
- "default": 0,
11552
- "description": "Share of the (gated) audience allocated to the experiment, in basis points (0–10000 = 0%–100%). `0` = unallocated. Use `allocation_percent` (0–100) below to think in percent. Immutable while the experiment is running.",
11595
+ "holdoutGate": {
11596
+ "anyOf": [
11597
+ {
11598
+ "type": "string"
11599
+ },
11600
+ {
11601
+ "type": "null"
11602
+ }
11603
+ ],
11604
+ "description": "Per-experiment holdout gate name (a `holdout`-type flag), or `null`."
11605
+ },
11606
+ "allocationPct": {
11553
11607
  "type": "integer",
11554
- "minimum": 0,
11555
- "maximum": 10000
11608
+ "minimum": -9007199254740991,
11609
+ "maximum": 9007199254740991,
11610
+ "description": "Allocation in basis points (0–10000)."
11556
11611
  },
11557
- "allocation_percent": {
11558
- "description": "Allocation as a **percentage** (0–100, fractional ok). Friendlier alias for `allocation_pct`; converted to basis points server-side (e.g. `50` = 5000 bp). If both are set, `allocation_percent` wins.",
11559
- "type": "number",
11612
+ "reservedHeadroom": {
11613
+ "type": "integer",
11560
11614
  "minimum": 0,
11561
- "maximum": 100
11615
+ "maximum": 10000,
11616
+ "description": "Basis points of the split reserved for appended variants (group weights sum to 10000 − this)."
11617
+ },
11618
+ "hashVersion": {
11619
+ "type": "integer",
11620
+ "minimum": 1,
11621
+ "description": "Bucketing hash algorithm version for the experiment's pool slice (§B4). Defaults to 1."
11622
+ },
11623
+ "poolOffsetBp": {
11624
+ "anyOf": [
11625
+ {
11626
+ "type": "integer",
11627
+ "minimum": 0,
11628
+ "maximum": 10000
11629
+ },
11630
+ {
11631
+ "type": "null"
11632
+ }
11633
+ ],
11634
+ "description": "Basis-point offset of the experiment's contiguous slice in the universe pool, or `null` before a slice is allocated."
11635
+ },
11636
+ "poolSizeBp": {
11637
+ "anyOf": [
11638
+ {
11639
+ "type": "integer",
11640
+ "minimum": 0,
11641
+ "maximum": 10000
11642
+ },
11643
+ {
11644
+ "type": "null"
11645
+ }
11646
+ ],
11647
+ "description": "Basis-point width of the experiment's pool slice (equal to its allocation), or `null` before a slice is allocated."
11562
11648
  },
11563
11649
  "salt": {
11564
- "description": "Hash salt for bucketing. Auto-generated if omitted. Immutable while running.",
11565
- "type": "string",
11566
- "minLength": 1,
11567
- "maxLength": 64
11650
+ "type": "string"
11568
11651
  },
11569
11652
  "params": {
11570
- "default": {},
11571
11653
  "type": "object",
11572
11654
  "additionalProperties": {
11573
11655
  "type": "string",
@@ -11576,11 +11658,9 @@
11576
11658
  "bool",
11577
11659
  "number"
11578
11660
  ]
11579
- },
11580
- "description": "Map of param-name → scalar type. Defines the shape of `groups[].params`. Example: `{ headline: 'string', show_cta: 'bool' }`."
11661
+ }
11581
11662
  },
11582
11663
  "groups": {
11583
- "minItems": 2,
11584
11664
  "type": "array",
11585
11665
  "items": {
11586
11666
  "type": "object",
@@ -11610,84 +11690,344 @@
11610
11690
  ],
11611
11691
  "additionalProperties": false,
11612
11692
  "description": "One variant in the experiment. Weights across all groups must sum to 10000."
11613
- },
11614
- "description": "Two or more variants. Weights must sum to exactly 10000 (100%). Immutable while running."
11693
+ }
11615
11694
  },
11616
- "significance_threshold": {
11617
- "default": 0.05,
11618
- "description": "p-value cutoff used by the analysis pass. Defaults to `0.05`. Values other than 0.05 require Pro plan or higher.",
11619
- "type": "number",
11620
- "minimum": 0.0001,
11621
- "maximum": 0.5
11695
+ "significanceThreshold": {
11696
+ "type": "number"
11622
11697
  },
11623
- "min_runtime_days": {
11624
- "default": 0,
11625
- "description": "Minimum days the experiment must run before results are considered conclusive.",
11698
+ "minRuntimeDays": {
11626
11699
  "type": "integer",
11627
- "minimum": 0,
11628
- "maximum": 365
11700
+ "minimum": -9007199254740991,
11701
+ "maximum": 9007199254740991
11629
11702
  },
11630
- "min_sample_size": {
11631
- "default": 100,
11632
- "description": "Minimum exposures per group before results are considered conclusive.",
11703
+ "minSampleSize": {
11633
11704
  "type": "integer",
11634
- "minimum": 1,
11705
+ "minimum": -9007199254740991,
11635
11706
  "maximum": 9007199254740991
11636
11707
  },
11637
- "sequential_testing": {
11638
- "default": false,
11639
- "description": "Enable sequential testing (always-valid p-values). Requires Premium plan or higher.",
11708
+ "sequentialTesting": {
11640
11709
  "type": "boolean"
11641
11710
  },
11642
- "goal_metric": {
11643
- "description": "Single goal metric defined inline — either a DSL `query` or an `event` (+`aggregation`/`value`) the server compiles. Attaching one is required before the experiment can be started. The underlying event is auto-created if missing.",
11644
- "$ref": "#/components/schemas/ExperimentInlineMetric"
11711
+ "startedAt": {
11712
+ "anyOf": [
11713
+ {
11714
+ "type": "string"
11715
+ },
11716
+ {
11717
+ "type": "null"
11718
+ }
11719
+ ],
11720
+ "description": "ISO-8601 timestamp the experiment last transitioned to `running`, or `null`."
11645
11721
  },
11646
- "guardrail_metrics": {
11647
- "default": [],
11648
- "description": "Up to 10 guardrail metrics defined inline. Each is upserted (event + metric) and attached with role=guardrail.",
11649
- "maxItems": 10,
11722
+ "stoppedAt": {
11723
+ "anyOf": [
11724
+ {
11725
+ "type": "string"
11726
+ },
11727
+ {
11728
+ "type": "null"
11729
+ }
11730
+ ]
11731
+ },
11732
+ "updatedAt": {
11733
+ "type": "string"
11734
+ },
11735
+ "version": {
11736
+ "anyOf": [
11737
+ {
11738
+ "type": "integer"
11739
+ },
11740
+ {
11741
+ "type": "null"
11742
+ }
11743
+ ],
11744
+ "description": "Save counter (number of published edits), or `null` on pre-versioning rows."
11745
+ },
11746
+ "creatorEmail": {
11747
+ "anyOf": [
11748
+ {
11749
+ "type": "string"
11750
+ },
11751
+ {
11752
+ "type": "null"
11753
+ }
11754
+ ],
11755
+ "description": "Resolved creator email (`created_by` → users). Enriched field: present on list rows, omitted from the by-id detail."
11756
+ },
11757
+ "updaterEmail": {
11758
+ "anyOf": [
11759
+ {
11760
+ "type": "string"
11761
+ },
11762
+ {
11763
+ "type": "null"
11764
+ }
11765
+ ],
11766
+ "description": "Resolved last-editor email (`updated_by` → users). Enriched field: present on list rows, omitted from the by-id detail."
11767
+ },
11768
+ "verdict": {
11769
+ "type": "string",
11770
+ "enum": [
11771
+ "ship",
11772
+ "hold",
11773
+ "wait",
11774
+ "invalid",
11775
+ "draft"
11776
+ ],
11777
+ "description": "Answer-first decision from the goal metric + guardrails + SRM vs. the significance threshold and min runtime: `ship`, `hold`, `wait`, `invalid`, `draft`. Enriched field: list rows only."
11778
+ },
11779
+ "verdictTitle": {
11780
+ "type": "string",
11781
+ "description": "Expanded verdict headline the results hero renders. Enriched field: list rows only."
11782
+ },
11783
+ "verdictWhy": {
11784
+ "type": "string",
11785
+ "description": "Verdict rationale the results hero renders. Enriched field: list rows only."
11786
+ },
11787
+ "goalMetric": {
11788
+ "anyOf": [
11789
+ {
11790
+ "type": "object",
11791
+ "properties": {
11792
+ "id": {
11793
+ "type": "string"
11794
+ },
11795
+ "name": {
11796
+ "type": "string"
11797
+ }
11798
+ },
11799
+ "required": [
11800
+ "id",
11801
+ "name"
11802
+ ],
11803
+ "additionalProperties": false
11804
+ },
11805
+ {
11806
+ "type": "null"
11807
+ }
11808
+ ],
11809
+ "description": "The experiment's goal metric `{ id, name }`, or `null` when none is set. Enriched field: list rows only."
11810
+ },
11811
+ "guardrails": {
11650
11812
  "type": "array",
11651
11813
  "items": {
11652
- "$ref": "#/components/schemas/ExperimentInlineMetric"
11653
- }
11814
+ "type": "object",
11815
+ "properties": {
11816
+ "id": {
11817
+ "type": "string"
11818
+ },
11819
+ "name": {
11820
+ "type": "string"
11821
+ },
11822
+ "eventName": {
11823
+ "anyOf": [
11824
+ {
11825
+ "type": "string"
11826
+ },
11827
+ {
11828
+ "type": "null"
11829
+ }
11830
+ ]
11831
+ }
11832
+ },
11833
+ "required": [
11834
+ "id",
11835
+ "name"
11836
+ ],
11837
+ "additionalProperties": false
11838
+ },
11839
+ "description": "Guardrail metrics `{ id, name, eventName? }`. Enriched field: list rows only."
11840
+ },
11841
+ "guardrailCount": {
11842
+ "type": "integer",
11843
+ "minimum": 0,
11844
+ "description": "Number of guardrail metrics. Enriched field: list rows only."
11845
+ },
11846
+ "primaryLiftPct": {
11847
+ "anyOf": [
11848
+ {
11849
+ "type": "number"
11850
+ },
11851
+ {
11852
+ "type": "null"
11853
+ }
11854
+ ],
11855
+ "description": "Goal-metric lift of the leading variant vs. control, in percent, or `null` when not yet computed. Enriched field: list rows only."
11856
+ },
11857
+ "significancePct": {
11858
+ "anyOf": [
11859
+ {
11860
+ "type": "number"
11861
+ },
11862
+ {
11863
+ "type": "null"
11864
+ }
11865
+ ],
11866
+ "description": "Statistical significance (1 − p) as a percent, or `null` when not yet computed. Enriched field: list rows only."
11867
+ },
11868
+ "sampleSize": {
11869
+ "anyOf": [
11870
+ {
11871
+ "type": "integer"
11872
+ },
11873
+ {
11874
+ "type": "null"
11875
+ }
11876
+ ],
11877
+ "description": "Total distinct units analysed, or `null` when not yet computed. Enriched field: list rows only."
11878
+ },
11879
+ "exposure": {
11880
+ "type": "array",
11881
+ "items": {
11882
+ "type": "object",
11883
+ "properties": {
11884
+ "ds": {
11885
+ "type": "string"
11886
+ },
11887
+ "value": {
11888
+ "type": "number"
11889
+ }
11890
+ },
11891
+ "required": [
11892
+ "ds",
11893
+ "value"
11894
+ ],
11895
+ "additionalProperties": false
11896
+ },
11897
+ "description": "Cumulative unique-users-enrolled series for the exposure sparkline. Enriched field: list rows only."
11898
+ },
11899
+ "exposureTotal": {
11900
+ "anyOf": [
11901
+ {
11902
+ "type": "integer"
11903
+ },
11904
+ {
11905
+ "type": "null"
11906
+ }
11907
+ ],
11908
+ "description": "Total distinct units ever exposed (sparkline headline), or `null`. Enriched field: list rows only."
11654
11909
  }
11655
11910
  },
11656
11911
  "required": [
11912
+ "id",
11657
11913
  "name",
11914
+ "description",
11915
+ "hypothesis",
11916
+ "tag",
11917
+ "ownerEmail",
11918
+ "audience",
11919
+ "bucketBy",
11920
+ "folder",
11921
+ "status",
11658
11922
  "universe",
11659
- "groups"
11923
+ "targetingGate",
11924
+ "holdoutGate",
11925
+ "allocationPct",
11926
+ "reservedHeadroom",
11927
+ "hashVersion",
11928
+ "poolOffsetBp",
11929
+ "poolSizeBp",
11930
+ "salt",
11931
+ "params",
11932
+ "groups",
11933
+ "significanceThreshold",
11934
+ "minRuntimeDays",
11935
+ "minSampleSize",
11936
+ "sequentialTesting",
11937
+ "startedAt",
11938
+ "stoppedAt",
11939
+ "updatedAt",
11940
+ "version"
11660
11941
  ],
11661
- "additionalProperties": false,
11662
- "description": "Body for `POST /api/admin/experiments`. `name`, `universe`, and `groups` (≥2, weights sum to 10000) required."
11942
+ "additionalProperties": false
11663
11943
  },
11664
- "CreateExperimentResponse": {
11944
+ "ListExperimentsResponse": {
11665
11945
  "type": "object",
11666
11946
  "properties": {
11667
- "id": {
11668
- "type": "string",
11669
- "description": "Newly assigned experiment id."
11947
+ "data": {
11948
+ "type": "array",
11949
+ "items": {
11950
+ "$ref": "#/components/schemas/ExperimentApiRow"
11951
+ }
11670
11952
  },
11671
- "name": {
11672
- "type": "string",
11673
- "maxLength": 128,
11674
- "pattern": "^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?)?$",
11675
- "description": "Stable experiment key. Single segment or `folder.name` (a-z, 0-9, `_`/`-`; max 128 chars). Used by SDKs as `Shipeasy.getExperiment(user, '<name>')`. Immutable after create."
11953
+ "next_cursor": {
11954
+ "anyOf": [
11955
+ {
11956
+ "type": "string"
11957
+ },
11958
+ {
11959
+ "type": "null"
11960
+ }
11961
+ ]
11676
11962
  }
11677
11963
  },
11678
11964
  "required": [
11679
- "id",
11680
- "name"
11965
+ "data",
11966
+ "next_cursor"
11681
11967
  ],
11682
11968
  "additionalProperties": false
11683
11969
  },
11684
- "GetExperimentResponse": {
11970
+ "MetricName": {
11971
+ "type": "string",
11972
+ "maxLength": 128,
11973
+ "pattern": "^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?)?$",
11974
+ "description": "Stable metric key. Single segment or `folder.name`; lowercase letters, digits, `_`/`-`; max 128 chars."
11975
+ },
11976
+ "ExperimentInlineMetric": {
11685
11977
  "type": "object",
11686
11978
  "properties": {
11687
- "id": {
11979
+ "name": {
11980
+ "$ref": "#/components/schemas/MetricName"
11981
+ },
11982
+ "query": {
11688
11983
  "type": "string",
11689
- "description": "Stable opaque experiment id (`exp_…`)."
11984
+ "minLength": 1,
11985
+ "maxLength": 4096,
11986
+ "description": "Metric DSL string. Provide this OR `event`."
11690
11987
  },
11988
+ "event": {
11989
+ "type": "string",
11990
+ "minLength": 1,
11991
+ "maxLength": 256,
11992
+ "description": "Event name to build the metric from server-side. Auto-created if missing."
11993
+ },
11994
+ "aggregation": {
11995
+ "type": "string",
11996
+ "enum": [
11997
+ "count_users",
11998
+ "count_events",
11999
+ "retention_7d",
12000
+ "retention_30d",
12001
+ "sum",
12002
+ "avg"
12003
+ ],
12004
+ "description": "Reducer for the `event` form. Defaults to `count_users`."
12005
+ },
12006
+ "value": {
12007
+ "type": "string",
12008
+ "minLength": 1,
12009
+ "maxLength": 256,
12010
+ "description": "Numeric event property for `sum`/`avg` (with `event`)."
12011
+ },
12012
+ "min_effect_of_interest": {
12013
+ "description": "Per-experiment override of the metric's default minimum effect of interest (relative, 0–1) — the smallest change worth acting on for this experiment's decision. `null`/omitted inherits the metric default. For a guardrail, the non-inferiority margin.",
12014
+ "default": null,
12015
+ "anyOf": [
12016
+ {
12017
+ "type": "number"
12018
+ },
12019
+ {
12020
+ "type": "null"
12021
+ }
12022
+ ]
12023
+ }
12024
+ },
12025
+ "additionalProperties": false,
12026
+ "description": "Inline metric — a DSL `query`, or an `event` (+ `aggregation`/`value`) the server compiles into one."
12027
+ },
12028
+ "CreateExperimentRequest": {
12029
+ "type": "object",
12030
+ "properties": {
11691
12031
  "name": {
11692
12032
  "type": "string",
11693
12033
  "maxLength": 128,
@@ -11695,9 +12035,12 @@
11695
12035
  "description": "Stable experiment key. Single segment or `folder.name` (a-z, 0-9, `_`/`-`; max 128 chars). Used by SDKs as `Shipeasy.getExperiment(user, '<name>')`. Immutable after create."
11696
12036
  },
11697
12037
  "description": {
12038
+ "default": null,
12039
+ "description": "Free-form description. Max 2000 chars, markdown rendered in the dashboard.",
11698
12040
  "anyOf": [
11699
12041
  {
11700
- "type": "string"
12042
+ "type": "string",
12043
+ "maxLength": 2000
11701
12044
  },
11702
12045
  {
11703
12046
  "type": "null"
@@ -11705,9 +12048,12 @@
11705
12048
  ]
11706
12049
  },
11707
12050
  "hypothesis": {
12051
+ "default": null,
12052
+ "description": "Hypothesis statement shown in the editor. Display-only.",
11708
12053
  "anyOf": [
11709
12054
  {
11710
- "type": "string"
12055
+ "type": "string",
12056
+ "maxLength": 4000
11711
12057
  },
11712
12058
  {
11713
12059
  "type": "null"
@@ -11715,19 +12061,25 @@
11715
12061
  ]
11716
12062
  },
11717
12063
  "tag": {
12064
+ "default": null,
12065
+ "description": "Short tag chip rendered next to the name. Display-only.",
11718
12066
  "anyOf": [
11719
12067
  {
11720
- "type": "string"
12068
+ "type": "string",
12069
+ "maxLength": 64
11721
12070
  },
11722
12071
  {
11723
12072
  "type": "null"
11724
12073
  }
11725
12074
  ]
11726
12075
  },
11727
- "ownerEmail": {
12076
+ "owner_email": {
12077
+ "default": null,
12078
+ "description": "Owner email. Display-only.",
11728
12079
  "anyOf": [
11729
12080
  {
11730
- "type": "string"
12081
+ "type": "string",
12082
+ "maxLength": 254
11731
12083
  },
11732
12084
  {
11733
12085
  "type": "null"
@@ -11735,16 +12087,20 @@
11735
12087
  ]
11736
12088
  },
11737
12089
  "audience": {
12090
+ "default": null,
12091
+ "description": "Audience label shown in the editor. Display-only.",
11738
12092
  "anyOf": [
11739
12093
  {
11740
- "type": "string"
12094
+ "type": "string",
12095
+ "maxLength": 256
11741
12096
  },
11742
12097
  {
11743
12098
  "type": "null"
11744
12099
  }
11745
12100
  ]
11746
12101
  },
11747
- "bucketBy": {
12102
+ "bucket_by": {
12103
+ "default": null,
11748
12104
  "anyOf": [
11749
12105
  {
11750
12106
  "type": "string",
@@ -11759,6 +12115,16 @@
11759
12115
  ]
11760
12116
  },
11761
12117
  "folder": {
12118
+ "$ref": "#/components/schemas/Folder"
12119
+ },
12120
+ "universe": {
12121
+ "type": "string",
12122
+ "minLength": 1,
12123
+ "description": "Name of an existing universe in the project. Returns `422` if the universe doesn't exist."
12124
+ },
12125
+ "targeting_gate": {
12126
+ "default": null,
12127
+ "description": "Optional gate name (a `targeting`-type flag). Only callers that pass the gate are enrolled in the experiment.",
11762
12128
  "anyOf": [
11763
12129
  {
11764
12130
  "type": "string"
@@ -11768,20 +12134,9 @@
11768
12134
  }
11769
12135
  ]
11770
12136
  },
11771
- "status": {
11772
- "type": "string",
11773
- "enum": [
11774
- "draft",
11775
- "running",
11776
- "stopped",
11777
- "archived"
11778
- ]
11779
- },
11780
- "universe": {
11781
- "type": "string",
11782
- "description": "Universe name this experiment draws from."
11783
- },
11784
- "targetingGate": {
12137
+ "holdout_gate": {
12138
+ "default": null,
12139
+ "description": "Optional per-experiment holdout gate — the name of a `holdout`-type flag (public % + whitelist). A caller the flag passes is *held out* (never assigned, sees the universe defaults). Distinct from the universe-level holdout.",
11785
12140
  "anyOf": [
11786
12141
  {
11787
12142
  "type": "string"
@@ -11791,16 +12146,33 @@
11791
12146
  }
11792
12147
  ]
11793
12148
  },
11794
- "allocationPct": {
12149
+ "allocation_pct": {
12150
+ "default": 0,
12151
+ "description": "Share of the (gated) audience allocated to the experiment, in basis points (0–10000 = 0%–100%). `0` = unallocated. Under pooled assignment this is the size of the universe-pool slice claimed. Use `allocation_percent` (0–100) below to think in percent. Immutable while the experiment is running.",
11795
12152
  "type": "integer",
11796
- "minimum": -9007199254740991,
11797
- "maximum": 9007199254740991,
11798
- "description": "Allocation in basis points (0–10000)."
12153
+ "minimum": 0,
12154
+ "maximum": 10000
12155
+ },
12156
+ "allocation_percent": {
12157
+ "description": "Allocation as a **percentage** (0–100, fractional ok). Friendlier alias for `allocation_pct`; converted to basis points server-side (e.g. `50` = 5000 bp). If both are set, `allocation_percent` wins.",
12158
+ "type": "number",
12159
+ "minimum": 0,
12160
+ "maximum": 100
12161
+ },
12162
+ "reserved_headroom": {
12163
+ "description": "Basis points of this experiment's split kept empty (0–10000) so a new variant can be appended into it while running without reshuffling. Group weights must sum to `10000 − reserved_headroom`. Defaults to the universe's `recommended_headroom` when omitted.",
12164
+ "type": "integer",
12165
+ "minimum": 0,
12166
+ "maximum": 10000
11799
12167
  },
11800
12168
  "salt": {
11801
- "type": "string"
12169
+ "description": "Hash salt for bucketing. Auto-generated if omitted. Immutable while running.",
12170
+ "type": "string",
12171
+ "minLength": 1,
12172
+ "maxLength": 64
11802
12173
  },
11803
12174
  "params": {
12175
+ "default": {},
11804
12176
  "type": "object",
11805
12177
  "additionalProperties": {
11806
12178
  "type": "string",
@@ -11809,9 +12181,11 @@
11809
12181
  "bool",
11810
12182
  "number"
11811
12183
  ]
11812
- }
12184
+ },
12185
+ "description": "**Deprecated** — the universe now owns the config schema (`param_schema`). Retained for back-compat; new experiments should leave this empty and declare params on the universe. Map of param-name → scalar type."
11813
12186
  },
11814
12187
  "groups": {
12188
+ "minItems": 2,
11815
12189
  "type": "array",
11816
12190
  "items": {
11817
12191
  "type": "object",
@@ -11826,11 +12200,11 @@
11826
12200
  "type": "integer",
11827
12201
  "minimum": 0,
11828
12202
  "maximum": 10000,
11829
- "description": "Allocation weight in basis points (0–10000). The sum across all groups must equal exactly **10000** (100%)."
12203
+ "description": "Allocation weight in basis points (0–10000). Weights across all groups must sum to `10000 − reserved_headroom` (100% minus any reserved tail)."
11830
12204
  },
11831
12205
  "params": {
11832
12206
  "default": {},
11833
- "description": "Per-group parameter values delivered to the SDK when a caller is hashed into this group. Keys must match the experiment's `params` schema.",
12207
+ "description": "Per-variant **override map** — a subset of the universe's `param_schema` keys. A key the variant doesn't set inherits the universe default. Unknown keys (not in the universe schema) are rejected.",
11834
12208
  "type": "object",
11835
12209
  "additionalProperties": {}
11836
12210
  }
@@ -11840,73 +12214,75 @@
11840
12214
  "weight"
11841
12215
  ],
11842
12216
  "additionalProperties": false,
11843
- "description": "One variant in the experiment. Weights across all groups must sum to 10000."
11844
- }
12217
+ "description": "One variant. Its `params` may only override keys declared on the universe schema."
12218
+ },
12219
+ "description": "Two or more variants. Weights must sum to `10000 − reserved_headroom`. Existing weights are immutable while running, but a new variant may be appended into the reserved tail."
11845
12220
  },
11846
- "significanceThreshold": {
11847
- "type": "number"
12221
+ "significance_threshold": {
12222
+ "default": 0.05,
12223
+ "description": "p-value cutoff used by the analysis pass. Defaults to `0.05`. Values other than 0.05 require Pro plan or higher.",
12224
+ "type": "number",
12225
+ "minimum": 0.0001,
12226
+ "maximum": 0.5
11848
12227
  },
11849
- "minRuntimeDays": {
12228
+ "min_runtime_days": {
12229
+ "default": 0,
12230
+ "description": "Minimum days the experiment must run before results are considered conclusive.",
11850
12231
  "type": "integer",
11851
- "minimum": -9007199254740991,
11852
- "maximum": 9007199254740991
12232
+ "minimum": 0,
12233
+ "maximum": 365
11853
12234
  },
11854
- "minSampleSize": {
12235
+ "min_sample_size": {
12236
+ "default": 100,
12237
+ "description": "Minimum exposures per group before results are considered conclusive.",
11855
12238
  "type": "integer",
11856
- "minimum": -9007199254740991,
12239
+ "minimum": 1,
11857
12240
  "maximum": 9007199254740991
11858
12241
  },
11859
- "sequentialTesting": {
12242
+ "sequential_testing": {
12243
+ "default": false,
12244
+ "description": "Enable sequential testing (always-valid p-values). Requires Premium plan or higher.",
11860
12245
  "type": "boolean"
11861
12246
  },
11862
- "startedAt": {
11863
- "anyOf": [
11864
- {
11865
- "type": "string"
11866
- },
11867
- {
11868
- "type": "null"
11869
- }
11870
- ],
11871
- "description": "ISO-8601 timestamp the experiment last transitioned to `running`, or `null`."
11872
- },
11873
- "stoppedAt": {
11874
- "anyOf": [
11875
- {
11876
- "type": "string"
11877
- },
11878
- {
11879
- "type": "null"
11880
- }
11881
- ]
12247
+ "goal_metric": {
12248
+ "description": "Single goal metric defined inline — either a DSL `query` or an `event` (+`aggregation`/`value`) the server compiles. Attaching one is required before the experiment can be started. The underlying event is auto-created if missing.",
12249
+ "$ref": "#/components/schemas/ExperimentInlineMetric"
11882
12250
  },
11883
- "updatedAt": {
11884
- "type": "string"
12251
+ "guardrail_metrics": {
12252
+ "default": [],
12253
+ "description": "Up to 10 guardrail metrics defined inline. Each is upserted (event + metric) and attached with role=guardrail.",
12254
+ "maxItems": 10,
12255
+ "type": "array",
12256
+ "items": {
12257
+ "$ref": "#/components/schemas/ExperimentInlineMetric"
12258
+ }
11885
12259
  }
11886
12260
  },
11887
12261
  "required": [
11888
- "id",
11889
12262
  "name",
11890
- "description",
11891
- "hypothesis",
11892
- "tag",
11893
- "ownerEmail",
11894
- "audience",
11895
- "bucketBy",
11896
- "folder",
11897
- "status",
11898
12263
  "universe",
11899
- "targetingGate",
11900
- "allocationPct",
11901
- "salt",
11902
- "params",
11903
- "groups",
11904
- "significanceThreshold",
11905
- "minRuntimeDays",
11906
- "minSampleSize",
11907
- "sequentialTesting",
11908
- "startedAt",
11909
- "updatedAt"
12264
+ "groups"
12265
+ ],
12266
+ "additionalProperties": false,
12267
+ "description": "Body for `POST /api/admin/experiments`. `name`, `universe`, and `groups` (≥2, weights sum to 10000) required."
12268
+ },
12269
+ "CreateExperimentResponse": {
12270
+ "type": "object",
12271
+ "properties": {
12272
+ "id": {
12273
+ "type": "string",
12274
+ "description": "Newly assigned experiment id."
12275
+ },
12276
+ "name": {
12277
+ "type": "string",
12278
+ "maxLength": 128,
12279
+ "pattern": "^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?)?$",
12280
+ "description": "Stable experiment key. Single segment or `folder.name` (a-z, 0-9, `_`/`-`; max 128 chars). Used by SDKs as `Shipeasy.getExperiment(user, '<name>')`. Immutable after create."
12281
+ }
12282
+ },
12283
+ "required": [
12284
+ "id",
12285
+ "name"
11910
12286
  ],
11911
12287
  "additionalProperties": false
11912
12288
  },
@@ -12014,12 +12390,29 @@
12014
12390
  }
12015
12391
  ]
12016
12392
  },
12393
+ "holdout_gate": {
12394
+ "description": "Per-experiment holdout gate — the name of a `holdout`-type flag, or `null` to clear. A caller the flag passes is held out.",
12395
+ "anyOf": [
12396
+ {
12397
+ "type": "string"
12398
+ },
12399
+ {
12400
+ "type": "null"
12401
+ }
12402
+ ]
12403
+ },
12017
12404
  "allocation_pct": {
12018
12405
  "description": "Basis-points allocation (0–10000). Use `allocation_percent` (0–100) for percent. Immutable while the experiment is running.",
12019
12406
  "type": "integer",
12020
12407
  "minimum": 0,
12021
12408
  "maximum": 10000
12022
12409
  },
12410
+ "reserved_headroom": {
12411
+ "description": "Basis points of the split kept empty for appended variants. Group weights must sum to `10000 − reserved_headroom`. May be shrunk (never grown into existing weights) while running when appending a variant.",
12412
+ "type": "integer",
12413
+ "minimum": 0,
12414
+ "maximum": 10000
12415
+ },
12023
12416
  "allocation_percent": {
12024
12417
  "description": "Allocation as a **percentage** (0–100). Friendlier alias for `allocation_pct`; converted to basis points server-side. Wins over `allocation_pct` if both are supplied. Immutable while running.",
12025
12418
  "type": "number",
@@ -12046,10 +12439,10 @@
12046
12439
  "number"
12047
12440
  ]
12048
12441
  },
12049
- "description": "Map of param-name → scalar type. Defines the shape of `groups[].params`. Example: `{ headline: 'string', show_cta: 'bool' }`."
12442
+ "description": "**Deprecated** — the universe owns the config schema (`param_schema`). Retained for back-compat. Map of param-name → scalar type."
12050
12443
  },
12051
12444
  "groups": {
12052
- "description": "Replacement groups. Weights must sum to 10000. Immutable while running.",
12445
+ "description": "Replacement groups. Weights must sum to `10000 − reserved_headroom`. Existing weights/values are immutable while running; a new variant may be appended into the reserved tail.",
12053
12446
  "minItems": 2,
12054
12447
  "type": "array",
12055
12448
  "items": {
@@ -12065,11 +12458,11 @@
12065
12458
  "type": "integer",
12066
12459
  "minimum": 0,
12067
12460
  "maximum": 10000,
12068
- "description": "Allocation weight in basis points (0–10000). The sum across all groups must equal exactly **10000** (100%)."
12461
+ "description": "Allocation weight in basis points (0–10000). Weights across all groups must sum to `10000 − reserved_headroom`."
12069
12462
  },
12070
12463
  "params": {
12071
12464
  "default": {},
12072
- "description": "Per-group parameter values delivered to the SDK when a caller is hashed into this group. Keys must match the experiment's `params` schema.",
12465
+ "description": "Per-variant **override map** — a subset of the universe's `param_schema` keys. Unknown keys are rejected; unset keys inherit the universe default.",
12073
12466
  "type": "object",
12074
12467
  "additionalProperties": {}
12075
12468
  }
@@ -12079,7 +12472,7 @@
12079
12472
  "weight"
12080
12473
  ],
12081
12474
  "additionalProperties": false,
12082
- "description": "One variant in the experiment. Weights across all groups must sum to 10000."
12475
+ "description": "One variant. Its `params` may only override keys declared on the universe schema."
12083
12476
  }
12084
12477
  },
12085
12478
  "significance_threshold": {
@@ -12114,7 +12507,7 @@
12114
12507
  }
12115
12508
  },
12116
12509
  "additionalProperties": false,
12117
- "description": "Body for `PATCH /api/admin/experiments/{id}`. Partial — only supplied fields change. `allocation_pct`, `groups`, `salt`, `universe`, `params` are immutable while the experiment is running (stop first)."
12510
+ "description": "Body for `PATCH /api/admin/experiments/{id}`. Partial — only supplied fields change. `allocation_pct`, `salt`, `universe` and existing group weights/values are immutable while running (stop first); a new variant may still be appended into the reserved tail."
12118
12511
  },
12119
12512
  "UpdateExperimentResponse": {
12120
12513
  "type": "object",
@@ -12191,6 +12584,18 @@
12191
12584
  "secondary"
12192
12585
  ],
12193
12586
  "description": "Metric role. `goal` drives the decision, `guardrail` blocks ship if degraded, `secondary` is informational."
12587
+ },
12588
+ "min_effect_of_interest": {
12589
+ "description": "Per-experiment override of the metric's `default_min_effect_of_interest` (relative, 0–1) — the smallest change worth acting on for THIS experiment's decision, which depends on the intervention's cost/risk. `null`/omitted falls back to the metric default. For a `guardrail` this is the non-inferiority margin (how large a regression is tolerated); for a `goal` it is the superiority threshold.",
12590
+ "default": null,
12591
+ "anyOf": [
12592
+ {
12593
+ "type": "number"
12594
+ },
12595
+ {
12596
+ "type": "null"
12597
+ }
12598
+ ]
12194
12599
  }
12195
12600
  },
12196
12601
  "required": [
@@ -12229,11 +12634,23 @@
12229
12634
  "guardrail",
12230
12635
  "secondary"
12231
12636
  ]
12637
+ },
12638
+ "min_effect_of_interest": {
12639
+ "description": "The resolved per-experiment override, or `null` when this attachment inherits the metric's `default_min_effect_of_interest`.",
12640
+ "anyOf": [
12641
+ {
12642
+ "type": "number"
12643
+ },
12644
+ {
12645
+ "type": "null"
12646
+ }
12647
+ ]
12232
12648
  }
12233
12649
  },
12234
12650
  "required": [
12235
12651
  "metric_id",
12236
- "role"
12652
+ "role",
12653
+ "min_effect_of_interest"
12237
12654
  ],
12238
12655
  "additionalProperties": false
12239
12656
  }
@@ -12342,6 +12759,17 @@
12342
12759
  }
12343
12760
  ],
12344
12761
  "description": "`1` if sample-ratio mismatch detected, else `0`."
12762
+ },
12763
+ "realized_mde": {
12764
+ "anyOf": [
12765
+ {
12766
+ "type": "number"
12767
+ },
12768
+ {
12769
+ "type": "null"
12770
+ }
12771
+ ],
12772
+ "description": "Realized minimum detectable effect (absolute, in metric units) — the smallest true effect this run could detect at the experiment's significance level and target power GIVEN the observed variance and per-arm sample sizes. A derived OUTPUT, not an input knob; compare it against the metric's minimum effect of interest to judge whether the run is adequately powered. `null` on the control row and on empty arms."
12345
12773
  }
12346
12774
  },
12347
12775
  "required": [
@@ -12352,7 +12780,8 @@
12352
12780
  "mean",
12353
12781
  "delta_pct",
12354
12782
  "p_value",
12355
- "srm_detected"
12783
+ "srm_detected",
12784
+ "realized_mde"
12356
12785
  ],
12357
12786
  "additionalProperties": false
12358
12787
  }
@@ -13505,6 +13934,44 @@
13505
13934
  ],
13506
13935
  "additionalProperties": false
13507
13936
  },
13937
+ "UniverseParam": {
13938
+ "type": "object",
13939
+ "properties": {
13940
+ "name": {
13941
+ "type": "string",
13942
+ "minLength": 1,
13943
+ "maxLength": 128,
13944
+ "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
13945
+ "description": "Param key, referenced by `groups[].params` on every experiment in the universe."
13946
+ },
13947
+ "type": {
13948
+ "type": "string",
13949
+ "enum": [
13950
+ "bool",
13951
+ "string",
13952
+ "int",
13953
+ "number"
13954
+ ],
13955
+ "description": "Scalar type of the param's value."
13956
+ },
13957
+ "default": {
13958
+ "description": "Universe-wide default. Returned when the assigned variant doesn't override this param (resolution: variant override → universe default)."
13959
+ }
13960
+ },
13961
+ "required": [
13962
+ "name",
13963
+ "type",
13964
+ "default"
13965
+ ],
13966
+ "additionalProperties": false
13967
+ },
13968
+ "UniverseParamSchema": {
13969
+ "type": "array",
13970
+ "items": {
13971
+ "$ref": "#/components/schemas/UniverseParam"
13972
+ },
13973
+ "description": "The universe's ordered config schema — the single source of truth for its params."
13974
+ },
13508
13975
  "ListUniversesResponse": {
13509
13976
  "type": "object",
13510
13977
  "properties": {
@@ -13523,6 +13990,17 @@
13523
13990
  "pattern": "^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?)?$",
13524
13991
  "description": "Stable universe key. Single segment or `folder.name`. Lowercase letters, digits, `_` or `-`; max 128 chars. Immutable after create."
13525
13992
  },
13993
+ "description": {
13994
+ "anyOf": [
13995
+ {
13996
+ "type": "string"
13997
+ },
13998
+ {
13999
+ "type": "null"
14000
+ }
14001
+ ],
14002
+ "description": "Human-readable blurb shown in the universe picker/hovercard."
14003
+ },
13526
14004
  "unitType": {
13527
14005
  "type": "string",
13528
14006
  "description": "Unit of randomisation. Snake-case in request (`unit_type`), camelCase in response."
@@ -13543,6 +14021,23 @@
13543
14021
  ],
13544
14022
  "description": "Reserved holdout bucket range, or `null` if none."
13545
14023
  },
14024
+ "recommendedHeadroom": {
14025
+ "type": "integer",
14026
+ "minimum": 0,
14027
+ "maximum": 10000,
14028
+ "description": "Basis points of headroom seeded into each new experiment's reserved tail (0 = none)."
14029
+ },
14030
+ "paramSchema": {
14031
+ "anyOf": [
14032
+ {
14033
+ "$ref": "#/components/schemas/UniverseParamSchema"
14034
+ },
14035
+ {
14036
+ "type": "null"
14037
+ }
14038
+ ],
14039
+ "description": "The universe-owned config schema, or `null` on pre-rework universes not yet seeded."
14040
+ },
13546
14041
  "createdAt": {
13547
14042
  "type": "string",
13548
14043
  "description": "ISO-8601 timestamp of creation."
@@ -13587,6 +14082,19 @@
13587
14082
  "folder": {
13588
14083
  "$ref": "#/components/schemas/Folder"
13589
14084
  },
14085
+ "description": {
14086
+ "default": null,
14087
+ "anyOf": [
14088
+ {
14089
+ "type": "string",
14090
+ "maxLength": 2000
14091
+ },
14092
+ {
14093
+ "type": "null"
14094
+ }
14095
+ ],
14096
+ "description": "Human-readable blurb shown in the universe picker/hovercard."
14097
+ },
13590
14098
  "unit_type": {
13591
14099
  "default": "user_id",
13592
14100
  "description": "Unit of randomisation. Typically `user_id`. Use `account_id` to keep whole accounts in the same group across an experiment.",
@@ -13610,6 +14118,25 @@
13610
14118
  }
13611
14119
  ],
13612
14120
  "description": "Inclusive `[lo, hi]` bucket range (0–9999) reserved as the **holdout** — callers hashed into this slice are excluded from every experiment in the universe. `null` disables the holdout. Pro plan or higher required."
14121
+ },
14122
+ "recommended_headroom": {
14123
+ "default": 0,
14124
+ "type": "integer",
14125
+ "minimum": 0,
14126
+ "maximum": 10000,
14127
+ "description": "Basis points of reserved headroom seeded into each new experiment created in this universe (0 = none). Lets variants be appended into a running experiment without reshuffling."
14128
+ },
14129
+ "param_schema": {
14130
+ "default": null,
14131
+ "anyOf": [
14132
+ {
14133
+ "$ref": "#/components/schemas/UniverseParamSchema"
14134
+ },
14135
+ {
14136
+ "type": "null"
14137
+ }
14138
+ ],
14139
+ "description": "The universe-owned config schema — an ordered `{ name, type, default }[]`. Experiments may only override values per variant, never add fields. `null` starts an empty schema."
13613
14140
  }
13614
14141
  },
13615
14142
  "required": [
@@ -13657,6 +14184,18 @@
13657
14184
  "folder": {
13658
14185
  "$ref": "#/components/schemas/Folder"
13659
14186
  },
14187
+ "description": {
14188
+ "anyOf": [
14189
+ {
14190
+ "type": "string",
14191
+ "maxLength": 2000
14192
+ },
14193
+ {
14194
+ "type": "null"
14195
+ }
14196
+ ],
14197
+ "description": "Human-readable blurb shown in the universe picker/hovercard."
14198
+ },
13660
14199
  "holdout_range": {
13661
14200
  "anyOf": [
13662
14201
  {
@@ -13674,10 +14213,27 @@
13674
14213
  }
13675
14214
  ],
13676
14215
  "description": "Inclusive `[lo, hi]` bucket range (0–9999) reserved as the **holdout** — callers hashed into this slice are excluded from every experiment in the universe. `null` disables the holdout. Pro plan or higher required."
14216
+ },
14217
+ "recommended_headroom": {
14218
+ "type": "integer",
14219
+ "minimum": 0,
14220
+ "maximum": 10000,
14221
+ "description": "Basis points of reserved headroom seeded into new experiments in this universe."
14222
+ },
14223
+ "param_schema": {
14224
+ "anyOf": [
14225
+ {
14226
+ "$ref": "#/components/schemas/UniverseParamSchema"
14227
+ },
14228
+ {
14229
+ "type": "null"
14230
+ }
14231
+ ],
14232
+ "description": "Replace the universe config schema. Additive changes + default edits are always allowed; removing a param a running experiment overrides is rejected (deprecate-only)."
13677
14233
  }
13678
14234
  },
13679
14235
  "additionalProperties": false,
13680
- "description": "Body for `PATCH /api/admin/universes/{id}`. Only `holdout_range` is mutable — name and unit_type are immutable after create."
14236
+ "description": "Body for `PATCH /api/admin/universes/{id}`. `name` and `unit_type` are immutable after create; `holdout_range`, `description`, `recommended_headroom` and `param_schema` are mutable."
13681
14237
  },
13682
14238
  "UpdateUniverseResponse": {
13683
14239
  "type": "object",
@@ -14709,8 +15265,8 @@
14709
15265
  "description": "Winsorise percentile applied to the metric.",
14710
15266
  "type": "number"
14711
15267
  },
14712
- "minDetectableEffect": {
14713
- "description": "Configured MDE, or `null`.",
15268
+ "defaultMinEffectOfInterest": {
15269
+ "description": "Metric-level default minimum effect of interest (relative, 0–1), or `null`.",
14714
15270
  "anyOf": [
14715
15271
  {
14716
15272
  "type": "number"
@@ -14760,9 +15316,9 @@
14760
15316
  "minimum": 1,
14761
15317
  "maximum": 99
14762
15318
  },
14763
- "MetricMinDetectableEffect": {
15319
+ "MetricDefaultMinEffectOfInterest": {
14764
15320
  "default": null,
14765
- "description": "Minimum detectable effect (relative, 0–1) for power planning. `null` to omit.",
15321
+ "description": "Default minimum effect of interest (relative, 0–1) — the smallest change in this metric worth acting on, used as the power-planning baseline. Intrinsic to the metric; an experiment overrides it per-attachment with `min_effect_of_interest` when a specific decision has a different cost/risk bar. `null` to omit.",
14766
15322
  "anyOf": [
14767
15323
  {
14768
15324
  "type": "number"
@@ -14800,8 +15356,8 @@
14800
15356
  "winsorize_pct": {
14801
15357
  "$ref": "#/components/schemas/MetricWinsorizePct"
14802
15358
  },
14803
- "min_detectable_effect": {
14804
- "$ref": "#/components/schemas/MetricMinDetectableEffect"
15359
+ "default_min_effect_of_interest": {
15360
+ "$ref": "#/components/schemas/MetricDefaultMinEffectOfInterest"
14805
15361
  },
14806
15362
  "direction": {
14807
15363
  "$ref": "#/components/schemas/MetricDirection"
@@ -14833,8 +15389,8 @@
14833
15389
  "winsorize_pct": {
14834
15390
  "$ref": "#/components/schemas/MetricWinsorizePct"
14835
15391
  },
14836
- "min_detectable_effect": {
14837
- "$ref": "#/components/schemas/MetricMinDetectableEffect"
15392
+ "default_min_effect_of_interest": {
15393
+ "$ref": "#/components/schemas/MetricDefaultMinEffectOfInterest"
14838
15394
  },
14839
15395
  "direction": {
14840
15396
  "$ref": "#/components/schemas/MetricDirection"
@@ -14931,8 +15487,8 @@
14931
15487
  "description": "Winsorise percentile applied to the metric.",
14932
15488
  "type": "number"
14933
15489
  },
14934
- "minDetectableEffect": {
14935
- "description": "Configured MDE, or `null`.",
15490
+ "defaultMinEffectOfInterest": {
15491
+ "description": "Metric-level default minimum effect of interest (relative, 0–1), or `null`.",
14936
15492
  "anyOf": [
14937
15493
  {
14938
15494
  "type": "number"
@@ -14991,8 +15547,8 @@
14991
15547
  "winsorize_pct": {
14992
15548
  "$ref": "#/components/schemas/MetricWinsorizePct"
14993
15549
  },
14994
- "min_detectable_effect": {
14995
- "$ref": "#/components/schemas/MetricMinDetectableEffect"
15550
+ "default_min_effect_of_interest": {
15551
+ "$ref": "#/components/schemas/MetricDefaultMinEffectOfInterest"
14996
15552
  },
14997
15553
  "direction": {
14998
15554
  "$ref": "#/components/schemas/MetricDirection"
@@ -15019,8 +15575,8 @@
15019
15575
  "winsorize_pct": {
15020
15576
  "$ref": "#/components/schemas/MetricWinsorizePct"
15021
15577
  },
15022
- "min_detectable_effect": {
15023
- "$ref": "#/components/schemas/MetricMinDetectableEffect"
15578
+ "default_min_effect_of_interest": {
15579
+ "$ref": "#/components/schemas/MetricDefaultMinEffectOfInterest"
15024
15580
  },
15025
15581
  "direction": {
15026
15582
  "$ref": "#/components/schemas/MetricDirection"
@@ -15044,15 +15600,15 @@
15044
15600
  "winsorize_pct": {
15045
15601
  "$ref": "#/components/schemas/MetricWinsorizePct"
15046
15602
  },
15047
- "min_detectable_effect": {
15048
- "$ref": "#/components/schemas/MetricMinDetectableEffect"
15603
+ "default_min_effect_of_interest": {
15604
+ "$ref": "#/components/schemas/MetricDefaultMinEffectOfInterest"
15049
15605
  },
15050
15606
  "direction": {
15051
15607
  "$ref": "#/components/schemas/MetricDirection"
15052
15608
  }
15053
15609
  },
15054
15610
  "additionalProperties": false,
15055
- "description": "Update-metric variant that leaves the query untouched (metadata-only edit — folder, event, winsorisation, MDE, direction)."
15611
+ "description": "Update-metric variant that leaves the query untouched (metadata-only edit — folder, event, winsorisation, default minimum effect of interest, direction)."
15056
15612
  },
15057
15613
  "UpdateMetricRequest": {
15058
15614
  "oneOf": [
@@ -16093,6 +16649,17 @@
16093
16649
  },
16094
16650
  "description": "Distinct, id-normalized routes this error has surfaced on (UUIDs/ids collapsed to `#`)."
16095
16651
  },
16652
+ "stackTail": {
16653
+ "anyOf": [
16654
+ {
16655
+ "type": "string"
16656
+ },
16657
+ {
16658
+ "type": "null"
16659
+ }
16660
+ ],
16661
+ "description": "The last 3 non-empty lines of the latest occurrence's stack — the innermost frames closest to where the throw bubbled out, baked in as an at-a-glance locator. `null` when the source supplied no stack. Fetch the full stack via `related.error`."
16662
+ },
16096
16663
  "sdkVersion": {
16097
16664
  "anyOf": [
16098
16665
  {
@@ -16394,6 +16961,17 @@
16394
16961
  "OpsMeasurePlanContext": {
16395
16962
  "type": "object",
16396
16963
  "properties": {
16964
+ "goal": {
16965
+ "anyOf": [
16966
+ {
16967
+ "type": "string"
16968
+ },
16969
+ {
16970
+ "type": "null"
16971
+ }
16972
+ ],
16973
+ "description": "The customer's underlying intent in their own words — WHAT they want to learn or achieve and WHY, independent of the specific steps. The plan below is a best-effort proposal made without repo access; when the code reveals a better path, this goal is the target you optimize for, not literal step-adherence."
16974
+ },
16397
16975
  "created": {
16398
16976
  "type": "array",
16399
16977
  "items": {
@@ -16997,6 +17575,133 @@
16997
17575
  "additionalProperties": false,
16998
17576
  "description": "Response for the update / link-pr endpoints."
16999
17577
  },
17578
+ "OpsCommentAuthorType": {
17579
+ "type": "string",
17580
+ "enum": [
17581
+ "user",
17582
+ "system"
17583
+ ],
17584
+ "description": "Who wrote a comment. `user` is a teammate (their email is in `authorEmail`); `system` is Jarvis — the AI agent — which authors comments when an ops-notify escalation lands on the item and when it is `@shipeasy`-mentioned in a thread."
17585
+ },
17586
+ "OpsComment": {
17587
+ "type": "object",
17588
+ "properties": {
17589
+ "id": {
17590
+ "type": "string",
17591
+ "description": "Comment id."
17592
+ },
17593
+ "feedbackId": {
17594
+ "type": "string",
17595
+ "description": "The queue item this comment hangs off."
17596
+ },
17597
+ "parentId": {
17598
+ "anyOf": [
17599
+ {
17600
+ "type": "string"
17601
+ },
17602
+ {
17603
+ "type": "null"
17604
+ }
17605
+ ],
17606
+ "description": "The top-level comment this is a reply to, or `null` for a top-level comment."
17607
+ },
17608
+ "authorType": {
17609
+ "$ref": "#/components/schemas/OpsCommentAuthorType"
17610
+ },
17611
+ "authorEmail": {
17612
+ "anyOf": [
17613
+ {
17614
+ "type": "string"
17615
+ },
17616
+ {
17617
+ "type": "null"
17618
+ }
17619
+ ],
17620
+ "description": "The teammate who wrote it, or `null` for a `system` (Jarvis) comment."
17621
+ },
17622
+ "body": {
17623
+ "type": "string",
17624
+ "description": "Comment body as markdown (may contain sanitised inline image/video embeds)."
17625
+ },
17626
+ "createdAt": {
17627
+ "type": "string",
17628
+ "description": "ISO-8601 creation timestamp."
17629
+ },
17630
+ "updatedAt": {
17631
+ "type": "string",
17632
+ "description": "ISO-8601 last-edit timestamp."
17633
+ }
17634
+ },
17635
+ "required": [
17636
+ "id",
17637
+ "feedbackId",
17638
+ "parentId",
17639
+ "authorType",
17640
+ "authorEmail",
17641
+ "body",
17642
+ "createdAt",
17643
+ "updatedAt"
17644
+ ],
17645
+ "additionalProperties": false,
17646
+ "description": "A single comment on a queue item."
17647
+ },
17648
+ "ListOpsCommentsResponse": {
17649
+ "type": "object",
17650
+ "properties": {
17651
+ "comments": {
17652
+ "type": "array",
17653
+ "items": {
17654
+ "$ref": "#/components/schemas/OpsComment"
17655
+ }
17656
+ }
17657
+ },
17658
+ "required": [
17659
+ "comments"
17660
+ ],
17661
+ "additionalProperties": false,
17662
+ "description": "Response for `GET /api/admin/ops/{handle}/comments` — the thread, oldest first."
17663
+ },
17664
+ "CreateOpsCommentRequest": {
17665
+ "type": "object",
17666
+ "properties": {
17667
+ "body": {
17668
+ "type": "string",
17669
+ "minLength": 1,
17670
+ "maxLength": 10000,
17671
+ "description": "The comment body as markdown. Mentions (`@teammate`, `@shipeasy`) are parsed from it."
17672
+ },
17673
+ "parentId": {
17674
+ "anyOf": [
17675
+ {
17676
+ "type": "string"
17677
+ },
17678
+ {
17679
+ "type": "null"
17680
+ }
17681
+ ],
17682
+ "description": "Reply under this top-level comment. Omit / `null` for a top-level comment. Replying to a reply attaches to the same top-level parent (threading is one level deep)."
17683
+ }
17684
+ },
17685
+ "required": [
17686
+ "body"
17687
+ ],
17688
+ "additionalProperties": false,
17689
+ "description": "Body for `POST /api/admin/ops/{handle}/comments`."
17690
+ },
17691
+ "CreateOpsCommentResponse": {
17692
+ "type": "object",
17693
+ "properties": {
17694
+ "id": {
17695
+ "type": "string",
17696
+ "description": "Newly created comment id."
17697
+ }
17698
+ },
17699
+ "required": [
17700
+ "id"
17701
+ ],
17702
+ "additionalProperties": false,
17703
+ "description": "Response for `POST /api/admin/ops/{handle}/comments`."
17704
+ },
17000
17705
  "NotifyOpsRequest": {
17001
17706
  "type": "object",
17002
17707
  "properties": {
@@ -17367,7 +18072,9 @@
17367
18072
  "type": "string",
17368
18073
  "enum": [
17369
18074
  "free",
17370
- "paid"
18075
+ "pro",
18076
+ "business",
18077
+ "enterprise"
17371
18078
  ],
17372
18079
  "description": "Billing plan tier."
17373
18080
  },
@@ -17472,6 +18179,69 @@
17472
18179
  ],
17473
18180
  "description": "Whether the feedback/ops module is enabled."
17474
18181
  },
18182
+ "minSampleSize": {
18183
+ "type": "integer",
18184
+ "description": "Verdict power guard — minimum users per arm before a ship/hold verdict."
18185
+ },
18186
+ "minRuntimeDays": {
18187
+ "type": "integer",
18188
+ "description": "Minimum days an experiment must run before a verdict (peeking guard)."
18189
+ },
18190
+ "defaultPower": {
18191
+ "type": "number",
18192
+ "description": "Target statistical power (1−β) feeding the realized-MDE calculation."
18193
+ },
18194
+ "ciConfidence": {
18195
+ "type": "number",
18196
+ "description": "Confidence level for the interval surfaced on results."
18197
+ },
18198
+ "defaultAllocationPct": {
18199
+ "type": "integer",
18200
+ "description": "Default traffic allocation (basis points) new experiments start with."
18201
+ },
18202
+ "defaultHoldout": {
18203
+ "type": "integer",
18204
+ "description": "Default holdout carve-out (basis points) that seeds each new universe's holdout."
18205
+ },
18206
+ "defaultWinsorizePct": {
18207
+ "type": "integer",
18208
+ "description": "Default winsorization percentile new metrics start with."
18209
+ },
18210
+ "defaultMei": {
18211
+ "anyOf": [
18212
+ {
18213
+ "type": "number"
18214
+ },
18215
+ {
18216
+ "type": "null"
18217
+ }
18218
+ ],
18219
+ "description": "Default minimum effect of interest (relative, 0–1) new metrics start with, or null."
18220
+ },
18221
+ "cupedBaselineDays": {
18222
+ "type": "integer",
18223
+ "description": "CUPED baseline window — days of pre-experiment history, frozen at start."
18224
+ },
18225
+ "cupedMinOverlap": {
18226
+ "type": "number",
18227
+ "description": "CUPED selection-bias guard — min share of users with a baseline, else skip."
18228
+ },
18229
+ "cupedMinBaselineUsers": {
18230
+ "type": "integer",
18231
+ "description": "CUPED — minimum users with a baseline before it runs at all."
18232
+ },
18233
+ "msprtTauMeiFactor": {
18234
+ "type": "number",
18235
+ "description": "mSPRT prior width — τ = minimum effect of interest × this factor."
18236
+ },
18237
+ "msprtTauSdFactor": {
18238
+ "type": "number",
18239
+ "description": "mSPRT fallback prior width — τ = this × control SD when no MEI is set."
18240
+ },
18241
+ "srmThreshold": {
18242
+ "type": "number",
18243
+ "description": "SRM chi-square p-value below which the run is called invalid."
18244
+ },
17475
18245
  "createdAt": {
17476
18246
  "type": "string",
17477
18247
  "description": "ISO-8601 timestamp of project creation."
@@ -17664,6 +18434,97 @@
17664
18434
  "moduleEvents": {
17665
18435
  "type": "boolean",
17666
18436
  "description": "Enable/disable the events module."
18437
+ },
18438
+ "minSampleSize": {
18439
+ "type": "integer",
18440
+ "minimum": 2,
18441
+ "maximum": 1000000,
18442
+ "description": "Verdict power guard — minimum users per arm before a ship/hold verdict."
18443
+ },
18444
+ "minRuntimeDays": {
18445
+ "type": "integer",
18446
+ "minimum": 0,
18447
+ "maximum": 365,
18448
+ "description": "Minimum days an experiment must run before a verdict (peeking guard)."
18449
+ },
18450
+ "defaultPower": {
18451
+ "type": "number",
18452
+ "minimum": 0.5,
18453
+ "maximum": 0.99,
18454
+ "description": "Target statistical power (1−β) feeding the realized-MDE calculation."
18455
+ },
18456
+ "ciConfidence": {
18457
+ "type": "number",
18458
+ "minimum": 0.5,
18459
+ "maximum": 0.999,
18460
+ "description": "Confidence level for the interval surfaced on results (any value in [0.5, 0.999], e.g. 0.90, 0.95, 0.975, 0.99)."
18461
+ },
18462
+ "defaultAllocationPct": {
18463
+ "type": "integer",
18464
+ "minimum": 1,
18465
+ "maximum": 10000,
18466
+ "description": "Default traffic allocation (basis points, 1000 = 10%) new experiments start with; overridable per experiment."
18467
+ },
18468
+ "defaultHoldout": {
18469
+ "type": "integer",
18470
+ "minimum": 0,
18471
+ "maximum": 10000,
18472
+ "description": "Default holdout carve-out (basis points) that seeds each new universe's holdout (0 = none)."
18473
+ },
18474
+ "defaultWinsorizePct": {
18475
+ "type": "integer",
18476
+ "minimum": 1,
18477
+ "maximum": 99,
18478
+ "description": "Default winsorization percentile new metrics start with; overridable per metric."
18479
+ },
18480
+ "defaultMei": {
18481
+ "anyOf": [
18482
+ {
18483
+ "type": "number",
18484
+ "minimum": 0,
18485
+ "maximum": 1
18486
+ },
18487
+ {
18488
+ "type": "null"
18489
+ }
18490
+ ],
18491
+ "description": "Default minimum effect of interest (relative, 0–1) new metrics start with; overridable per metric and per experiment. Null clears it."
18492
+ },
18493
+ "cupedBaselineDays": {
18494
+ "type": "integer",
18495
+ "minimum": 1,
18496
+ "maximum": 365,
18497
+ "description": "CUPED baseline window — days of pre-experiment history, frozen at start."
18498
+ },
18499
+ "cupedMinOverlap": {
18500
+ "type": "number",
18501
+ "minimum": 0.01,
18502
+ "maximum": 0.99,
18503
+ "description": "CUPED selection-bias guard — min share of users with a baseline, else skip."
18504
+ },
18505
+ "cupedMinBaselineUsers": {
18506
+ "type": "integer",
18507
+ "minimum": 10,
18508
+ "maximum": 100000,
18509
+ "description": "CUPED — minimum users with a baseline before it runs at all."
18510
+ },
18511
+ "msprtTauMeiFactor": {
18512
+ "type": "number",
18513
+ "minimum": 0.1,
18514
+ "maximum": 2,
18515
+ "description": "mSPRT prior width — τ = minimum effect of interest × this factor."
18516
+ },
18517
+ "msprtTauSdFactor": {
18518
+ "type": "number",
18519
+ "minimum": 0.05,
18520
+ "maximum": 1,
18521
+ "description": "mSPRT fallback prior width — τ = this × control SD when no MEI is set."
18522
+ },
18523
+ "srmThreshold": {
18524
+ "type": "number",
18525
+ "minimum": 0.0001,
18526
+ "maximum": 0.05,
18527
+ "description": "SRM chi-square p-value below which the run is called invalid."
17667
18528
  }
17668
18529
  },
17669
18530
  "additionalProperties": false,
@@ -18329,6 +19190,102 @@
18329
19190
  "additionalProperties": false,
18330
19191
  "description": "Result of a one-shot set-and-publish."
18331
19192
  },
19193
+ "ErrorOccurrence": {
19194
+ "type": "object",
19195
+ "description": "One sampled instance behind a tracked error — the minimal per-occurrence payload (what varies between instances; everything else lives on the parent `ErrorRecord`).",
19196
+ "properties": {
19197
+ "id": {
19198
+ "type": "string",
19199
+ "description": "Stable opaque occurrence id."
19200
+ },
19201
+ "message": {
19202
+ "type": "string",
19203
+ "description": "This instance's raw message (unlike the fingerprint, never normalized)."
19204
+ },
19205
+ "stack": {
19206
+ "description": "This instance's stack trace, or `null` if none was captured.",
19207
+ "anyOf": [
19208
+ {
19209
+ "type": "string"
19210
+ },
19211
+ {
19212
+ "type": "null"
19213
+ }
19214
+ ]
19215
+ },
19216
+ "url": {
19217
+ "description": "This instance's raw URL (ids intact), or `null`.",
19218
+ "anyOf": [
19219
+ {
19220
+ "type": "string"
19221
+ },
19222
+ {
19223
+ "type": "null"
19224
+ }
19225
+ ]
19226
+ },
19227
+ "env": {
19228
+ "description": "Published env this instance ran against, or `null`.",
19229
+ "anyOf": [
19230
+ {
19231
+ "type": "string"
19232
+ },
19233
+ {
19234
+ "type": "null"
19235
+ }
19236
+ ]
19237
+ },
19238
+ "side": {
19239
+ "description": "Which SDK side reported it — `client` or `server`. `null` if unknown.",
19240
+ "anyOf": [
19241
+ {
19242
+ "type": "string"
19243
+ },
19244
+ {
19245
+ "type": "null"
19246
+ }
19247
+ ]
19248
+ },
19249
+ "sdkVersion": {
19250
+ "description": "`@shipeasy/sdk` version of this instance, or `null`.",
19251
+ "anyOf": [
19252
+ {
19253
+ "type": "string"
19254
+ },
19255
+ {
19256
+ "type": "null"
19257
+ }
19258
+ ]
19259
+ },
19260
+ "extrasJson": {
19261
+ "description": "This instance's sanitized extras, JSON-encoded. `null` if none.",
19262
+ "anyOf": [
19263
+ {
19264
+ "type": "string"
19265
+ },
19266
+ {
19267
+ "type": "null"
19268
+ }
19269
+ ]
19270
+ },
19271
+ "sampleRate": {
19272
+ "type": "integer",
19273
+ "minimum": 1,
19274
+ "description": "1-in-N sampling rate in force when this instance was kept (`1` = the issue was still being recorded exhaustively)."
19275
+ },
19276
+ "seenAt": {
19277
+ "type": "string",
19278
+ "description": "ISO-8601 timestamp this instance was ingested."
19279
+ }
19280
+ },
19281
+ "required": [
19282
+ "id",
19283
+ "message",
19284
+ "sampleRate",
19285
+ "seenAt"
19286
+ ],
19287
+ "additionalProperties": false
19288
+ },
18332
19289
  "ErrorRecord": {
18333
19290
  "type": "object",
18334
19291
  "description": "A tracked production error — one row per distinct issue, keyed by `fingerprint`. Rows are never created by hand: an ingestion path (worker log drain / the `see()` SDK reporter) folds each occurrence into the matching row, bumping `count` and `lastSeenAt`. The admin surface only lists them, reads one, and flips `status`.\n\nField names are camelCase (the D1 row projected through Drizzle). Many columns are nullable because the reporting source may not supply them.",
@@ -18343,7 +19300,7 @@
18343
19300
  },
18344
19301
  "fingerprint": {
18345
19302
  "type": "string",
18346
- "description": "Stable dedupe key (hash of `errorType` + normalized `message` + consequence). Unique per project — every occurrence with the same fingerprint folds into this one row."
19303
+ "description": "Stable dedupe key — the issue TITLE (hash of `errorType` + `subject` + `outcome`, plus the violation name for violations). Unique per project — every occurrence with the same title folds into this one row; per-instance variety (message/stack/extras) lives in `occurrences`."
18347
19304
  },
18348
19305
  "causedByFingerprint": {
18349
19306
  "description": "Fingerprint of the issue this one descends from — set when `see()` reported the same error (re-throw) or its `{ cause }` (wrap) at an inner boundary first. Points at another error's `fingerprint` in the same project (soft reference, no FK). `null` for a root issue.",
@@ -18502,7 +19459,14 @@
18502
19459
  "count": {
18503
19460
  "type": "integer",
18504
19461
  "minimum": 1,
18505
- "description": "Number of folded occurrences for this fingerprint."
19462
+ "description": "EXACT number of folded occurrences for this fingerprint — every occurrence increments it, regardless of detail-row sampling."
19463
+ },
19464
+ "occurrences": {
19465
+ "type": "array",
19466
+ "description": "Sampled per-instance detail rows behind this issue, newest first. Returned only by `GET /api/admin/errors/{id}` (never in list responses). The parent row's `count` / `firstSeenAt` / `lastSeenAt` are exact; these rows are a *sampled sketch* of the instances — exhaustive while the issue is small, thinning to roughly 1-in-10 past 10 occurrences, 1-in-100 past 100, and 1-in-1000 past 1000 (each row's `sampleRate` records the rate in force when it was kept), capped at the newest 100 rows.",
19467
+ "items": {
19468
+ "$ref": "#/components/schemas/ErrorOccurrence"
19469
+ }
18506
19470
  },
18507
19471
  "status": {
18508
19472
  "type": "string",
@@ -18546,7 +19510,7 @@
18546
19510
  },
18547
19511
  "ListErrorsResponse": {
18548
19512
  "type": "array",
18549
- "description": "A bare JSON array of tracked errors, ordered by `lastSeenAt` descending. There is no pagination envelope — `limit` caps the page size.",
19513
+ "description": "A bare JSON array of tracked errors, ordered by `lastSeenAt` descending. There is no pagination envelope — `limit` caps the page size. List rows never include `occurrences` — read one error to get them.",
18550
19514
  "items": {
18551
19515
  "$ref": "#/components/schemas/ErrorRecord"
18552
19516
  }
@@ -19640,10 +20604,11 @@
19640
20604
  "gates:evaluate",
19641
20605
  "events:write",
19642
20606
  "configs:write",
19643
- "experiments:write"
20607
+ "experiments:write",
20608
+ "tickets:public_create"
19644
20609
  ]
19645
20610
  },
19646
- "description": "Optional permission strings recorded on the key for audit/display."
20611
+ "description": "Optional permission strings recorded on the key. `tickets:public_create` is enforced: a client key needs it (plus the project's `allowPublicTickets` setting) to file a `pending_approval` bug via the public `POST /cli/report` endpoint. The rest are audit/display only."
19647
20612
  },
19648
20613
  "expiresInDays": {
19649
20614
  "anyOf": [