@curviate/sdk 0.43.0 → 0.44.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.
package/dist/index.js CHANGED
@@ -281,10 +281,17 @@ var ERROR_CODES = [
281
281
  "IMPORT_QUEUE_FULL",
282
282
  // Sequences (`Sequence`, `validateSequence`). `SEQUENCE_INVALID` (422 from
283
283
  // the API, or thrown offline by `toDocument()` / `Sequence.fromDocument`):
284
- // the Sequence document breaks one or more rules. `errors` lists every one as
285
- // `{path, rule}` in step order, so one fix round covers them all. Nothing is
286
- // saved. user_fixable, never retryable as sent.
284
+ // the Sequence document breaks one or more rules. `errors` lists every one
285
+ // (`path`, `rule`, `code`, `pointer`, `hint`, `docs_url`) in step order, so
286
+ // one fix round covers them all. Nothing is saved. user_fixable, never
287
+ // retryable as sent.
288
+ //
289
+ // `SEQUENCE_CONFLICT` (409): `motions.update` sent `if_sequence_revision`
290
+ // and someone saved the Sequence since; nothing is saved. `current` carries
291
+ // the stored document and its `sequence_revision`: merge, or send yours again
292
+ // with that revision to replace it.
287
293
  "SEQUENCE_INVALID",
294
+ "SEQUENCE_CONFLICT",
288
295
  // Motions and Enrollments (`motions`, `enrollments`).
289
296
  //
290
297
  // - `MOTION_NOT_STARTABLE` (422): `start` or `resume` found gaps; `errors`
@@ -294,15 +301,33 @@ var ERROR_CODES = [
294
301
  // Motion's status; the message names it and the operations it allows.
295
302
  // Only a draft can be deleted: archive instead.
296
303
  // - `ENROLLMENT_NOT_ACTIVE` (409): the Enrollment's status does not take
297
- // this operation (stop: pending, active or paused; signal: active or
298
- // paused; resume: active, paused or finished, not when the account or
299
- // Contact is gone).
304
+ // this operation (stop and pause: pending, active or paused; events:
305
+ // active or paused; resume: active, paused or finished, not when the
306
+ // account or Contact is gone).
307
+ // - `STEP_IN_FLIGHT` (409): a Step of the Enrollment is being sent right
308
+ // now; pause and reassign wait until it settles. Retry in a minute.
309
+ // - `NOTHING_TO_RETRY` (409, or thrown by `enrollments.retry`): Retry found no
310
+ // Step whose latest outcome is `failed`; nothing was resumed or sent.
311
+ // - `ACCOUNT_ALREADY_ENGAGED` (409): reassign refused: the Enrollment's
312
+ // account already did something the Contact can see.
313
+ // - `ALREADY_ENROLLED_ON_ACCOUNT` (409): reassign refused: the target
314
+ // account already holds this Contact in the Motion, or is its own account.
315
+ // - `KNOWN_CHAT` (409): reassign refused: the Motion excludes known chats
316
+ // and the Contact already has a chat with your team.
300
317
  // - `CONTACT_NOT_ENROLLABLE` (422): adding a Contact to a Motion from a chat
301
318
  // was refused (already enrolled, excluded, in another Motion, filtered).
319
+ // - `CHAT_NOT_ONE_TO_ONE` (422): a Unibox Contact action on a group chat,
320
+ // or a chat whose counterpart is not a person.
302
321
  "MOTION_NOT_STARTABLE",
303
322
  "MOTION_STATUS_CONFLICT",
304
323
  "ENROLLMENT_NOT_ACTIVE",
324
+ "STEP_IN_FLIGHT",
325
+ "NOTHING_TO_RETRY",
326
+ "ACCOUNT_ALREADY_ENGAGED",
327
+ "ALREADY_ENROLLED_ON_ACCOUNT",
328
+ "KNOWN_CHAT",
305
329
  "CONTACT_NOT_ENROLLABLE",
330
+ "CHAT_NOT_ONE_TO_ONE",
306
331
  // Client-side, never sent by the API. `CLIENT_TIMEOUT`: no response within
307
332
  // the client's `timeout` (default 30 s, per attempt). A read is retried
308
333
  // first; a write is not, and may still have reached the API, so read back
@@ -330,6 +355,7 @@ var CurviateError = class _CurviateError extends Error {
330
355
  this.blocked = init.blocked;
331
356
  this.connectionLocation = init.connectionLocation;
332
357
  this.errors = init.errors;
358
+ this.current = init.current;
333
359
  this.feature = init.feature;
334
360
  this.requiredTier = init.requiredTier;
335
361
  this.currentTier = init.currentTier;
@@ -360,6 +386,7 @@ var CurviateError = class _CurviateError extends Error {
360
386
  if (this.blocked !== void 0) json.blocked = this.blocked;
361
387
  if (this.connectionLocation !== void 0) json.connectionLocation = this.connectionLocation;
362
388
  if (this.errors !== void 0) json.errors = this.errors;
389
+ if (this.current !== void 0) json.current = this.current;
363
390
  if (this.feature !== void 0) json.feature = this.feature;
364
391
  if (this.requiredTier !== void 0) json.requiredTier = this.requiredTier;
365
392
  if (this.currentTier !== void 0) json.currentTier = this.currentTier;
@@ -403,11 +430,19 @@ function resolveConfig(config) {
403
430
  }
404
431
 
405
432
  // src/transport.ts
433
+ var DETAIL_KEYS = ["code", "message", "pointer", "hint", "docs_url"];
406
434
  function toErrorDetails(errors) {
407
435
  if (!Array.isArray(errors)) return void 0;
408
436
  return errors.flatMap((e) => {
409
- const { path, rule } = typeof e === "object" && e !== null ? e : {};
410
- return typeof path === "string" && typeof rule === "string" ? [{ path, rule }] : [];
437
+ const entry2 = typeof e === "object" && e !== null ? e : {};
438
+ const { path, rule } = entry2;
439
+ if (typeof path !== "string" || typeof rule !== "string") return [];
440
+ const detail = { path, rule };
441
+ for (const k of DETAIL_KEYS) {
442
+ const v = entry2[k];
443
+ if (typeof v === "string") detail[k] = v;
444
+ }
445
+ return [detail];
411
446
  });
412
447
  }
413
448
  var BASE_DELAY_MS = 500;
@@ -497,6 +532,7 @@ async function errorFromResponse(res) {
497
532
  ...typeof env?.blocked === "boolean" ? { blocked: env.blocked } : {},
498
533
  ...env?.connection_location !== null && typeof env?.connection_location === "object" ? { connectionLocation: env.connection_location } : {},
499
534
  ...Array.isArray(env?.errors) ? { errors: toErrorDetails(env.errors) } : {},
535
+ ...env?.code === "SEQUENCE_CONFLICT" && env.current !== null && typeof env.current === "object" && !Array.isArray(env.current) ? { current: env.current } : {},
500
536
  // `TIER_REQUIRED` details. `required_tier` / `current_tier` are null-BEARING
501
537
  // (no Tier has it / the team has none), so null survives; a non-string
502
538
  // value (neither a name nor null) is dropped rather than mistyped.
@@ -2886,7 +2922,7 @@ var TagsResource = class {
2886
2922
 
2887
2923
  // src/sequence/order.ts
2888
2924
  var DOC = ["$schema", "on_reply", "steps"];
2889
- var STEP = ["id", "do", "wait", "until", "if", "emit", "tag", "timeout", "variants", "with", "then", "else", "next", "note"];
2925
+ var STEP = ["id", "do", "wait", "until", "if", "emit", "tag", "timeout", "variants", "with", "event", "then", "else", "next", "note"];
2890
2926
  var WITH = ["post", "newer_than", "reaction", "subject", "text", "note", "note_fallback", "fallback", "withdraw_after"];
2891
2927
  var VARIANT = ["id", "subject", "text"];
2892
2928
  var FALLBACK = ["subject", "text"];
@@ -2921,7 +2957,7 @@ var schema_v1_default = {
2921
2957
  $schema: "https://json-schema.org/draft/2020-12/schema",
2922
2958
  $id: "https://docs.curviate.com/schemas/sequence/v1.json",
2923
2959
  title: "Sequence document v1",
2924
- description: `A Motion's Sequence: a flat steps array run from the first step, ending explicitly in "end". Served documents use this key order: $schema, on_reply, steps; in a step id, the kind key, timeout, variants, with, then, else, next, note; in with post, newer_than, reaction, subject, text, note, note_fallback, fallback, withdraw_after; in a variant id, subject, text. A text holds at most 50 variables.`,
2960
+ description: 'A Motion\'s Sequence: a flat steps array run from the first step, ending explicitly in "end". Served documents use this key order: $schema, on_reply, steps; in a step id, the kind key, timeout, variants, with, event, then, else, next, note; in with post, newer_than, reaction, subject, text, note, note_fallback, fallback, withdraw_after; in a variant id, subject, text. A text holds at most 50 variables. Variables: `{{first_name}}` (any standard Contact field), `{{my_first_name}}`, `{{my_last_name}}`, `{{my_full_name}}` (the sending Account), any other `{{<key>}}` is the custom field `contact.extra.<key>`; the long forms `{{contact.<field>}}`, `{{contact.extra.<key>}}` and `{{account.<name>}}` stay valid. Delays are about their duration: every wait and until timeout gets a small random spread that only ever adds time.',
2925
2961
  type: "object",
2926
2962
  additionalProperties: false,
2927
2963
  required: [
@@ -2966,6 +3002,11 @@ var schema_v1_default = {
2966
3002
  additionalProperties: false,
2967
3003
  properties: {}
2968
3004
  },
3005
+ event: {
3006
+ type: "string",
3007
+ pattern: "^[a-z][a-z0-9_.]{0,63}$",
3008
+ description: "Custom event sent once each time this step runs (not when skipped or failed), for example invite_sent."
3009
+ },
2969
3010
  next: {
2970
3011
  type: "string",
2971
3012
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
@@ -3005,6 +3046,11 @@ var schema_v1_default = {
3005
3046
  additionalProperties: false,
3006
3047
  properties: {}
3007
3048
  },
3049
+ event: {
3050
+ type: "string",
3051
+ pattern: "^[a-z][a-z0-9_.]{0,63}$",
3052
+ description: "Custom event sent once each time this step runs (not when skipped or failed), for example invite_sent."
3053
+ },
3008
3054
  next: {
3009
3055
  type: "string",
3010
3056
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
@@ -3044,6 +3090,11 @@ var schema_v1_default = {
3044
3090
  additionalProperties: false,
3045
3091
  properties: {}
3046
3092
  },
3093
+ event: {
3094
+ type: "string",
3095
+ pattern: "^[a-z][a-z0-9_.]{0,63}$",
3096
+ description: "Custom event sent once each time this step runs (not when skipped or failed), for example invite_sent."
3097
+ },
3047
3098
  next: {
3048
3099
  type: "string",
3049
3100
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
@@ -3106,7 +3157,7 @@ var schema_v1_default = {
3106
3157
  type: "array",
3107
3158
  minItems: 2,
3108
3159
  maxItems: 5,
3109
- description: "A/B variants, split evenly and sticky per Enrollment. Exclusive with the text in with.",
3160
+ description: "A/B variants: exactly 2, split evenly and sticky per Enrollment. Exclusive with the text in with. More than 2 (up to 5) is deprecated: accepted with warning W13 until 2027-01-19.",
3110
3161
  items: {
3111
3162
  type: "object",
3112
3163
  additionalProperties: false,
@@ -3127,6 +3178,11 @@ var schema_v1_default = {
3127
3178
  }
3128
3179
  }
3129
3180
  },
3181
+ event: {
3182
+ type: "string",
3183
+ pattern: "^[a-z][a-z0-9_.]{0,63}$",
3184
+ description: "Custom event sent once each time this step runs (not when skipped or failed), for example invite_sent."
3185
+ },
3130
3186
  next: {
3131
3187
  type: "string",
3132
3188
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
@@ -3166,6 +3222,11 @@ var schema_v1_default = {
3166
3222
  additionalProperties: false,
3167
3223
  properties: {}
3168
3224
  },
3225
+ event: {
3226
+ type: "string",
3227
+ pattern: "^[a-z][a-z0-9_.]{0,63}$",
3228
+ description: "Custom event sent once each time this step runs (not when skipped or failed), for example invite_sent."
3229
+ },
3169
3230
  next: {
3170
3231
  type: "string",
3171
3232
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
@@ -3220,7 +3281,7 @@ var schema_v1_default = {
3220
3281
  type: "array",
3221
3282
  minItems: 2,
3222
3283
  maxItems: 5,
3223
- description: "A/B variants, split evenly and sticky per Enrollment. Exclusive with the text in with.",
3284
+ description: "A/B variants: exactly 2, split evenly and sticky per Enrollment. Exclusive with the text in with. More than 2 (up to 5) is deprecated: accepted with warning W13 until 2027-01-19.",
3224
3285
  items: {
3225
3286
  type: "object",
3226
3287
  additionalProperties: false,
@@ -3241,6 +3302,11 @@ var schema_v1_default = {
3241
3302
  }
3242
3303
  }
3243
3304
  },
3305
+ event: {
3306
+ type: "string",
3307
+ pattern: "^[a-z][a-z0-9_.]{0,63}$",
3308
+ description: "Custom event sent once each time this step runs (not when skipped or failed), for example invite_sent."
3309
+ },
3244
3310
  next: {
3245
3311
  type: "string",
3246
3312
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
@@ -3314,7 +3380,7 @@ var schema_v1_default = {
3314
3380
  type: "array",
3315
3381
  minItems: 2,
3316
3382
  maxItems: 5,
3317
- description: "A/B variants, split evenly and sticky per Enrollment. Exclusive with the text in with.",
3383
+ description: "A/B variants: exactly 2, split evenly and sticky per Enrollment. Exclusive with the text in with. More than 2 (up to 5) is deprecated: accepted with warning W13 until 2027-01-19.",
3318
3384
  items: {
3319
3385
  type: "object",
3320
3386
  additionalProperties: false,
@@ -3340,6 +3406,11 @@ var schema_v1_default = {
3340
3406
  }
3341
3407
  }
3342
3408
  },
3409
+ event: {
3410
+ type: "string",
3411
+ pattern: "^[a-z][a-z0-9_.]{0,63}$",
3412
+ description: "Custom event sent once each time this step runs (not when skipped or failed), for example invite_sent."
3413
+ },
3343
3414
  next: {
3344
3415
  type: "string",
3345
3416
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
@@ -3404,6 +3475,11 @@ var schema_v1_default = {
3404
3475
  }
3405
3476
  }
3406
3477
  },
3478
+ event: {
3479
+ type: "string",
3480
+ pattern: "^[a-z][a-z0-9_.]{0,63}$",
3481
+ description: "Custom event sent once each time this step runs (not when skipped or failed), for example invite_sent."
3482
+ },
3407
3483
  next: {
3408
3484
  type: "string",
3409
3485
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
@@ -3470,7 +3546,7 @@ var schema_v1_default = {
3470
3546
  type: "array",
3471
3547
  minItems: 2,
3472
3548
  maxItems: 5,
3473
- description: "A/B variants, split evenly and sticky per Enrollment. Exclusive with the text in with.",
3549
+ description: "A/B variants: exactly 2, split evenly and sticky per Enrollment. Exclusive with the text in with. More than 2 (up to 5) is deprecated: accepted with warning W13 until 2027-01-19.",
3474
3550
  items: {
3475
3551
  type: "object",
3476
3552
  additionalProperties: false,
@@ -3491,6 +3567,11 @@ var schema_v1_default = {
3491
3567
  }
3492
3568
  }
3493
3569
  },
3570
+ event: {
3571
+ type: "string",
3572
+ pattern: "^[a-z][a-z0-9_.]{0,63}$",
3573
+ description: "Custom event sent once each time this step runs (not when skipped or failed), for example invite_sent."
3574
+ },
3494
3575
  next: {
3495
3576
  type: "string",
3496
3577
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
@@ -3530,6 +3611,11 @@ var schema_v1_default = {
3530
3611
  additionalProperties: false,
3531
3612
  properties: {}
3532
3613
  },
3614
+ event: {
3615
+ type: "string",
3616
+ pattern: "^[a-z][a-z0-9_.]{0,63}$",
3617
+ description: "Custom event sent once each time this step runs (not when skipped or failed), for example invite_sent."
3618
+ },
3533
3619
  next: {
3534
3620
  type: "string",
3535
3621
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
@@ -3563,7 +3649,7 @@ var schema_v1_default = {
3563
3649
  wait: {
3564
3650
  type: "string",
3565
3651
  pattern: "^[1-9][0-9]*(m|h|d)$",
3566
- description: "Calendar time to wait (1m to 365d)."
3652
+ description: "Calendar time to wait, about this long (1m to 365d; a small random spread is added, never subtracted)."
3567
3653
  },
3568
3654
  next: {
3569
3655
  type: "string",
@@ -3604,15 +3690,15 @@ var schema_v1_default = {
3604
3690
  },
3605
3691
  {
3606
3692
  type: "string",
3607
- pattern: "^signal\\.[a-z][a-z0-9_]{0,39}$"
3693
+ pattern: "^(event|signal)\\.[a-z][a-z0-9_.]{0,63}$"
3608
3694
  }
3609
3695
  ],
3610
- description: "`invite.accepted`, `message.replied`, `message.read`, or `signal.<name>`."
3696
+ description: "`invite.accepted`, `message.replied`, `message.read`, or `event.<name>` (a custom event an agent sends). `signal.<name>` is the deprecated spelling of `event.<name>`: accepted with warning W13 until 2027-01-19."
3611
3697
  },
3612
3698
  timeout: {
3613
3699
  type: "string",
3614
3700
  pattern: "^[1-9][0-9]*(m|h|d)$",
3615
- description: "Give up waiting after this long (1h to 180d)."
3701
+ description: "Give up waiting after about this long (1h to 180d; a small random spread is added, never subtracted)."
3616
3702
  },
3617
3703
  then: {
3618
3704
  type: "string",
@@ -4257,6 +4343,19 @@ var schema_v1_default = {
4257
4343
  }
4258
4344
  }
4259
4345
  },
4346
+ {
4347
+ type: "object",
4348
+ additionalProperties: false,
4349
+ required: [
4350
+ "message.seen"
4351
+ ],
4352
+ properties: {
4353
+ "message.seen": {
4354
+ type: "boolean",
4355
+ description: "if only: the Enrollment's last message.send or inmail.send was seen (a read receipt, or a reply after it). False before any message."
4356
+ }
4357
+ }
4358
+ },
4260
4359
  {
4261
4360
  type: "object",
4262
4361
  additionalProperties: false,
@@ -4874,6 +4973,19 @@ var schema_v1_default = {
4874
4973
  type: "boolean"
4875
4974
  }
4876
4975
  }
4976
+ },
4977
+ {
4978
+ type: "object",
4979
+ additionalProperties: false,
4980
+ required: [
4981
+ "message.seen"
4982
+ ],
4983
+ properties: {
4984
+ "message.seen": {
4985
+ type: "boolean",
4986
+ description: "if only: the Enrollment's last message.send or inmail.send was seen (a read receipt, or a reply after it). False before any message."
4987
+ }
4988
+ }
4877
4989
  }
4878
4990
  ]
4879
4991
  }
@@ -4914,7 +5026,7 @@ var schema_v1_default = {
4914
5026
  emit: {
4915
5027
  type: "string",
4916
5028
  pattern: "^[a-z][a-z0-9_.]{0,63}$",
4917
- description: "Signal name for the motion.signal webhook."
5029
+ description: "Legacy: a custom event name. New documents put event on the step that matters."
4918
5030
  },
4919
5031
  next: {
4920
5032
  type: "string",
@@ -4948,6 +5060,11 @@ var schema_v1_default = {
4948
5060
  pattern: "^[^,;]*[^,;\\s][^,;]*$",
4949
5061
  description: "Tag added to the Contact."
4950
5062
  },
5063
+ event: {
5064
+ type: "string",
5065
+ pattern: "^[a-z][a-z0-9_.]{0,63}$",
5066
+ description: "Custom event sent once each time this step runs (not when skipped or failed), for example invite_sent."
5067
+ },
4951
5068
  next: {
4952
5069
  type: "string",
4953
5070
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
@@ -4982,10 +5099,10 @@ var END = "end";
4982
5099
  var H = 60;
4983
5100
  var D = 24 * H;
4984
5101
  var DURATION_BOUNDS = {
4985
- wait: { min: 1, max: 365 * D, label: "1m and 365d" },
4986
- timeout: { min: 1 * H, max: 180 * D, label: "1h and 180d" },
4987
- withdraw_after: { min: 14 * D, max: 180 * D, label: "14d and 180d" },
4988
- newer_than: { min: 1 * H, max: 365 * D, label: "1h and 365d" }
5102
+ wait: { min: 1, max: 365 * D, label: "1m and 365d", example: "1d" },
5103
+ timeout: { min: 1 * H, max: 180 * D, label: "1h and 180d", example: "21d" },
5104
+ withdraw_after: { min: 14 * D, max: 180 * D, label: "14d and 180d", example: "21d" },
5105
+ newer_than: { min: 1 * H, max: 365 * D, label: "1h and 365d", example: "30d" }
4989
5106
  };
4990
5107
  var TEXT_LIMITS = {
4991
5108
  message: 8e3,
@@ -5001,6 +5118,53 @@ var MAX_QUOTED_TOKEN_CHARS = 64;
5001
5118
  var ACCOUNT_FIELDS = ["first_name", "last_name", "full_name"];
5002
5119
  var NAME_FIELDS = ["full_name", "first_name", "last_name"];
5003
5120
  var MOTION_KEY_PATTERN = "^[a-z][a-z0-9_]{0,39}$";
5121
+ var MAX_VARIANTS = 2;
5122
+ var DEPRECATED_ACCEPTED_UNTIL = "2027-01-19";
5123
+ var SHORT_VARIABLE_PATTERN = "^[a-z][a-z0-9_]{0,63}$";
5124
+ var MY_FIELDS = ["my_first_name", "my_last_name", "my_full_name"];
5125
+
5126
+ // src/sequence/rules.ts
5127
+ var SEQUENCE_RULES_DOCS_URL = "https://docs.curviate.com/reference/sequence-rules";
5128
+ var HINTS = {
5129
+ R1: () => "Keep only $schema, on_reply and steps at the top level; $schema, when present, is https://docs.curviate.com/schemas/sequence/v1.json.",
5130
+ R2: () => "Keep the document to 1 to 100 steps and 256 KiB; split a longer flow into two Motions.",
5131
+ R3: () => "Give each step a unique id: lowercase letters, digits and _, starting with a letter, at most 40 characters, not end.",
5132
+ R4: () => "Give the step exactly one of do, wait, until, if, emit or tag.",
5133
+ R5: () => "Remove the key: each step kind takes only its own keys, and the sending Account is chosen with settings.assign.",
5134
+ R6: () => 'Point the exit at a step id of this document, or at "end".',
5135
+ R7: () => "Add then: the step to go to when the event happens or the condition is true.",
5136
+ R8: () => 'Break the loop: point one exit of the cycle at a later step or at "end".',
5137
+ R9: () => "Lead an exit of another step here, or delete the step.",
5138
+ R10: () => 'End the last step explicitly: set its exits to a step id or "end".',
5139
+ R11: () => 'Set on_reply to a step id or "end".',
5140
+ R12: (t) => `Use a duration between ${t}.`,
5141
+ R13: () => "Use an action from the list and only the parameters it takes, with values of their type.",
5142
+ R14: () => "Wait for invite.accepted, message.replied, message.read or event.<name>, and give a timeout.",
5143
+ R15: () => "Use one predicate key per object with its typed operand; chain if steps instead of all or any. account.* and message.* work in if steps only.",
5144
+ R16: () => "Write a variable as {{first_name}}, {{my_first_name}} or a custom field {{opener}}; the long forms {{contact.first_name}}, {{contact.extra.opener}} and {{account.first_name}} work too.",
5145
+ R17: () => "Remove the part after | and add a variable-free fallback on the step instead.",
5146
+ R18: () => "Write a variable with double braces, like {{first_name}}, or remove the braces.",
5147
+ R19: () => 'Add a variable-free fallback: with.fallback, with.note_fallback (or "drop") on an invitation, "skip" on a comment.',
5148
+ R20: () => "Use a Contact variable in the comment, for example a custom field {{comment}} your agent writes per Contact.",
5149
+ R21: () => "Set withdraw_after between 14d and 180d, for example 21d.",
5150
+ R22: () => "Shorten the text to the limit (variables are not counted) or use fewer variables.",
5151
+ R23: () => "Give the step exactly 2 variants (A/B) with ids unique in the document, or one text in with.",
5152
+ R24: () => "Name the event with lowercase letters, digits, _ and ., starting with a letter, like reply.received; a Tag is 1 to 64 characters without , or ;.",
5153
+ W1: () => "If the merge is intended, keep it; otherwise give the jumping step its own continuation.",
5154
+ W5: () => "Remove the follow step: the invitation already follows the person.",
5155
+ W6: () => "Send an invitation on this path before waiting for its acceptance.",
5156
+ W7: () => "Write the comment per Contact, for example in a custom field {{comment}}.",
5157
+ W8: () => "Keep the link for a later message.",
5158
+ W9: () => "Motion variables are deprecated: write the text in place, or use a custom field.",
5159
+ W11: (t) => `Did you mean ${t}?`,
5160
+ W12: () => "Send a message on this path before checking whether it was seen.",
5161
+ W13: (t) => `Use ${t} instead.`
5162
+ };
5163
+ function ruleHelp(code, token = "") {
5164
+ const hint = HINTS[code];
5165
+ if (!hint) throw new Error(`no hint for ${code}`);
5166
+ return { hint: hint(token), docs_url: `${SEQUENCE_RULES_DOCS_URL}#${code.toLowerCase()}` };
5167
+ }
5004
5168
 
5005
5169
  // src/sequence/vocab.ts
5006
5170
  var SUPPORTED_KEYWORDS = /* @__PURE__ */ new Set([
@@ -5143,7 +5307,7 @@ function vocabulary(schema) {
5143
5307
  maxNoteChars: num(obj(propsOf(alts[0])["note"], "note")["maxLength"], "note.maxLength"),
5144
5308
  durationPattern: str(obj(byKind("wait")["wait"], "wait")["pattern"], "wait.pattern"),
5145
5309
  events: arr(until[0]["enum"], "until.enum").map((e) => str(e, "until.enum[]")),
5146
- signalNamePattern: after(str(until[1]["pattern"], "until.pattern"), "^signal\\."),
5310
+ eventNamePattern: after(str(until[1]["pattern"], "until.pattern"), "^(event|signal)\\."),
5147
5311
  emitPattern: str(obj(byKind("emit")["emit"], "emit")["pattern"], "emit.pattern"),
5148
5312
  tagPattern: str(tag["pattern"], "tag.pattern"),
5149
5313
  maxTagChars: num(tag["maxLength"], "tag.maxLength"),
@@ -5159,6 +5323,18 @@ function vocabulary(schema) {
5159
5323
  }
5160
5324
 
5161
5325
  // src/sequence/validate.ts
5326
+ var issue = (ctx, i) => ctx.issues.push(i);
5327
+ var warning = (ctx, i) => ctx.warnings.push(i);
5328
+ function jsonPointer(...tokens) {
5329
+ return tokens.map((t) => "/" + String(t).replace(/~/g, "~0").replace(/\//g, "~1")).join("");
5330
+ }
5331
+ function entry(code, path, pointer, rule, token) {
5332
+ return { path, rule, code, message: rule, pointer, ...ruleHelp(code, token) };
5333
+ }
5334
+ function entries(issues) {
5335
+ return issues.sort((a, b) => a.order - b.order).map((i) => entry(i.code, i.path, i.pointer, i.rule, i.token));
5336
+ }
5337
+ var UNTIL = `; accepted until ${DEPRECATED_ACCEPTED_UNTIL}`;
5162
5338
  var VARIABLE_RE = /\{\{([^{}]*)\}\}/g;
5163
5339
  var SINGLE_BRACE_RE = /\{[A-Za-z_][A-Za-z0-9_.]*\}/g;
5164
5340
  function keysOf(o) {
@@ -5198,6 +5374,7 @@ function durationMinutes(v) {
5198
5374
  function durationRule(bounds) {
5199
5375
  return `must be a duration between ${bounds.label}: a number then m, h or d, like 30d`;
5200
5376
  }
5377
+ var durationHint = (bounds) => `${bounds.label}, for example ${bounds.example}`;
5201
5378
  function durationOk(ctx, v, bounds) {
5202
5379
  if (typeof v !== "string" || !ctx.re.duration.test(v)) return false;
5203
5380
  const m = durationMinutes(v);
@@ -5215,75 +5392,123 @@ function hasVariable(text) {
5215
5392
  function staticLength(text) {
5216
5393
  return [...text.replace(VARIABLE_RE, "")].length;
5217
5394
  }
5395
+ function longVariable(ctx, inner) {
5396
+ if (inner.includes(".")) return inner;
5397
+ if (!ctx.re.short.test(inner)) return null;
5398
+ if (MY_FIELDS.includes(inner)) return `account.${inner.slice(3)}`;
5399
+ if (inner.startsWith("my_")) return null;
5400
+ if (ctx.v.standardFields.includes(inner)) return `contact.${inner}`;
5401
+ return `contact.extra.${inner}`;
5402
+ }
5218
5403
  function isKnownVariable(ctx, inner) {
5219
- if (inner.startsWith("contact.extra.")) return ctx.re.extraKey.test(inner.slice("contact.extra.".length));
5220
- if (inner.startsWith("contact.")) return ctx.v.standardFields.includes(inner.slice(8));
5221
- if (inner.startsWith("account.")) return ACCOUNT_FIELDS.includes(inner.slice(8));
5404
+ const long = longVariable(ctx, inner);
5405
+ if (long === null) return false;
5406
+ if (long.startsWith("contact.extra.")) return ctx.re.extraKey.test(long.slice("contact.extra.".length));
5407
+ if (long.startsWith("contact.")) return ctx.v.standardFields.includes(long.slice(8));
5408
+ if (long.startsWith("account.")) return ACCOUNT_FIELDS.includes(long.slice(8));
5222
5409
  return false;
5223
5410
  }
5224
- function checkText(ctx, order, path, value, limit) {
5411
+ function distance(a, b, cap) {
5412
+ if (Math.abs(a.length - b.length) > cap) return cap + 1;
5413
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
5414
+ for (let i = 1; i <= a.length; i++) {
5415
+ const cur = [i];
5416
+ for (let j = 1; j <= b.length; j++) cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
5417
+ prev = cur;
5418
+ }
5419
+ return prev[b.length];
5420
+ }
5421
+ function likelyTypoOf(ctx, key) {
5422
+ let best = null;
5423
+ let bestD = 3;
5424
+ for (const n of [...ctx.v.standardFields, ...MY_FIELDS]) {
5425
+ const d = distance(key, n, 2);
5426
+ if (d < bestD) [best, bestD] = [n, d];
5427
+ }
5428
+ return best;
5429
+ }
5430
+ function checkText(ctx, order, path, pointer, parent, value, limit) {
5225
5431
  if (typeof value !== "string" || value.trim() === "") {
5226
- ctx.issues.push({ order, path, rule: "must be non-empty text" });
5432
+ issue(ctx, { order, code: "R13", path, pointer: value === void 0 ? parent : pointer, rule: "must be non-empty text" });
5227
5433
  return null;
5228
5434
  }
5435
+ const at = { order, path, pointer };
5229
5436
  const vars = variablesIn(value);
5230
5437
  if (vars.length > MAX_VARIABLE_REFS_PER_TEXT) {
5231
- ctx.issues.push({ order, path, rule: `${vars.length} variables; the limit is ${MAX_VARIABLE_REFS_PER_TEXT} per text` });
5438
+ issue(ctx, { ...at, code: "R22", rule: `${vars.length} variables; the limit is ${MAX_VARIABLE_REFS_PER_TEXT} per text` });
5232
5439
  }
5233
5440
  for (const v of vars.length > MAX_VARIABLE_REFS_PER_TEXT ? [] : vars) {
5234
5441
  if (v.inner.includes("|")) {
5235
- ctx.issues.push({ order, path, rule: "variable defaults are not supported; add a fallback" });
5442
+ issue(ctx, { ...at, code: "R17", rule: "variable defaults are not supported; add a fallback" });
5236
5443
  continue;
5237
5444
  }
5238
5445
  if (v.inner.startsWith("motion.") && ctx.re.motion.test(v.inner.slice(7))) {
5239
- ctx.warnings.push({ order, code: "W9", path, rule: `${quote(v.token)} is checked against settings.variables by the server only` });
5446
+ const key = v.inner.slice(7);
5447
+ warning(ctx, {
5448
+ ...at,
5449
+ code: "W13",
5450
+ rule: `${quote(v.token)}: Motion variables were removed${UNTIL}; write the text in place or use a custom field`,
5451
+ token: `text in place or a custom field such as {{${key}}}`
5452
+ });
5453
+ warning(ctx, { ...at, code: "W9", rule: `${quote(v.token)} is checked against settings.variables by the server only` });
5454
+ continue;
5455
+ }
5456
+ if (!v.inner.includes(".") && v.inner.startsWith("my_") && !MY_FIELDS.includes(v.inner)) {
5457
+ issue(ctx, { ...at, code: "R16", rule: `${quote(v.token)}: the my_ prefix is reserved for my_first_name, my_last_name and my_full_name` });
5240
5458
  continue;
5241
5459
  }
5242
5460
  if (!isKnownVariable(ctx, v.inner)) {
5243
- ctx.issues.push({
5244
- order,
5245
- path,
5246
- rule: `unknown variable ${quote(v.token)}; use contact.<field>, contact.extra.<key>, account.first_name, account.last_name, account.full_name or motion.<key>`
5461
+ issue(ctx, {
5462
+ ...at,
5463
+ code: "R16",
5464
+ rule: `unknown variable ${quote(v.token)}; use a field ({{first_name}}), your name ({{my_first_name}}), a custom field ({{opener}}), or contact.<field>, contact.extra.<key>, account.first_name, account.last_name, account.full_name`
5247
5465
  });
5466
+ continue;
5467
+ }
5468
+ if (!v.inner.includes(".") && longVariable(ctx, v.inner).startsWith("contact.extra.")) {
5469
+ const near = likelyTypoOf(ctx, v.inner);
5470
+ if (near !== null) warning(ctx, { ...at, code: "W11", rule: `${quote(v.token)} is read as a custom field; did you mean ${near}?`, token: near });
5248
5471
  }
5249
5472
  }
5250
5473
  const rest = value.replace(VARIABLE_RE, " ");
5251
5474
  if (rest.includes("{{")) {
5252
- ctx.issues.push({ order, path, rule: "unclosed variable; write a variable as {{contact.first_name}}" });
5475
+ issue(ctx, { ...at, code: "R16", rule: "unclosed variable; write a variable as {{contact.first_name}}" });
5253
5476
  } else {
5254
5477
  for (const m of rest.matchAll(SINGLE_BRACE_RE)) {
5255
- ctx.issues.push({ order, path, rule: `${quote(m[0])} is not a variable; write {{contact.<field>}} or remove the braces` });
5478
+ issue(ctx, { ...at, code: "R18", rule: `${quote(m[0])} is not a variable; write {{contact.<field>}} or remove the braces` });
5256
5479
  }
5257
5480
  }
5258
5481
  const len = staticLength(value);
5259
- if (len > limit) ctx.issues.push({ order, path, rule: `${len} characters; the limit is ${limit}, variables not counted` });
5482
+ if (len > limit) issue(ctx, { ...at, code: "R22", rule: `${len} characters; the limit is ${limit}, variables not counted` });
5260
5483
  return value;
5261
5484
  }
5262
- function checkFallbackText(ctx, order, path, value, limit, keyword) {
5485
+ function checkFallbackText(ctx, order, path, pointer, parent, value, limit, keyword) {
5263
5486
  if (keyword !== void 0 && value === keyword) return;
5487
+ const at = { order, code: "R19", path, pointer };
5264
5488
  if (typeof value !== "string") {
5265
- ctx.issues.push({ order, path, rule: keyword ? `must be text or "${keyword}"` : "must be text" });
5489
+ issue(ctx, { ...at, pointer: value === void 0 ? parent : pointer, rule: keyword ? `must be text or "${keyword}"` : "must be text" });
5266
5490
  return;
5267
5491
  }
5268
5492
  if (value.trim() === "") {
5269
- ctx.issues.push({ order, path, rule: "a fallback must not be empty" });
5493
+ issue(ctx, { ...at, rule: "a fallback must not be empty" });
5270
5494
  return;
5271
5495
  }
5272
5496
  if (hasVariable(value)) {
5273
- ctx.issues.push({ order, path, rule: "a fallback must not contain a variable" });
5497
+ issue(ctx, { ...at, rule: "a fallback must not contain a variable" });
5274
5498
  return;
5275
5499
  }
5276
- checkText(ctx, order, path, value, limit);
5500
+ checkText(ctx, order, path, pointer, parent, value, limit);
5277
5501
  }
5278
5502
  function createValidator(v) {
5279
5503
  const re = {
5280
5504
  id: new RegExp(v.idPattern),
5281
5505
  duration: new RegExp(v.durationPattern),
5282
5506
  emit: new RegExp(v.emitPattern),
5283
- signal: new RegExp(v.signalNamePattern),
5507
+ event: new RegExp(v.eventNamePattern),
5284
5508
  extraKey: new RegExp(v.extraKeyPattern),
5285
5509
  tag: new RegExp(v.tagPattern),
5286
- motion: new RegExp(MOTION_KEY_PATTERN)
5510
+ motion: new RegExp(MOTION_KEY_PATTERN),
5511
+ short: new RegExp(SHORT_VARIABLE_PATTERN)
5287
5512
  };
5288
5513
  return (doc) => run(doc, { v, re, issues: [], warnings: [] });
5289
5514
  }
@@ -5310,9 +5535,9 @@ function firstUnstorable(doc) {
5310
5535
  } else if (Array.isArray(value)) {
5311
5536
  for (let i = value.length - 1; i >= 0; i--) stack.push(["value", value[i], `${path}[${i}]`]);
5312
5537
  } else if (typeof value === "object" && value !== null) {
5313
- const entries = Object.entries(value);
5314
- for (let i = entries.length - 1; i >= 0; i--) {
5315
- const [k, v] = entries[i];
5538
+ const entries2 = Object.entries(value);
5539
+ for (let i = entries2.length - 1; i >= 0; i--) {
5540
+ const [k, v] = entries2[i];
5316
5541
  stack.push(["value", v, path ? `${path}.${k}` : k], ["key", k, path]);
5317
5542
  }
5318
5543
  }
@@ -5324,53 +5549,55 @@ function run(doc, ctx) {
5324
5549
  if (bad !== null) return { errors: [{ path: bad, rule: UNSTORABLE_RULE }], warnings: [] };
5325
5550
  const size = compactBytes(doc);
5326
5551
  if (size > MAX_SEQUENCE_BYTES) {
5327
- return { errors: [{ path: "$", rule: `the document is ${size} bytes; the limit is ${MAX_SEQUENCE_BYTES}, UTF-8 compact JSON` }], warnings: [] };
5552
+ return { errors: [entry("R2", "$", "", `the document is ${size} bytes; the limit is ${MAX_SEQUENCE_BYTES}, UTF-8 compact JSON`)], warnings: [] };
5328
5553
  }
5329
5554
  return rules(doc, ctx);
5330
5555
  }
5331
5556
  function rules(doc, ctx) {
5332
5557
  const { v } = ctx;
5333
5558
  const DOC2 = -1;
5334
- if (!isObject(doc)) return { errors: [{ path: "$", rule: "a Sequence document is a JSON object" }], warnings: [] };
5559
+ if (!isObject(doc)) return { errors: [entry("R1", "$", "", "a Sequence document is a JSON object")], warnings: [] };
5335
5560
  for (const k of keysOf(doc)) {
5336
5561
  if (k !== "$schema" && k !== "on_reply" && k !== "steps") {
5337
- ctx.issues.push({ order: DOC2, path: quote(k), rule: "unknown key; a Sequence document holds only $schema, on_reply and steps" });
5562
+ issue(ctx, { order: DOC2, code: "R1", path: quote(k), pointer: jsonPointer(k), rule: "unknown key; a Sequence document holds only $schema, on_reply and steps" });
5338
5563
  }
5339
5564
  }
5340
5565
  if (has(doc, "$schema") && doc["$schema"] !== v.schemaUrl) {
5341
- ctx.issues.push({ order: DOC2, path: "$schema", rule: `must be ${v.schemaUrl}` });
5566
+ issue(ctx, { order: DOC2, code: "R1", path: "$schema", pointer: "/$schema", rule: `must be ${v.schemaUrl}` });
5342
5567
  }
5343
5568
  const rawSteps = doc["steps"];
5344
5569
  const stepsArr = Array.isArray(rawSteps) ? rawSteps : [];
5345
5570
  if (!Array.isArray(rawSteps) || rawSteps.length < v.minSteps) {
5346
- ctx.issues.push({ order: DOC2, path: "steps", rule: `required: an array of ${v.minSteps} to ${v.maxSteps} steps` });
5571
+ issue(ctx, { order: DOC2, code: "R2", path: "steps", pointer: has(doc, "steps") ? "/steps" : "", rule: `required: an array of ${v.minSteps} to ${v.maxSteps} steps` });
5347
5572
  } else if (rawSteps.length > v.maxSteps) {
5348
- ctx.issues.push({ order: DOC2, path: "steps", rule: `${rawSteps.length} steps; the limit is ${v.maxSteps}` });
5573
+ issue(ctx, { order: DOC2, code: "R2", path: "steps", pointer: "/steps", rule: `${rawSteps.length} steps; the limit is ${v.maxSteps}` });
5349
5574
  }
5350
5575
  const infos = [];
5351
5576
  const firstIndex = /* @__PURE__ */ new Map();
5352
5577
  let structural = !Array.isArray(rawSteps) || rawSteps.length < v.minSteps || rawSteps.length > v.maxSteps;
5353
5578
  stepsArr.forEach((raw, index) => {
5354
- const info = { index, prefix: `steps[${index}]`, id: null, kind: null, raw: null };
5579
+ const ptr = jsonPointer("steps", index);
5580
+ const info = { index, prefix: `steps[${index}]`, ptr, id: null, kind: null, raw: null };
5355
5581
  infos.push(info);
5356
5582
  if (!isObject(raw)) {
5357
- ctx.issues.push({ order: index, path: info.prefix, rule: "a step is a JSON object" });
5583
+ issue(ctx, { order: index, code: "R4", path: info.prefix, pointer: ptr, rule: "a step is a JSON object" });
5358
5584
  structural = true;
5359
5585
  return;
5360
5586
  }
5361
5587
  info.raw = raw;
5362
5588
  const id = raw["id"];
5589
+ const idAt = { order: index, code: "R3", path: `${info.prefix}.id`, pointer: `${ptr}/id` };
5363
5590
  if (!has(raw, "id")) {
5364
- ctx.issues.push({ order: index, path: `${info.prefix}.id`, rule: `required: matching ${v.idPattern}` });
5591
+ issue(ctx, { ...idAt, pointer: ptr, rule: `required: matching ${v.idPattern}` });
5365
5592
  structural = true;
5366
5593
  } else if (typeof id !== "string" || !ctx.re.id.test(id)) {
5367
- ctx.issues.push({ order: index, path: `${info.prefix}.id`, rule: `must match ${v.idPattern}` });
5594
+ issue(ctx, { ...idAt, rule: `must match ${v.idPattern}` });
5368
5595
  structural = true;
5369
5596
  } else if (id === END) {
5370
- ctx.issues.push({ order: index, path: `${info.prefix}.id`, rule: '"end" is reserved' });
5597
+ issue(ctx, { ...idAt, rule: '"end" is reserved' });
5371
5598
  structural = true;
5372
5599
  } else if (firstIndex.has(id)) {
5373
- ctx.issues.push({ order: index, path: `${info.prefix}.id`, rule: `duplicate step id ${id}` });
5600
+ issue(ctx, { ...idAt, rule: `duplicate step id ${id}` });
5374
5601
  structural = true;
5375
5602
  } else {
5376
5603
  firstIndex.set(id, index);
@@ -5379,9 +5606,11 @@ function rules(doc, ctx) {
5379
5606
  }
5380
5607
  const kinds = v.stepKinds.filter((k) => has(raw, k));
5381
5608
  if (kinds.length !== 1) {
5382
- ctx.issues.push({
5609
+ issue(ctx, {
5383
5610
  order: index,
5611
+ code: "R4",
5384
5612
  path: info.prefix,
5613
+ pointer: ptr,
5385
5614
  rule: kinds.length === 0 ? `a step needs exactly one of ${v.stepKinds.join(", ")}` : `a step has exactly one kind; found ${kinds.join(", ")}`
5386
5615
  });
5387
5616
  structural = true;
@@ -5391,93 +5620,115 @@ function rules(doc, ctx) {
5391
5620
  });
5392
5621
  const onReply = doc["on_reply"];
5393
5622
  const targetOk = (t) => t === END || typeof t === "string" && firstIndex.has(t);
5623
+ const replyAt = { order: DOC2, code: "R11", path: "on_reply", pointer: "/on_reply" };
5394
5624
  if (!has(doc, "on_reply")) {
5395
- ctx.issues.push({ order: DOC2, path: "on_reply", rule: 'required: a step id or "end"' });
5625
+ issue(ctx, { ...replyAt, pointer: "", rule: 'required: a step id or "end"' });
5396
5626
  structural = true;
5397
5627
  } else if (typeof onReply !== "string") {
5398
- ctx.issues.push({ order: DOC2, path: "on_reply", rule: 'must be a step id or "end"' });
5628
+ issue(ctx, { ...replyAt, rule: 'must be a step id or "end"' });
5399
5629
  structural = true;
5400
5630
  } else if (!targetOk(onReply)) {
5401
- ctx.issues.push({ order: DOC2, path: "on_reply", rule: `unknown step ${quote(onReply)}` });
5631
+ issue(ctx, { ...replyAt, rule: `unknown step ${quote(onReply)}` });
5402
5632
  structural = true;
5403
5633
  }
5404
5634
  const usedIds = new Set(firstIndex.keys());
5405
5635
  for (const info of infos) {
5406
5636
  if (!info.raw || !info.kind) continue;
5407
- const { raw, kind, index: order, prefix: p } = info;
5637
+ const { raw, kind, index: order, prefix: p, ptr } = info;
5408
5638
  const keys = v.stepKeys[kind] ?? [];
5409
5639
  for (const k of keysOf(raw)) {
5410
5640
  if (keys.includes(k)) continue;
5411
- ctx.issues.push({
5641
+ issue(ctx, {
5412
5642
  order,
5643
+ code: "R5",
5413
5644
  path: `${p}.${quote(k)}`,
5645
+ pointer: `${ptr}${jsonPointer(k)}`,
5414
5646
  rule: k === "by" ? "step-level by is not supported; use settings.assign" : `unknown key for a ${kind} step`
5415
5647
  });
5416
5648
  }
5417
5649
  if (has(raw, "note")) {
5418
5650
  const n = raw["note"];
5419
5651
  if (typeof n !== "string" || [...n].length > v.maxNoteChars) {
5420
- ctx.issues.push({ order, path: `${p}.note`, rule: `must be text of at most ${v.maxNoteChars} characters` });
5652
+ issue(ctx, { order, code: "R22", path: `${p}.note`, pointer: `${ptr}/note`, rule: `must be text of at most ${v.maxNoteChars} characters` });
5653
+ }
5654
+ }
5655
+ if (has(raw, "event") && keys.includes("event")) {
5656
+ const e = raw["event"];
5657
+ if (typeof e !== "string" || !ctx.re.event.test(e)) {
5658
+ issue(ctx, { order, code: "R24", path: `${p}.event`, pointer: `${ptr}/event`, rule: `must match ${v.eventNamePattern}, like invite_sent` });
5421
5659
  }
5422
5660
  }
5423
5661
  switch (kind) {
5424
5662
  case "do":
5425
- checkDo(ctx, order, p, raw, usedIds);
5663
+ checkDo(ctx, order, p, ptr, raw, usedIds);
5426
5664
  break;
5427
5665
  case "wait":
5428
- if (!durationOk(ctx, raw["wait"], DURATION_BOUNDS.wait)) ctx.issues.push({ order, path: `${p}.wait`, rule: durationRule(DURATION_BOUNDS.wait) });
5666
+ if (!durationOk(ctx, raw["wait"], DURATION_BOUNDS.wait)) {
5667
+ issue(ctx, { order, code: "R12", path: `${p}.wait`, pointer: `${ptr}/wait`, rule: durationRule(DURATION_BOUNDS.wait), token: durationHint(DURATION_BOUNDS.wait) });
5668
+ }
5429
5669
  break;
5430
5670
  case "until":
5431
- checkUntil(ctx, order, p, raw);
5671
+ checkUntil(ctx, order, p, ptr, raw);
5432
5672
  break;
5433
5673
  case "if":
5434
- checkPredicate(ctx, raw["if"], `${p}.if`, order);
5674
+ checkPredicate(ctx, raw["if"], `${p}.if`, `${ptr}/if`, order);
5435
5675
  break;
5436
5676
  case "emit": {
5437
5677
  const e = raw["emit"];
5438
5678
  if (typeof e !== "string" || !ctx.re.emit.test(e)) {
5439
- ctx.issues.push({ order, path: `${p}.emit`, rule: `must match ${v.emitPattern}, like reply.received` });
5679
+ issue(ctx, { order, code: "R24", path: `${p}.emit`, pointer: `${ptr}/emit`, rule: `must match ${v.emitPattern}, like reply.received` });
5440
5680
  }
5441
5681
  break;
5442
5682
  }
5443
5683
  case "tag":
5444
- if (!tagOk(ctx, raw["tag"])) ctx.issues.push({ order, path: `${p}.tag`, rule: "a Tag name is 1 to 64 characters, without , or ;" });
5684
+ if (!tagOk(ctx, raw["tag"])) issue(ctx, { order, code: "R24", path: `${p}.tag`, pointer: `${ptr}/tag`, rule: "a Tag name is 1 to 64 characters, without , or ;" });
5445
5685
  break;
5446
5686
  }
5447
5687
  if ((kind === "until" || kind === "if") && !has(raw, "then")) {
5448
- ctx.issues.push({ order, path: `${p}.then`, rule: `required on an ${kind} step` });
5688
+ issue(ctx, { order, code: "R7", path: `${p}.then`, pointer: ptr, rule: `required on an ${kind} step` });
5449
5689
  structural = true;
5450
5690
  }
5451
5691
  for (const f of ["then", "else", "next"]) {
5452
5692
  if (!has(raw, f) || !keys.includes(f)) continue;
5453
5693
  const t = raw[f];
5694
+ const at = { order, code: "R6", path: `${p}.${f}`, pointer: `${ptr}/${f}` };
5454
5695
  if (typeof t !== "string") {
5455
- ctx.issues.push({ order, path: `${p}.${f}`, rule: 'must be a step id or "end"' });
5696
+ issue(ctx, { ...at, rule: 'must be a step id or "end"' });
5456
5697
  structural = true;
5457
5698
  } else if (!targetOk(t)) {
5458
- ctx.issues.push({ order, path: `${p}.${f}`, rule: `unknown step ${quote(t)}` });
5699
+ issue(ctx, { ...at, rule: `unknown step ${quote(t)}` });
5459
5700
  structural = true;
5460
5701
  }
5461
5702
  }
5462
5703
  }
5463
5704
  if (!structural) graph(ctx, infos, firstIndex, onReply);
5464
- const byOrder = (a, b) => a.order - b.order;
5465
- return {
5466
- errors: ctx.issues.sort(byOrder).map(({ path, rule }) => ({ path, rule })),
5467
- warnings: ctx.warnings.sort(byOrder).map(({ code, path, rule }) => ({ code, path, rule }))
5468
- };
5705
+ return { errors: entries(ctx.issues), warnings: entries(ctx.warnings) };
5469
5706
  }
5470
- function checkUntil(ctx, order, p, raw) {
5707
+ function checkUntil(ctx, order, p, ptr, raw) {
5471
5708
  const ev = raw["until"];
5472
- if (typeof ev === "string" && ev.startsWith("signal.")) {
5473
- if (!ctx.re.signal.test(ev.slice(7))) ctx.issues.push({ order, path: `${p}.until`, rule: `a signal name must match ${ctx.v.signalNamePattern}` });
5709
+ const at = { order, code: "R14", path: `${p}.until`, pointer: `${ptr}/until` };
5710
+ const custom = typeof ev === "string" ? /^(event|signal)\.(.*)$/s.exec(ev) : null;
5711
+ if (custom) {
5712
+ const [, ns, name] = custom;
5713
+ if (!ctx.re.event.test(name)) {
5714
+ issue(ctx, { ...at, rule: `an event name must match ${ctx.v.eventNamePattern}` });
5715
+ } else if (ns === "signal") {
5716
+ warning(ctx, { ...at, code: "W13", rule: `until signal.${quote(name)} is now until event.${quote(name)}${UNTIL}`, token: `event.${quote(name)}` });
5717
+ }
5474
5718
  } else if (!ctx.v.events.includes(ev)) {
5475
- ctx.issues.push({ order, path: `${p}.until`, rule: `unknown event; use ${ctx.v.events.join(", ")} or signal.<name>` });
5719
+ issue(ctx, { ...at, rule: `unknown event; use ${ctx.v.events.join(", ")} or event.<name>` });
5476
5720
  }
5477
5721
  if (!has(raw, "timeout")) {
5478
- ctx.issues.push({ order, path: `${p}.timeout`, rule: "required on an until step" });
5722
+ issue(ctx, { order, code: "R14", path: `${p}.timeout`, pointer: ptr, rule: "required on an until step" });
5479
5723
  } else if (!durationOk(ctx, raw["timeout"], DURATION_BOUNDS.timeout)) {
5480
- ctx.issues.push({ order, path: `${p}.timeout`, rule: durationRule(DURATION_BOUNDS.timeout) });
5724
+ issue(ctx, {
5725
+ order,
5726
+ code: "R12",
5727
+ path: `${p}.timeout`,
5728
+ pointer: `${ptr}/timeout`,
5729
+ rule: durationRule(DURATION_BOUNDS.timeout),
5730
+ token: durationHint(DURATION_BOUNDS.timeout)
5731
+ });
5481
5732
  }
5482
5733
  }
5483
5734
  function limitOf(a, field) {
@@ -5486,110 +5737,135 @@ function limitOf(a, field) {
5486
5737
  if (a === "invite.send") return TEXT_LIMITS.invite_note;
5487
5738
  return field === "subject" ? TEXT_LIMITS.inmail_subject : TEXT_LIMITS.inmail_text;
5488
5739
  }
5489
- function checkDo(ctx, order, p, raw, usedIds) {
5740
+ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5490
5741
  const { v } = ctx;
5491
5742
  const a = raw["do"];
5492
5743
  if (typeof a !== "string" || !v.actions.includes(a)) {
5493
- ctx.issues.push({ order, path: `${p}.do`, rule: `unknown action; use ${v.actions.join(", ")}` });
5744
+ issue(ctx, { order, code: "R13", path: `${p}.do`, pointer: `${ptr}/do`, rule: `unknown action; use ${v.actions.join(", ")}` });
5494
5745
  return;
5495
5746
  }
5496
5747
  const params = v.actionParams[a] ?? [];
5748
+ const wp = `${ptr}/with`;
5749
+ const withParent = isObject(raw["with"]) ? wp : ptr;
5497
5750
  let w = {};
5498
5751
  if (has(raw, "with")) {
5499
5752
  if (!isObject(raw["with"])) {
5500
- ctx.issues.push({ order, path: `${p}.with`, rule: "must be an object" });
5753
+ issue(ctx, { order, code: "R13", path: `${p}.with`, pointer: wp, rule: "must be an object" });
5501
5754
  } else {
5502
5755
  w = raw["with"];
5503
5756
  for (const k of keysOf(w)) {
5504
5757
  if (!params.includes(k)) {
5505
- ctx.issues.push({ order, path: `${p}.with.${quote(k)}`, rule: `unknown parameter; ${a} takes ${params.length ? params.join(", ") : "no parameters"}` });
5758
+ issue(ctx, {
5759
+ order,
5760
+ code: "R13",
5761
+ path: `${p}.with.${quote(k)}`,
5762
+ pointer: `${wp}${jsonPointer(k)}`,
5763
+ rule: `unknown parameter; ${a} takes ${params.length ? params.join(", ") : "no parameters"}`
5764
+ });
5506
5765
  }
5507
5766
  }
5508
5767
  }
5509
5768
  }
5510
5769
  const messaging = v.messagingActions.includes(a);
5511
5770
  const hasVariants = has(raw, "variants");
5512
- if (hasVariants && !messaging) ctx.issues.push({ order, path: `${p}.variants`, rule: "variants are allowed on messaging steps only" });
5771
+ const vptr = `${ptr}/variants`;
5772
+ if (hasVariants && !messaging) issue(ctx, { order, code: "R23", path: `${p}.variants`, pointer: vptr, rule: "variants are allowed on messaging steps only" });
5513
5773
  if (has(w, "post") && params.includes("post") && w["post"] !== v.post) {
5514
- ctx.issues.push({ order, path: `${p}.with.post`, rule: `only "${v.post}" is supported` });
5774
+ issue(ctx, { order, code: "R13", path: `${p}.with.post`, pointer: `${wp}/post`, rule: `only "${v.post}" is supported` });
5515
5775
  }
5516
5776
  if (has(w, "newer_than") && params.includes("newer_than") && !durationOk(ctx, w["newer_than"], DURATION_BOUNDS.newer_than)) {
5517
- ctx.issues.push({ order, path: `${p}.with.newer_than`, rule: durationRule(DURATION_BOUNDS.newer_than) });
5777
+ issue(ctx, {
5778
+ order,
5779
+ code: "R12",
5780
+ path: `${p}.with.newer_than`,
5781
+ pointer: `${wp}/newer_than`,
5782
+ rule: durationRule(DURATION_BOUNDS.newer_than),
5783
+ token: durationHint(DURATION_BOUNDS.newer_than)
5784
+ });
5518
5785
  }
5519
5786
  if (has(w, "reaction") && params.includes("reaction") && !v.reactions.includes(w["reaction"])) {
5520
- ctx.issues.push({ order, path: `${p}.with.reaction`, rule: `must be one of ${v.reactions.join(", ")}` });
5787
+ issue(ctx, { order, code: "R13", path: `${p}.with.reaction`, pointer: `${wp}/reaction`, rule: `must be one of ${v.reactions.join(", ")}` });
5521
5788
  }
5522
5789
  if (has(w, "withdraw_after") && params.includes("withdraw_after") && !durationOk(ctx, w["withdraw_after"], DURATION_BOUNDS.withdraw_after)) {
5523
- ctx.issues.push({ order, path: `${p}.with.withdraw_after`, rule: durationRule(DURATION_BOUNDS.withdraw_after) });
5790
+ issue(ctx, { order, code: "R21", path: `${p}.with.withdraw_after`, pointer: `${wp}/withdraw_after`, rule: durationRule(DURATION_BOUNDS.withdraw_after) });
5524
5791
  }
5525
5792
  if (!messaging) return;
5526
5793
  const textKeys = a === "inmail.send" ? ["subject", "text"] : ["text"];
5527
5794
  const withTextKeys = a === "invite.send" ? ["note"] : textKeys;
5528
5795
  const variants = hasVariants ? raw["variants"] : void 0;
5529
5796
  if (hasVariants && withTextKeys.some((k) => has(w, k) && w[k] !== null)) {
5530
- ctx.issues.push({ order, path: `${p}.variants`, rule: `use either variants or with.${withTextKeys.join(" and with.")}, not both` });
5797
+ issue(ctx, { order, code: "R23", path: `${p}.variants`, pointer: vptr, rule: `use either variants or with.${withTextKeys.join(" and with.")}, not both` });
5531
5798
  }
5532
5799
  const texts = [];
5533
5800
  if (hasVariants) {
5534
- if (!Array.isArray(variants) || variants.length < v.minVariants || variants.length > v.maxVariants) {
5535
- ctx.issues.push({
5801
+ const n = Array.isArray(variants) ? variants.length : null;
5802
+ if (n !== null && n > MAX_VARIANTS && n <= v.maxVariants) {
5803
+ warning(ctx, { order, code: "W13", path: `${p}.variants`, pointer: vptr, rule: `${n} variants; A/B takes exactly ${MAX_VARIANTS}${UNTIL}`, token: `${MAX_VARIANTS} variants` });
5804
+ } else if (n === null || n < v.minVariants || n > MAX_VARIANTS) {
5805
+ issue(ctx, {
5536
5806
  order,
5807
+ code: "R23",
5537
5808
  path: `${p}.variants`,
5538
- rule: `needs ${v.minVariants} to ${v.maxVariants} variants${Array.isArray(variants) ? `; found ${variants.length}` : ""}`
5809
+ pointer: vptr,
5810
+ rule: `needs exactly ${MAX_VARIANTS} variants (A/B)${n === null ? "" : `; found ${n}`}`
5539
5811
  });
5540
5812
  }
5541
5813
  (Array.isArray(variants) ? variants : []).forEach((variant, j) => {
5542
5814
  let vp = `${p}.variants[${j}]`;
5815
+ const vptrJ = `${vptr}/${j}`;
5543
5816
  if (!isObject(variant)) {
5544
- ctx.issues.push({ order, path: vp, rule: "a variant is a JSON object" });
5817
+ issue(ctx, { order, code: "R23", path: vp, pointer: vptrJ, rule: "a variant is a JSON object" });
5545
5818
  return;
5546
5819
  }
5547
5820
  const vid = variant["id"];
5821
+ const idAt = { order, code: "R23", path: `${vp}.id`, pointer: has(variant, "id") ? `${vptrJ}/id` : vptrJ };
5548
5822
  if (typeof vid !== "string" || !ctx.re.id.test(vid)) {
5549
- ctx.issues.push({ order, path: `${vp}.id`, rule: `must match ${v.idPattern}` });
5823
+ issue(ctx, { ...idAt, rule: `must match ${v.idPattern}` });
5550
5824
  } else if (vid === END) {
5551
- ctx.issues.push({ order, path: `${vp}.id`, rule: '"end" is reserved' });
5825
+ issue(ctx, { ...idAt, rule: '"end" is reserved' });
5552
5826
  } else if (usedIds.has(vid)) {
5553
- ctx.issues.push({ order, path: `${vp}.id`, rule: `${vid} is already a step or variant id` });
5827
+ issue(ctx, { ...idAt, rule: `${vid} is already a step or variant id` });
5554
5828
  } else {
5555
5829
  usedIds.add(vid);
5556
5830
  vp = `${p}.variants.${vid}`;
5557
5831
  }
5558
5832
  for (const k of keysOf(variant)) {
5559
5833
  if (k !== "id" && !textKeys.includes(k)) {
5560
- ctx.issues.push({ order, path: `${vp}.${quote(k)}`, rule: `unknown key; a variant holds id and ${textKeys.join(" and ")}` });
5834
+ issue(ctx, { order, code: "R23", path: `${vp}.${quote(k)}`, pointer: `${vptrJ}${jsonPointer(k)}`, rule: `unknown key; a variant holds id and ${textKeys.join(" and ")}` });
5561
5835
  }
5562
5836
  }
5563
5837
  for (const field of textKeys) {
5564
- const value = checkText(ctx, order, `${vp}.${field}`, variant[field], limitOf(a, field));
5565
- if (value !== null) texts.push({ path: `${vp}.${field}`, value });
5838
+ const value = checkText(ctx, order, `${vp}.${field}`, `${vptrJ}/${field}`, vptrJ, variant[field], limitOf(a, field));
5839
+ if (value !== null) texts.push({ path: `${vp}.${field}`, pointer: `${vptrJ}/${field}`, value });
5566
5840
  }
5567
5841
  });
5568
5842
  } else {
5569
5843
  for (const field of withTextKeys) {
5844
+ const fp = `${wp}/${field}`;
5570
5845
  if (a === "invite.send") {
5571
5846
  if (has(w, "note") && w["note"] !== null) {
5572
- const value = checkText(ctx, order, `${p}.with.note`, w["note"], limitOf(a, field));
5573
- if (value !== null) texts.push({ path: `${p}.with.note`, value });
5847
+ const value = checkText(ctx, order, `${p}.with.note`, fp, withParent, w["note"], limitOf(a, field));
5848
+ if (value !== null) texts.push({ path: `${p}.with.note`, pointer: fp, value });
5574
5849
  }
5575
5850
  } else if (!has(w, field)) {
5576
- ctx.issues.push({ order, path: `${p}.with.${field}`, rule: "required, or use variants" });
5851
+ issue(ctx, { order, code: "R13", path: `${p}.with.${field}`, pointer: withParent, rule: "required, or use variants" });
5577
5852
  } else {
5578
- const value = checkText(ctx, order, `${p}.with.${field}`, w[field], limitOf(a, field));
5579
- if (value !== null) texts.push({ path: `${p}.with.${field}`, value });
5853
+ const value = checkText(ctx, order, `${p}.with.${field}`, fp, withParent, w[field], limitOf(a, field));
5854
+ if (value !== null) texts.push({ path: `${p}.with.${field}`, pointer: fp, value });
5580
5855
  }
5581
5856
  }
5582
5857
  }
5583
5858
  if (a === "post.comment") {
5584
5859
  for (const t of texts) {
5585
- const contactVars = variablesIn(t.value).map((x) => x.inner).filter((i) => i.startsWith("contact.") && isKnownVariable(ctx, i));
5860
+ const contactVars = variablesIn(t.value).filter((x) => isKnownVariable(ctx, x.inner)).map((x) => longVariable(ctx, x.inner)).filter((l) => l.startsWith("contact."));
5586
5861
  if (contactVars.length === 0) {
5587
- ctx.issues.push({ order, path: t.path, rule: "a comment must use a contact variable, such as {{contact.extra.comment}}" });
5588
- } else if (contactVars.every((i) => NAME_FIELDS.includes(i.slice(8)))) {
5589
- ctx.warnings.push({
5862
+ issue(ctx, { order, code: "R20", path: t.path, pointer: t.pointer, rule: "a comment must use a contact variable, such as {{contact.extra.comment}}" });
5863
+ } else if (contactVars.every((l) => NAME_FIELDS.includes(l.slice(8)))) {
5864
+ warning(ctx, {
5590
5865
  order,
5591
5866
  code: "W7",
5592
5867
  path: t.path,
5868
+ pointer: t.pointer,
5593
5869
  rule: "the comment's only per-Contact variables are name fields; write it per Contact, for example in contact.extra"
5594
5870
  });
5595
5871
  }
@@ -5598,35 +5874,38 @@ function checkDo(ctx, order, p, raw, usedIds) {
5598
5874
  const usesVariable = texts.some((t) => hasVariable(t.value));
5599
5875
  if (a === "invite.send") {
5600
5876
  const fp = `${p}.with.note_fallback`;
5601
- if (has(w, "note_fallback")) checkFallbackText(ctx, order, fp, w["note_fallback"], TEXT_LIMITS.invite_note, "drop");
5602
- else if (usesVariable) ctx.issues.push({ order, path: fp, rule: 'required because the note uses a variable: text, or "drop"' });
5877
+ if (has(w, "note_fallback")) checkFallbackText(ctx, order, fp, `${wp}/note_fallback`, withParent, w["note_fallback"], TEXT_LIMITS.invite_note, "drop");
5878
+ else if (usesVariable) issue(ctx, { order, code: "R19", path: fp, pointer: withParent, rule: 'required because the note uses a variable: text, or "drop"' });
5603
5879
  } else if (a === "inmail.send") {
5604
5880
  const fp = `${p}.with.fallback`;
5881
+ const fptr = `${wp}/fallback`;
5605
5882
  if (has(w, "fallback")) {
5606
5883
  const f = w["fallback"];
5607
5884
  if (!isObject(f)) {
5608
- ctx.issues.push({ order, path: fp, rule: "must be an object with subject and text" });
5885
+ issue(ctx, { order, code: "R19", path: fp, pointer: fptr, rule: "must be an object with subject and text" });
5609
5886
  } else {
5610
5887
  for (const k of keysOf(f)) {
5611
- if (k !== "subject" && k !== "text") ctx.issues.push({ order, path: `${fp}.${quote(k)}`, rule: "unknown key; a fallback holds subject and text" });
5888
+ if (k !== "subject" && k !== "text") {
5889
+ issue(ctx, { order, code: "R19", path: `${fp}.${quote(k)}`, pointer: `${fptr}${jsonPointer(k)}`, rule: "unknown key; a fallback holds subject and text" });
5890
+ }
5612
5891
  }
5613
- checkFallbackText(ctx, order, `${fp}.subject`, f["subject"], TEXT_LIMITS.inmail_subject);
5614
- checkFallbackText(ctx, order, `${fp}.text`, f["text"], TEXT_LIMITS.inmail_text);
5892
+ checkFallbackText(ctx, order, `${fp}.subject`, `${fptr}/subject`, fptr, f["subject"], TEXT_LIMITS.inmail_subject);
5893
+ checkFallbackText(ctx, order, `${fp}.text`, `${fptr}/text`, fptr, f["text"], TEXT_LIMITS.inmail_text);
5615
5894
  }
5616
5895
  } else if (usesVariable) {
5617
- ctx.issues.push({ order, path: fp, rule: "required because the subject or text uses a variable: an object with subject and text" });
5896
+ issue(ctx, { order, code: "R19", path: fp, pointer: withParent, rule: "required because the subject or text uses a variable: an object with subject and text" });
5618
5897
  }
5619
5898
  } else {
5620
5899
  const fp = `${p}.with.fallback`;
5621
5900
  const comment = a === "post.comment";
5622
- if (has(w, "fallback")) checkFallbackText(ctx, order, fp, w["fallback"], limitOf(a, "text"), comment ? "skip" : void 0);
5623
- else if (comment) ctx.issues.push({ order, path: fp, rule: 'required on a comment: text, or "skip"' });
5624
- else if (usesVariable) ctx.issues.push({ order, path: fp, rule: "required because the text uses a variable" });
5901
+ if (has(w, "fallback")) checkFallbackText(ctx, order, fp, `${wp}/fallback`, withParent, w["fallback"], limitOf(a, "text"), comment ? "skip" : void 0);
5902
+ else if (comment) issue(ctx, { order, code: "R19", path: fp, pointer: withParent, rule: 'required on a comment: text, or "skip"' });
5903
+ else if (usesVariable) issue(ctx, { order, code: "R19", path: fp, pointer: withParent, rule: "required because the text uses a variable" });
5625
5904
  }
5626
5905
  }
5627
- function checkPredicate(ctx, pred, path, order, nested = false) {
5906
+ function checkPredicate(ctx, pred, path, pointer, order, nested = false) {
5628
5907
  const { v } = ctx;
5629
- const push = (rule) => ctx.issues.push({ order, path, rule });
5908
+ const push = (rule) => issue(ctx, { order, code: "R15", path, pointer, rule });
5630
5909
  if (!nested && deeperThan(pred, MAX_PREDICATE_DEPTH)) {
5631
5910
  push(`a predicate nests at most ${MAX_PREDICATE_DEPTH} levels`);
5632
5911
  return;
@@ -5639,7 +5918,7 @@ function checkPredicate(ctx, pred, path, order, nested = false) {
5639
5918
  const val = pred[key];
5640
5919
  if (key === "not") {
5641
5920
  if (nested) push("not cannot wrap another not");
5642
- else checkPredicate(ctx, val, `${path}.not`, order, true);
5921
+ else checkPredicate(ctx, val, `${path}.not`, `${pointer}/not`, order, true);
5643
5922
  return;
5644
5923
  }
5645
5924
  if (key.startsWith("account.")) {
@@ -5647,6 +5926,11 @@ function checkPredicate(ctx, pred, path, order, nested = false) {
5647
5926
  else if (typeof val !== "boolean") push("account.premium takes true or false");
5648
5927
  return;
5649
5928
  }
5929
+ if (key.startsWith("message.")) {
5930
+ if (key !== "message.seen") push(`unknown predicate ${quote(key)}`);
5931
+ else if (typeof val !== "boolean") push("message.seen takes true or false");
5932
+ return;
5933
+ }
5650
5934
  if (v.booleanPredicates.includes(key)) {
5651
5935
  if (typeof val !== "boolean") push(`${key} takes true or false`);
5652
5936
  return;
@@ -5700,11 +5984,18 @@ function graph(ctx, infos, ids, onReply) {
5700
5984
  return [pick("next")];
5701
5985
  }
5702
5986
  });
5987
+ const exitPtr = (i, e) => e.fallThrough ? infos[i].ptr : `${infos[i].ptr}/${e.field}`;
5703
5988
  const graphIssues = [];
5704
5989
  infos.forEach((info, i) => {
5705
5990
  for (const e of edges[i]) {
5706
5991
  if (e.to === "off") {
5707
- graphIssues.push({ order: i, path: `${info.prefix}.${e.field}`, rule: `the last step must end explicitly; set ${e.field} to a step id or "end"` });
5992
+ graphIssues.push({
5993
+ order: i,
5994
+ code: "R10",
5995
+ path: `${info.prefix}.${e.field}`,
5996
+ pointer: exitPtr(i, e),
5997
+ rule: `the last step must end explicitly; set ${e.field} to a step id or "end"`
5998
+ });
5708
5999
  }
5709
6000
  }
5710
6001
  });
@@ -5718,7 +6009,7 @@ function graph(ctx, infos, ids, onReply) {
5718
6009
  for (const e of edges[i]) if (typeof e.to === "number") queue.push(e.to);
5719
6010
  }
5720
6011
  infos.forEach((info, i) => {
5721
- if (!seen.has(i)) graphIssues.push({ order: i, path: info.prefix, rule: "unreachable: no path leads here from the first step or from on_reply" });
6012
+ if (!seen.has(i)) graphIssues.push({ order: i, code: "R9", path: info.prefix, pointer: info.ptr, rule: "unreachable: no path leads here from the first step or from on_reply" });
5722
6013
  });
5723
6014
  const state = new Array(n).fill(0);
5724
6015
  const stack = [];
@@ -5729,7 +6020,7 @@ function graph(ctx, infos, ids, onReply) {
5729
6020
  if (typeof e.to !== "number") continue;
5730
6021
  if (state[e.to] === 1) {
5731
6022
  const rule = `cycle: ${[...stack.slice(stack.indexOf(e.to)), e.to].map((k) => infos[k].prefix).join(" -> ")}`;
5732
- if (!graphIssues.some((x) => x.rule === rule)) graphIssues.push({ order: i, path: `${infos[i].prefix}.${e.field}`, rule });
6023
+ if (!graphIssues.some((x) => x.rule === rule)) graphIssues.push({ order: i, code: "R8", path: `${infos[i].prefix}.${e.field}`, pointer: exitPtr(i, e), rule });
5733
6024
  } else if (state[e.to] === 0) {
5734
6025
  dfs(e.to);
5735
6026
  }
@@ -5748,10 +6039,11 @@ function graph(ctx, infos, ids, onReply) {
5748
6039
  const jumpFrom = preds[i].filter((x) => !x.edge.fallThrough && x.from !== i - 1).map((x) => x.from);
5749
6040
  if (fallFrom.length && jumpFrom.length) {
5750
6041
  const names = (xs) => [...new Set(xs)].map((k) => infos[k].prefix).join(", ");
5751
- ctx.warnings.push({
6042
+ warning(ctx, {
5752
6043
  order: i,
5753
6044
  code: "W1",
5754
6045
  path: info.prefix,
6046
+ pointer: info.ptr,
5755
6047
  rule: `merge point: reached by fall-through from ${names(fallFrom)} and by a jump from ${names(jumpFrom)}`
5756
6048
  });
5757
6049
  }
@@ -5759,22 +6051,36 @@ function graph(ctx, infos, ids, onReply) {
5759
6051
  const order = topo(n, edges, roots);
5760
6052
  const afterInvite = new Array(n).fill(false);
5761
6053
  const withoutInvite = new Array(n).fill(false);
6054
+ const withoutMessage = new Array(n).fill(false);
5762
6055
  const fromFirst = new Set(topo(n, edges, [0]));
5763
- for (const r of roots) if (r === 0 || !fromFirst.has(r)) withoutInvite[r] = true;
6056
+ for (const r of roots) if (r === 0 || !fromFirst.has(r)) withoutInvite[r] = withoutMessage[r] = true;
5764
6057
  for (const i of order) {
5765
6058
  for (const { from, edge } of preds[i]) {
5766
6059
  const invited = action(from) === "invite.send" && edge.field === "next";
5767
6060
  if (afterInvite[from] || invited) afterInvite[i] = true;
5768
6061
  if (withoutInvite[from] && !invited) withoutInvite[i] = true;
6062
+ const messaged = (action(from) === "message.send" || action(from) === "inmail.send") && edge.field === "next";
6063
+ if (withoutMessage[from] && !messaged) withoutMessage[i] = true;
5769
6064
  }
5770
6065
  }
5771
6066
  infos.forEach((info, i) => {
5772
6067
  if (!seen.has(i)) return;
5773
6068
  if (action(i) === "profile.follow" && afterInvite[i]) {
5774
- ctx.warnings.push({ order: i, code: "W5", path: `${info.prefix}.do`, rule: "profile.follow after invite.send on the same path; an invitation already follows" });
6069
+ warning(ctx, { order: i, code: "W5", path: `${info.prefix}.do`, pointer: `${info.ptr}/do`, rule: "profile.follow after invite.send on the same path; an invitation already follows" });
6070
+ }
6071
+ const cond = info.raw["if"];
6072
+ const seenPred = isObject(cond) && (has(cond, "message.seen") || isObject(cond["not"]) && has(cond["not"], "message.seen"));
6073
+ if (seenPred && withoutMessage[i]) {
6074
+ warning(ctx, {
6075
+ order: i,
6076
+ code: "W12",
6077
+ path: `${info.prefix}.if`,
6078
+ pointer: `${info.ptr}/if`,
6079
+ rule: "if message.seen on a path with no message.send or inmail.send before it; it is always false there"
6080
+ });
5775
6081
  }
5776
6082
  if (info.raw["until"] === "invite.accepted" && withoutInvite[i]) {
5777
- ctx.warnings.push({ order: i, code: "W6", path: `${info.prefix}.until`, rule: "until invite.accepted on a path with no invite.send before it" });
6083
+ warning(ctx, { order: i, code: "W6", path: `${info.prefix}.until`, pointer: `${info.ptr}/until`, rule: "until invite.accepted on a path with no invite.send before it" });
5778
6084
  }
5779
6085
  });
5780
6086
  const flagged = /* @__PURE__ */ new Set();
@@ -5791,7 +6097,7 @@ function graph(ctx, infos, ids, onReply) {
5791
6097
  if (!flagged.has(k)) {
5792
6098
  flagged.add(k);
5793
6099
  const link = messageTexts(infos[k]).find((t) => t.value.includes("http"));
5794
- if (link) ctx.warnings.push({ order: k, code: "W8", path: link.path, rule: "a link in the first message after the invitation is accepted" });
6100
+ if (link) warning(ctx, { order: k, code: "W8", path: link.path, pointer: link.pointer, rule: "a link in the first message after the invitation is accepted" });
5795
6101
  }
5796
6102
  continue;
5797
6103
  }
@@ -5807,12 +6113,12 @@ function messageTexts(info) {
5807
6113
  raw["variants"].forEach((variant, j) => {
5808
6114
  if (isObject(variant) && typeof variant["text"] === "string") {
5809
6115
  const id = typeof variant["id"] === "string" ? `variants.${variant["id"]}` : `variants[${j}]`;
5810
- out.push({ path: `${info.prefix}.${id}.text`, value: variant["text"] });
6116
+ out.push({ path: `${info.prefix}.${id}.text`, pointer: `${info.ptr}/variants/${j}/text`, value: variant["text"] });
5811
6117
  }
5812
6118
  });
5813
6119
  }
5814
- if (typeof w["text"] === "string") out.push({ path: `${info.prefix}.with.text`, value: w["text"] });
5815
- if (typeof w["fallback"] === "string") out.push({ path: `${info.prefix}.with.fallback`, value: w["fallback"] });
6120
+ if (typeof w["text"] === "string") out.push({ path: `${info.prefix}.with.text`, pointer: `${info.ptr}/with/text`, value: w["text"] });
6121
+ if (typeof w["fallback"] === "string") out.push({ path: `${info.prefix}.with.fallback`, pointer: `${info.ptr}/with/fallback`, value: w["fallback"] });
5816
6122
  return out;
5817
6123
  }
5818
6124
  function topo(n, edges, roots) {
@@ -5880,7 +6186,7 @@ var _Sequence = class _Sequence {
5880
6186
  * Appends steps in order: constructor output or object literals.
5881
6187
  *
5882
6188
  * @example
5883
- * const next = seq.add(step.wait("pause", "2d"), step.emit("done", "sequence.done", { next: "end" }));
6189
+ * const next = seq.add(step.wait("pause", "2d"), step.tag("done", "sequence-done", { event: "sequence_done", next: "end" }));
5884
6190
  */
5885
6191
  add(...steps) {
5886
6192
  return __privateMethod(this, _Sequence_instances, with_fn).call(this, [...__privateGet(this, _doc).steps, ...clone(steps)]);
@@ -6029,7 +6335,10 @@ var MotionsResource = class {
6029
6335
  * Change a Motion. Objects (`schedule`, `settings`) merge by key; arrays,
6030
6336
  * `where` and `sequence` replace whole; `null` clears. `sequence` takes a
6031
6337
  * builder or a document, as in `create`. After start, a structural Sequence
6032
- * change makes a new version; running Enrollments keep theirs.
6338
+ * change makes a new version; running Enrollments keep theirs. Send
6339
+ * `if_sequence_revision` (the `sequence_revision` you read) to refuse the
6340
+ * save when someone saved the Sequence since: `SEQUENCE_CONFLICT`, nothing
6341
+ * saved, the stored document in `err.current`. Omitted: the last write wins.
6033
6342
  * `PATCH /v1/motions/{id}`
6034
6343
  *
6035
6344
  * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
@@ -6038,6 +6347,7 @@ var MotionsResource = class {
6038
6347
  * const motion = await curviate.motions.get("mot_123");
6039
6348
  * await curviate.motions.update("mot_123", {
6040
6349
  * sequence: Sequence.fromDocument(motion.sequence).replace("pause", step.wait("pause", "3d", { next: "end" })),
6350
+ * if_sequence_revision: motion.sequence_revision,
6041
6351
  * });
6042
6352
  */
6043
6353
  async update(motionId, body) {
@@ -6172,15 +6482,22 @@ var EnrollmentsResource = class {
6172
6482
  }
6173
6483
  /**
6174
6484
  * List Enrollments, newest first. Filters: `motion_id`, `contact_id`,
6175
- * `account_id`, `status` (any of them), `step_id`. Pages with an opaque
6176
- * `cursor` until it is `null`.
6485
+ * `account_id`, `status` (any of them), `step_id` (at that Step now),
6486
+ * `version`; by Step outcome: `done_step` with `outcome` (how that Step
6487
+ * ended), `reason` (catalogue codes, any of them), `variant_id`,
6488
+ * `relationship_at_enrollment` (any of them). Pages with an opaque `cursor`
6489
+ * until it is `null`.
6177
6490
  * `GET /v1/enrollments`
6178
6491
  *
6179
6492
  * @example
6180
- * const { items } = await curviate.enrollments.list({ motion_id: "mot_123", status: ["active"] });
6493
+ * const { items } = await curviate.enrollments.list({ motion_id: "mot_123", done_step: "opener", outcome: "failed" });
6181
6494
  */
6182
6495
  list(params) {
6183
- return this.ctx.request({ method: "GET", path: "/v1/enrollments", query: commaLists(params, "status") });
6496
+ return this.ctx.request({
6497
+ method: "GET",
6498
+ path: "/v1/enrollments",
6499
+ query: commaLists(params, "status", "reason", "relationship_at_enrollment")
6500
+ });
6184
6501
  }
6185
6502
  /**
6186
6503
  * One Enrollment with its `history` of Steps (metadata only, no rendered
@@ -6205,29 +6522,131 @@ var EnrollmentsResource = class {
6205
6522
  return this.ctx.request({ method: "POST", path: apiPath`/v1/enrollments/${enrollmentId}/stop` });
6206
6523
  }
6207
6524
  /**
6208
- * Continue an Enrollment at Step `at` of its Sequence version, `on_reply`
6209
- * re-armed: the out-of-office path. Works on `active`, `paused` and most
6210
- * `finished` Enrollments; else `ENROLLMENT_NOT_ACTIVE`.
6525
+ * Hold one `pending`, `active` or `paused` Enrollment until you `resume` it
6526
+ * (`waiting.kind: "paused_by_user"`); its waits freeze. A Step being sent
6527
+ * right now is `STEP_IN_FLIGHT`: retry in a minute. Works on every plan.
6528
+ * `POST /v1/enrollments/{id}/pause`
6529
+ *
6530
+ * @example
6531
+ * await curviate.enrollments.pause("enr_123");
6532
+ */
6533
+ pause(enrollmentId) {
6534
+ return this.ctx.request({ method: "POST", path: apiPath`/v1/enrollments/${enrollmentId}/pause` });
6535
+ }
6536
+ /**
6537
+ * Without `at`: lift your own pause; the Enrollment continues where it was,
6538
+ * with the time its wait had left. With `at`: an explicit jump to that Step
6539
+ * of its Sequence version, `on_reply` re-armed (the out-of-office path).
6540
+ * The jump is not guarded: it re-enters the Step you name even when that
6541
+ * Step already sent, so a message can go out again. To retry a failed Step,
6542
+ * use `enrollments.retry(id)`, which acts only on a failure. Works on
6543
+ * `active`, `paused` and most `finished` Enrollments; else
6544
+ * `ENROLLMENT_NOT_ACTIVE`.
6211
6545
  * `POST /v1/enrollments/{id}/resume`
6212
6546
  *
6213
6547
  * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
6214
6548
  *
6215
6549
  * @example
6216
- * await curviate.enrollments.resume("enr_123", { at: "bump" });
6550
+ * await curviate.enrollments.resume("enr_123"); // lift your pause
6551
+ * await curviate.enrollments.resume("enr_123", { at: "bump" }); // jump to a Step after an out-of-office reply
6217
6552
  */
6218
6553
  resume(enrollmentId, body) {
6219
6554
  return this.ctx.request({ method: "POST", path: apiPath`/v1/enrollments/${enrollmentId}/resume`, body });
6220
6555
  }
6221
6556
  /**
6222
- * Send the signal a Step waits for (`until signal.<name>`), on an `active`
6223
- * or `paused` Enrollment. A name no Step of its version awaits is
6224
- * `INVALID_REQUEST`, listing the awaited names.
6557
+ * Send a custom event to an `active` or `paused` Enrollment whose Sequence
6558
+ * waits for it (`until event.<name>`), for example once your agent wrote
6559
+ * the researched text to the Contact's custom fields. Input only: fires no
6560
+ * webhook. A name no Step of its version awaits is `INVALID_REQUEST`,
6561
+ * listing the awaited names.
6562
+ * `POST /v1/enrollments/{id}/events`
6563
+ *
6564
+ * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
6565
+ *
6566
+ * @example
6567
+ * await curviate.enrollments.events("enr_123", { name: "research_ready" });
6568
+ */
6569
+ events(enrollmentId, body) {
6570
+ return this.ctx.request({ method: "POST", path: apiPath`/v1/enrollments/${enrollmentId}/events`, body });
6571
+ }
6572
+ /**
6573
+ * Move an Enrollment to another sending account of its Motion; it restarts
6574
+ * there at the first Step of its Sequence version (one you paused stays
6575
+ * paused until you resume it). `warnings` names each Step that will be
6576
+ * skipped. Refused, nothing changed, with `ACCOUNT_ALREADY_ENGAGED` (its
6577
+ * account already contacted the person beyond a profile view),
6578
+ * `ALREADY_ENROLLED_ON_ACCOUNT`, `KNOWN_CHAT` or `STEP_IN_FLIGHT`.
6579
+ * `POST /v1/enrollments/{id}/reassign`
6580
+ *
6581
+ * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
6582
+ *
6583
+ * @example
6584
+ * const { warnings } = await curviate.enrollments.reassign("enr_123", { account_id: "acc_456" });
6585
+ */
6586
+ reassign(enrollmentId, body) {
6587
+ return this.ctx.request({ method: "POST", path: apiPath`/v1/enrollments/${enrollmentId}/reassign`, body });
6588
+ }
6589
+ /**
6590
+ * Pause, resume, stop, retry, send an event to or reassign up to 1,000
6591
+ * Enrollments, by `ids` or by a `filter` of the list's fields (`motion_id`
6592
+ * required). Each is decided alone, as its single call would be, with its
6593
+ * own result (`ok`, or `error` with that call's code; a server error on one
6594
+ * is `INTERNAL` for it); not atomic. List with the same filter to see the
6595
+ * matches first. Below Automate only `pause` and `stop` run.
6596
+ * `POST /v1/enrollments/bulk`
6597
+ *
6598
+ * @example
6599
+ * const { results } = await curviate.enrollments.bulk({
6600
+ * action: "retry",
6601
+ * filter: { motion_id: "mot_123", done_step: "opener", outcome: "failed" },
6602
+ * });
6603
+ */
6604
+ bulk(body) {
6605
+ return this.ctx.request({ method: "POST", path: "/v1/enrollments/bulk", body });
6606
+ }
6607
+ /**
6608
+ * Retry the failed Step of one Enrollment: the guarded Retry. It acts only
6609
+ * while that Step's latest outcome is `failed`, so a repeated or looped
6610
+ * call never sends a delivered message again. Resolves with the
6611
+ * Enrollment's result when it was resumed; otherwise throws its refusal as
6612
+ * a `CurviateError` (`NOTHING_TO_RETRY` when nothing failed,
6613
+ * `STEP_IN_FLIGHT`, `ENROLLMENT_NOT_ACTIVE`, `RESOURCE_NOT_FOUND`, ...),
6614
+ * with no `httpStatus`. Sent as `bulk` with this one id.
6615
+ * `POST /v1/enrollments/bulk`
6616
+ *
6617
+ * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
6618
+ *
6619
+ * @example
6620
+ * try {
6621
+ * await curviate.enrollments.retry("enr_123");
6622
+ * } catch (err) {
6623
+ * if (isCurviateError(err) && err.code === "NOTHING_TO_RETRY") console.log("nothing failed");
6624
+ * }
6625
+ */
6626
+ async retry(enrollmentId) {
6627
+ const res = await this.bulk({ action: "retry", ids: [enrollmentId] });
6628
+ const item = res.results[0];
6629
+ if (item?.outcome === "ok") return item;
6630
+ const known = item?.error && KNOWN_ERROR_CODES.has(item.error.code);
6631
+ const code = known ? item.error.code : "INTERNAL";
6632
+ throw new CurviateError({
6633
+ code,
6634
+ message: item?.error?.message ?? `No retry result for ${enrollmentId}.`,
6635
+ userFixable: false,
6636
+ retryLikelyToSucceed: code === "STEP_IN_FLIGHT"
6637
+ });
6638
+ }
6639
+ /**
6640
+ * Deprecated alias of {@link EnrollmentsResource.events}, answered the same
6641
+ * plus a `Deprecation` header and warning W13; accepted until 2027-01-19.
6225
6642
  * `POST /v1/enrollments/{id}/signal`
6226
6643
  *
6227
6644
  * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
6228
6645
  *
6646
+ * @deprecated Use `enrollments.events`; `signal` stops working on 2027-01-19.
6647
+ *
6229
6648
  * @example
6230
- * await curviate.enrollments.signal("enr_123", { name: "demo_booked" });
6649
+ * await curviate.enrollments.events("enr_123", { name: "demo_booked" }); // instead of signal
6231
6650
  */
6232
6651
  signal(enrollmentId, body) {
6233
6652
  return this.ctx.request({ method: "POST", path: apiPath`/v1/enrollments/${enrollmentId}/signal`, body });
@@ -7868,6 +8287,42 @@ var step = {
7868
8287
  tag: (id, name, fields) => build(id, "tag", name, fields)
7869
8288
  };
7870
8289
 
8290
+ // src/sequence/ids.ts
8291
+ var ID_BASE = {
8292
+ "profile.view": "visit",
8293
+ "profile.follow": "follow",
8294
+ "profile.unfollow": "unfollow",
8295
+ "invite.send": "invite",
8296
+ "invite.withdraw": "withdraw",
8297
+ "message.send": "message",
8298
+ "inmail.send": "inmail",
8299
+ "post.react": "like",
8300
+ "post.comment": "comment",
8301
+ "skill.endorse": "endorse",
8302
+ wait: "wait",
8303
+ until: "accepted",
8304
+ if: "check",
8305
+ emit: "event",
8306
+ tag: "tag"
8307
+ };
8308
+ var MAX_ID = 40;
8309
+ var idOf = (v) => isObject(v) && typeof v["id"] === "string" ? v["id"] : null;
8310
+ var fit = (stem, suffix) => stem.slice(0, MAX_ID - suffix.length) + suffix;
8311
+ function newStepId(doc, kind, beforeId) {
8312
+ const taken = /* @__PURE__ */ new Set();
8313
+ for (const s of isObject(doc) && Array.isArray(doc["steps"]) ? doc["steps"] : []) {
8314
+ const id = idOf(s);
8315
+ if (id) taken.add(id);
8316
+ const vs = isObject(s) ? s["variants"] : void 0;
8317
+ if (Array.isArray(vs)) {
8318
+ for (const v of vs) if (idOf(v)) taken.add(idOf(v));
8319
+ }
8320
+ }
8321
+ const base = kind === "wait" && beforeId !== void 0 ? `w_${beforeId}` : ID_BASE[kind];
8322
+ if (!taken.has(fit(base, ""))) return fit(base, "");
8323
+ for (let n = 2; ; n++) if (!taken.has(fit(base, `_${n}`))) return fit(base, `_${n}`);
8324
+ }
8325
+
7871
8326
  // src/webhooks.ts
7872
8327
  var WebhookSignatureError = class _WebhookSignatureError extends Error {
7873
8328
  constructor(reason, message) {
@@ -8012,6 +8467,7 @@ export {
8012
8467
  WebhookSignatureError,
8013
8468
  constructEvent,
8014
8469
  isCurviateError,
8470
+ newStepId,
8015
8471
  step,
8016
8472
  validateSequence
8017
8473
  };