@xema/omni-protocol 0.1.31 → 0.1.33

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.
@@ -9,7 +9,7 @@
9
9
  // Each list is pinned to its type both ways -- a member the type lacks, or a member the list
10
10
  // lacks, fails to compile -- so what the validators accept cannot drift from what the
11
11
  // declarations say.
12
- import { ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, IDLE_CAPABILITIES, OMNI_FAILURE_CODES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, negotiateProtocolVersion, DEFAULT_LEVELS, effectiveLevels, } from "./index.js";
12
+ import { ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, HANDLING_STEPS_THAT_DIAL, IDLE_CAPABILITIES, OMNI_FAILURE_CODES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, negotiateProtocolVersion, DEFAULT_LEVELS, effectiveLevels, } from "./index.js";
13
13
  export class ProtocolConformanceError extends Error {
14
14
  violations;
15
15
  constructor(violations, summary = "Adapter violates the Omni protocol") {
@@ -67,6 +67,10 @@ const ACCESS_APPLIES_TO = membersOf({
67
67
  "initial-url": true, "all-navigation": true,
68
68
  });
69
69
  const DIAL_DESTINATION_POLICIES = membersOf({ "contacts-only": true, "any-number": true });
70
+ const DIAL_OUTCOMES = membersOf({ answered: true, busy: true, "no-answer": true, unreachable: true, rejected: true });
71
+ const ON_CALL_ROLES = membersOf({ party: true, agent: true, consulted: true, conferenced: true });
72
+ /** The task capabilities under which a command dials, and so need the manifest to say how a dial ends. */
73
+ const DIALLING_CAPABILITIES = ["callback", "blindTransfer", "consultTransfer", "conference"];
70
74
  const SNAPSHOT_REASONS = membersOf({
71
75
  reconnected: true, "provider-requested": true,
72
76
  });
@@ -92,6 +96,8 @@ const TASK_CAPABILITIES = {
92
96
  chat: membersOf({ browsers: true, dispositions: true, custom: true, reject: true, hold: true }),
93
97
  email: membersOf({ browsers: true, dispositions: true, custom: true, reject: true }),
94
98
  };
99
+ // The published list and the type's keys are the same set, or one of them is wrong.
100
+ true;
95
101
  /** Idle capabilities each channel may declare. Only voice may dial, and runtime has to say so too. */
96
102
  const IDLE_CAPABILITIES_BY_CHANNEL = {
97
103
  voice: IDLE_CAPABILITIES,
@@ -266,6 +272,36 @@ function validateIdleCapabilities(value, channel, path, into) {
266
272
  }
267
273
  }
268
274
  }
275
+ /**
276
+ * A provider that dials says how a dial can end, both ways: `answered`, or the desk cannot tell
277
+ * reached from still ringing, and at least one way of not reaching, or it can never say failed.
278
+ * Off voice nothing dials, and the idle dialpad is a dial, so declaring it is declaring this.
279
+ */
280
+ function validateDialOutcomes(manifest, path, into) {
281
+ const declared = manifest.dialOutcomes;
282
+ const dials = isPlainObject(manifest.idleCapabilities) && manifest.idleCapabilities.dial !== undefined;
283
+ if (declared === undefined) {
284
+ into.require(!dials, "manifest.dialOutcomes.required", path, "a manifest that declares dial says how a dial ends: dialOutcomes with answered and at least one way of not reaching");
285
+ return;
286
+ }
287
+ if (!into.require(manifest.channel === "voice", "manifest.dialOutcomes.channel", path, `a ${String(manifest.channel)} provider dials nothing and declares no dial outcomes`))
288
+ return;
289
+ if (!Array.isArray(declared) || declared.length === 0) {
290
+ into.add("manifest.dialOutcomes.shape", path, "dialOutcomes is a non-empty array of the outcomes the platform distinguishes");
291
+ return;
292
+ }
293
+ declared.forEach((outcome, index) => {
294
+ if (into.oneOf(outcome, DIAL_OUTCOMES, "manifest.dialOutcome", `${path}[${index}]`) && declared.indexOf(outcome) !== index) {
295
+ into.add("manifest.dialOutcome.unique", `${path}[${index}]`, `duplicate dial outcome: ${String(outcome)}`);
296
+ }
297
+ });
298
+ into.require(declared.includes("answered"), "manifest.dialOutcomes.answered", path, "a dial that reached its destination is stated, never inferred: dialOutcomes includes answered");
299
+ into.require(declared.some((outcome) => outcome !== "answered"), "manifest.dialOutcomes.failure", path, "a provider that can only say answered can never say a destination was not reached: declare at least one other outcome");
300
+ }
301
+ /** Whether a manifest says how a dial ends, and so may publish tasks that dial. `undefined` where there is no manifest to ask. */
302
+ function manifestDials(manifest) {
303
+ return isPlainObject(manifest) ? Array.isArray(manifest.dialOutcomes) : undefined;
304
+ }
269
305
  export function validateManifest(manifest, path = "manifest") {
270
306
  const into = new Collector();
271
307
  if (!isPlainObject(manifest)) {
@@ -301,6 +337,7 @@ export function validateManifest(manifest, path = "manifest") {
301
337
  }
302
338
  if (channelValid)
303
339
  validateIdleCapabilities(manifest.idleCapabilities, manifest.channel, `${path}.idleCapabilities`, into);
340
+ validateDialOutcomes(manifest, `${path}.dialOutcomes`, into);
304
341
  if (manifest.phaseLabels !== undefined) {
305
342
  if (!isPlainObject(manifest.phaseLabels)) {
306
343
  into.add("manifest.phaseLabels.shape", `${path}.phaseLabels`, "phaseLabels must be an object when present");
@@ -312,6 +349,9 @@ export function validateManifest(manifest, path = "manifest") {
312
349
  }
313
350
  }
314
351
  }
352
+ if (manifest.runningStepReports !== undefined) {
353
+ into.require(manifest.runningStepReports === true, "manifest.runningStepReports", `${path}.runningStepReports`, "runningStepReports is declared by presence, as true; a provider that takes begin and end only omits it");
354
+ }
315
355
  if (manifest.orgLevels !== undefined) {
316
356
  if (!Array.isArray(manifest.orgLevels)) {
317
357
  into.add("manifest.orgLevels.shape", `${path}.orgLevels`, "orgLevels must be an array when present");
@@ -628,13 +668,27 @@ function validateTaskAttributes(value, path, into) {
628
668
  function validateHandlingHistory(value, path, into) {
629
669
  if (value === undefined)
630
670
  return;
631
- if (!Array.isArray(value)) {
632
- into.add("task.handlingHistory.shape", path, "handlingHistory must be an array when present");
671
+ if (!isPlainObject(value)) {
672
+ into.add("task.handlingHistory.shape", path, "handlingHistory must be an object with its steps when present");
673
+ return;
674
+ }
675
+ // What the record adds up to before this agent: each total present when the provider knows it
676
+ // and absent when it does not, never a plausible nought.
677
+ for (const field of ["handleSeconds", "holdSeconds", "queueSeconds"]) {
678
+ if (value[field] !== undefined) {
679
+ into.require(isDurationSeconds(value[field]), `task.handlingHistory.${field}`, `${path}.${field}`, `${field} must be a whole number of seconds, zero or more, or omitted when unknown`);
680
+ }
681
+ }
682
+ if (value.transfers !== undefined) {
683
+ into.require(typeof value.transfers === "number" && Number.isInteger(value.transfers) && value.transfers >= 0, "task.handlingHistory.transfers", `${path}.transfers`, "transfers must be a whole number, zero or more, or omitted when unknown");
684
+ }
685
+ if (!Array.isArray(value.steps)) {
686
+ into.add("task.handlingHistory.steps.shape", `${path}.steps`, "handlingHistory carries its steps as an array, empty when the task has had none");
633
687
  return;
634
688
  }
635
689
  let previous;
636
- value.forEach((entry, index) => {
637
- const at = `${path}[${index}]`;
690
+ value.steps.forEach((entry, index) => {
691
+ const at = `${path}.steps[${index}]`;
638
692
  if (!isPlainObject(entry)) {
639
693
  into.add("task.handlingHistory.entry", at, "each handling step must be an object");
640
694
  return;
@@ -658,6 +712,15 @@ function validateHandlingHistory(value, path, into) {
658
712
  // On `queued` nobody takes part, so there is nothing to name.
659
713
  into.require(entry.step !== "queued", "task.handlingHistory.by.unexpected", `${at}.by`, "a queued step names nobody");
660
714
  }
715
+ // A step that dialled says where to and, when a host placed it, which dial; no other step dialled.
716
+ const dialled = typeof entry.step === "string" && HANDLING_STEPS_THAT_DIAL.includes(entry.step);
717
+ for (const field of ["dialId", "destination"]) {
718
+ if (entry[field] === undefined)
719
+ continue;
720
+ if (into.filled(entry[field], `task.handlingHistory.${field}`, `${at}.${field}`, `${field} must not be empty when present`)) {
721
+ into.require(dialled, `task.handlingHistory.${field}.unexpected`, `${at}.${field}`, `${String(entry.step)} dialled nothing; ${field} belongs on transferred, conferenced, or unanswered`);
722
+ }
723
+ }
661
724
  });
662
725
  }
663
726
  /** Present only while consulting, and only on voice: elsewhere there is nobody to consult. */
@@ -670,21 +733,57 @@ function validateTaskMedia(value, channel, path, into) {
670
733
  return;
671
734
  into.oneOf(value, TASK_MEDIA_STATES, "task.media", path);
672
735
  }
673
- function validateConsultation(value, channel, path, into) {
736
+ /**
737
+ * Who is on the call, as the provider states it. Voice only. Each entry is a role with what that
738
+ * role needs and nothing another role would: a party or an agent dialled nowhere, so neither
739
+ * carries a destination; somebody consulted or conferenced came from one, so both do.
740
+ */
741
+ function validateOnCall(value, channel, path, into) {
674
742
  if (value === undefined)
675
743
  return;
676
- if (!into.require(channel === "voice", "task.consultation.channel", path, `a ${channel} task cannot carry a consultation`))
744
+ if (!into.require(channel === "voice", "task.onCall.channel", path, `a ${channel} task has nobody on a call`))
677
745
  return;
678
- if (!isPlainObject(value)) {
679
- into.add("task.consultation.shape", path, "a consultation must be an object when present");
746
+ if (!Array.isArray(value)) {
747
+ into.add("task.onCall.shape", path, "onCall is an array of who is on the call, when the provider knows");
680
748
  return;
681
749
  }
682
- into.filled(value.destination, "task.consultation.destination", `${path}.destination`, "a consultation names the destination being consulted");
683
- if (value.label !== undefined) {
684
- into.filled(value.label, "task.consultation.label", `${path}.label`, "a label must not be empty when present");
685
- }
686
- if (value.since !== undefined)
687
- into.timestamp(value.since, "task.consultation.since", `${path}.since`);
750
+ const counted = { party: 0, consulted: 0 };
751
+ value.forEach((entry, index) => {
752
+ const at = `${path}[${index}]`;
753
+ if (!isPlainObject(entry)) {
754
+ into.add("task.onCall.entry", at, "each entry on the call must be an object");
755
+ return;
756
+ }
757
+ if (!into.oneOf(entry.role, ON_CALL_ROLES, "task.onCall.role", `${at}.role`))
758
+ return;
759
+ const role = entry.role;
760
+ into.timestamp(entry.since, "task.onCall.since", `${at}.since`);
761
+ if (entry.held !== undefined) {
762
+ into.require(entry.held === true, "task.onCall.held", `${at}.held`, "held is stated by presence: send true or omit it");
763
+ }
764
+ if (role === "party" || role === "consulted")
765
+ counted[role] += 1;
766
+ if (role === "agent") {
767
+ into.require(isUserId(entry.userId), "task.onCall.userId", `${at}.userId`, "an agent on the call is named by their user id");
768
+ }
769
+ else {
770
+ into.require(entry.userId === undefined, "task.onCall.userId.unexpected", `${at}.userId`, `a ${role} is not named by a user id`);
771
+ }
772
+ if (role === "consulted" || role === "conferenced") {
773
+ into.filled(entry.destination, "task.onCall.destination", `${at}.destination`, `a ${role} entry names the destination that was dialled`);
774
+ if (entry.dialId !== undefined)
775
+ into.filled(entry.dialId, "task.onCall.dialId", `${at}.dialId`, "dialId must not be empty when present");
776
+ if (entry.label !== undefined)
777
+ into.filled(entry.label, "task.onCall.label", `${at}.label`, "a label must not be empty when present");
778
+ }
779
+ else {
780
+ for (const field of ["destination", "dialId", "label"]) {
781
+ into.require(entry[field] === undefined, `task.onCall.${field}.unexpected`, `${at}.${field}`, `a ${role} was dialled from nowhere; ${field} belongs on consulted or conferenced`);
782
+ }
783
+ }
784
+ });
785
+ into.require(counted.party <= 1, "task.onCall.party.single", path, "a call has one party; a second is somebody else's role");
786
+ into.require(counted.consulted <= 1, "task.onCall.consulted.single", path, "one consultation at a time: transfer complete and cancel name no destination because there is exactly one");
688
787
  }
689
788
  const LEAD_STAGES = membersOf({ requested: true, joined: true });
690
789
  /** The agent's request for a lead. Voice only; `joined` names the lead, `requested` cannot. */
@@ -773,7 +872,7 @@ function validateTaskInto(task, context, path, into) {
773
872
  validateBrowsers(task.browsers, `${path}.browsers`, into);
774
873
  validateTaskAttributes(task.attributes, `${path}.attributes`, into);
775
874
  validateHandlingHistory(task.handlingHistory, `${path}.handlingHistory`, into);
776
- validateConsultation(task.consultation, context.channel, `${path}.consultation`, into);
875
+ validateOnCall(task.onCall, context.channel, `${path}.onCall`, into);
777
876
  validateTaskMedia(task.media, context.channel, `${path}.media`, into);
778
877
  validateLead(task.lead, context.channel, `${path}.lead`, into);
779
878
  validateAssisting(task.assisting, context.channel, `${path}.assisting`, into);
@@ -797,6 +896,10 @@ function validateTaskInto(task, context, path, into) {
797
896
  continue;
798
897
  if (!into.require(allowed.includes(name), "task.capability.channel", `${path}.capabilities.${name}`, `a ${context.channel} task may not declare ${name}`))
799
898
  continue;
899
+ // A control that dials needs the manifest to have said how a dial ends, or its outcome has no words.
900
+ if (DIALLING_CAPABILITIES.includes(name) && context.dialOutcomesDeclared === false) {
901
+ into.add("task.capability.dialOutcomes.required", `${path}.capabilities.${name}`, `${name} dials, and the manifest declares no dialOutcomes to say how a dial ends`);
902
+ }
800
903
  // A control the queue could allow may stand locked in its place, saying whose. What the
801
904
  // queue provides -- browsers, dispositions, custom controls -- is content, not a control.
802
905
  if (isLocked(declared)) {
@@ -1032,7 +1135,7 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
1032
1135
  const seen = new Set();
1033
1136
  let assisting;
1034
1137
  snapshot.tasks.forEach((task, index) => {
1035
- validateTaskInto(task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks }, `${path}.tasks[${index}]`, into);
1138
+ validateTaskInto(task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks, dialOutcomesDeclared: manifestDials(manifest) }, `${path}.tasks[${index}]`, into);
1036
1139
  // A lead assists one call at a time.
1037
1140
  if (isPlainObject(task) && task.assisting !== undefined) {
1038
1141
  if (assisting !== undefined)
@@ -1219,7 +1322,7 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1219
1322
  validateBreakState(event.break, `${at}.break`, into);
1220
1323
  break;
1221
1324
  case "task-offered":
1222
- validateTaskInto(event.task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks }, `${at}.task`, into);
1325
+ validateTaskInto(event.task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks, dialOutcomesDeclared: manifestDials(manifest) }, `${at}.task`, into);
1223
1326
  // An offer introduces work that is not yet under way; work in progress arrives only on a snapshot.
1224
1327
  if (isPlainObject(event.task) && typeof event.task.phase === "string") {
1225
1328
  into.require(OFFERABLE_PHASES.includes(event.task.phase), "event.taskOffered.phase", `${at}.task.phase`, `task-offered introduces a task as ${OFFERABLE_PHASES.join(", ")}, never as ${event.task.phase}`);
@@ -1230,7 +1333,7 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1230
1333
  }
1231
1334
  break;
1232
1335
  case "task-updated":
1233
- validateTaskInto(event.task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks }, `${at}.task`, into);
1336
+ validateTaskInto(event.task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks, dialOutcomesDeclared: manifestDials(manifest) }, `${at}.task`, into);
1234
1337
  break;
1235
1338
  case "task-media-started":
1236
1339
  into.require(isTaskId(event.taskId), "event.taskMediaStarted.taskId", `${at}.taskId`, "a task id is required");
@@ -1242,6 +1345,21 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1242
1345
  into.require(isTaskId(event.taskId), "event.taskEnded.taskId", `${at}.taskId`, "a task id is required");
1243
1346
  validateTaskOutcome(event.outcome, `${at}.outcome`, into);
1244
1347
  break;
1348
+ case "dial-outcome": {
1349
+ into.filled(event.dialId, "event.dialOutcome.dialId", `${at}.dialId`, "an outcome names the dial it ends, by the host's dialId");
1350
+ // Only a distinction the manifest declared: an undeclared word is a guess dressed as a code.
1351
+ const declared = isPlainObject(manifest) && Array.isArray(manifest.dialOutcomes) ? manifest.dialOutcomes : undefined;
1352
+ if (into.oneOf(event.outcome, DIAL_OUTCOMES, "event.dialOutcome.outcome", `${at}.outcome`)) {
1353
+ into.require(declared !== undefined && declared.includes(event.outcome), "event.dialOutcome.undeclared", `${at}.outcome`, `the manifest does not declare ${String(event.outcome)} among its dialOutcomes`);
1354
+ }
1355
+ if (event.taskId !== undefined)
1356
+ into.require(isTaskId(event.taskId), "event.dialOutcome.taskId", `${at}.taskId`, "taskId must be a task id when present");
1357
+ if (event.destination !== undefined)
1358
+ into.filled(event.destination, "event.dialOutcome.destination", `${at}.destination`, "a destination must not be empty when present");
1359
+ if (event.reason !== undefined)
1360
+ into.filled(event.reason, "event.dialOutcome.reason", `${at}.reason`, "a reason must not be empty when present");
1361
+ break;
1362
+ }
1245
1363
  case "announcement":
1246
1364
  into.filled(event.text, "event.announcement.text", `${at}.text`, "an announcement needs text");
1247
1365
  into.timestamp(event.announcedAt, "event.announcement.announcedAt", `${at}.announcedAt`);
@@ -1400,6 +1518,35 @@ export function validateHostGuarantees(guarantees, path = "host.guarantees") {
1400
1518
  }
1401
1519
  return into.violations;
1402
1520
  }
1521
+ /**
1522
+ * What the host reports of a leg it performed, as an adapter may validate it before forwarding:
1523
+ * a task, a step, when it began, how long so far if the host says, and an explicit end that
1524
+ * carries the final duration.
1525
+ */
1526
+ export function validateHandlingReport(report, path = "handlingReport", manifest) {
1527
+ const into = new Collector();
1528
+ if (!isPlainObject(report)) {
1529
+ into.add("handlingReport.shape", path, "a handling report must be an object");
1530
+ return into.violations;
1531
+ }
1532
+ into.require(isTaskId(report.taskId), "handlingReport.taskId", `${path}.taskId`, "a report names the task");
1533
+ into.oneOf(report.step, HANDLING_STEPS, "handlingReport.step", `${path}.step`);
1534
+ into.timestamp(report.at, "handlingReport.at", `${path}.at`);
1535
+ if (report.seconds !== undefined) {
1536
+ into.require(isDurationSeconds(report.seconds) && report.seconds > 0, "handlingReport.seconds", `${path}.seconds`, "seconds must be a positive whole number; omit it rather than report nought");
1537
+ }
1538
+ if (report.ended !== undefined) {
1539
+ if (into.require(report.ended === true, "handlingReport.ended", `${path}.ended`, "ended is declared by presence, as true; a running leg omits it")) {
1540
+ into.require(report.seconds !== undefined, "handlingReport.ended.seconds", `${path}.seconds`, "an ended leg states its final duration");
1541
+ }
1542
+ }
1543
+ else if (report.seconds !== undefined && manifest !== undefined) {
1544
+ // A running report crosses only to a provider that asked for one: begin and end are the whole
1545
+ // of what the rest receive.
1546
+ into.require(isPlainObject(manifest) && manifest.runningStepReports === true, "handlingReport.running.unexpected", `${path}.seconds`, "this provider takes begin and end only; a running report was never asked for");
1547
+ }
1548
+ return into.violations;
1549
+ }
1403
1550
  export function validateHostReport(report, path = "host") {
1404
1551
  const into = new Collector();
1405
1552
  if (!isPlainObject(report)) {
@@ -1455,7 +1602,7 @@ export function validateHostReport(report, path = "host") {
1455
1602
  // failure. A method added to `Connection` without a row here is a compile error at the call site.
1456
1603
  const RESULT_STATUSES = {
1457
1604
  execute: { success: "applied", failure: "failed" },
1458
- dial: { success: "dialled", failure: "failed" },
1605
+ dial: { success: "dialling", failure: "failed" },
1459
1606
  setCapacity: { success: "applied", failure: "failed" },
1460
1607
  requestBreak: { success: "requested", failure: "failed" },
1461
1608
  commitBreak: { success: "committed", failure: "failed" },
@@ -1465,6 +1612,7 @@ const RESULT_STATUSES = {
1465
1612
  executeTeamConsult: { success: "applied", failure: "failed" },
1466
1613
  openMedia: { success: "opened", failure: "unavailable" },
1467
1614
  setPreference: { success: "applied", failure: "failed" },
1615
+ recordStep: { success: "recorded", failure: "failed" },
1468
1616
  executeTeamPolicy: { success: "applied", failure: "failed" },
1469
1617
  };
1470
1618
  /**
@@ -1473,9 +1621,12 @@ const RESULT_STATUSES = {
1473
1621
  * shows the agent what it says. A status the method does not answer, a failure status without a
1474
1622
  * failure, a success carrying one, or an `omni.` code the contract lacks are each refused.
1475
1623
  */
1476
- export function validateResult(result, method, path = "result") {
1624
+ export function validateResult(result, method, path = "result", dialId) {
1477
1625
  const into = new Collector();
1478
- const statuses = RESULT_STATUSES[method];
1626
+ // A command that dials -- `dial`, or `execute` with a command carrying a `dialId` -- answers
1627
+ // `dialling` and restates the host's identity, so the host compares and confirms.
1628
+ const dials = method === "dial" || (method === "execute" && dialId !== undefined);
1629
+ const statuses = dials ? { success: "dialling", failure: RESULT_STATUSES[method].failure } : RESULT_STATUSES[method];
1479
1630
  if (!isPlainObject(result)) {
1480
1631
  into.add("result.shape", path, `${method} must answer an object`);
1481
1632
  return into.violations;
@@ -1485,6 +1636,14 @@ export function validateResult(result, method, path = "result") {
1485
1636
  if (method === "openMedia") {
1486
1637
  into.require(isPlainObject(result.session), "result.session", `${path}.session`, "opened carries the media session");
1487
1638
  }
1639
+ if (dials) {
1640
+ if (into.filled(result.dialId, "result.dialId", `${path}.dialId`, "dialling restates the dialId the host sent") && dialId !== undefined) {
1641
+ into.require(result.dialId === dialId, "result.dialId.mismatch", `${path}.dialId`, `the host dialled ${dialId} and the provider answered for ${String(result.dialId)}`);
1642
+ }
1643
+ }
1644
+ else {
1645
+ into.require(result.dialId === undefined, "result.dialId.unexpected", `${path}.dialId`, `${statuses.success} dialled nothing and names no dial`);
1646
+ }
1488
1647
  }
1489
1648
  else if (result.status === statuses.failure) {
1490
1649
  if (result.failure === undefined) {