@xema/omni-protocol 0.1.77 → 0.1.78

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.
@@ -54,7 +54,7 @@ const BREAK_APPROVALS = membersOf({
54
54
  "not-requested": true, "awaiting-decision": true, granted: true, "starting-after-task": true, "in-effect": true,
55
55
  });
56
56
  const TEAM_AVAILABILITIES = membersOf({
57
- ready: true, "on-task": true, "on-break": true, "signed-out": true,
57
+ ready: true, "on-task": true, "on-break": true, reserved: true, "signed-out": true,
58
58
  });
59
59
  const HANDLING_STEPS = membersOf({
60
60
  queued: true, offered: true, answered: true, held: true, muted: true, transferred: true, conferenced: true, unanswered: true,
@@ -82,7 +82,7 @@ const MEMBER_BREAKS = membersOf({
82
82
  "awaiting-decision": true, granted: true, "starting-after-task": true,
83
83
  });
84
84
  const OFFERABLE_PHASES = membersOf({
85
- pending: true, confirmed: true, preview: true,
85
+ pending: true,
86
86
  });
87
87
  const PREVIEW_DEADLINES = membersOf({ calls: true, expires: true });
88
88
  const PHONES = membersOf({ softphone: true, deskPhone: true });
@@ -279,9 +279,8 @@ function validateIdleCapabilities(value, channel, path, into) {
279
279
  }
280
280
  else {
281
281
  validateBrowserAccess(browser.access, `${path}.personalBrowser.access`, into);
282
- if (browser.accessAppliesTo !== undefined) {
283
- into.oneOf(browser.accessAppliesTo, ACCESS_APPLIES_TO, "manifest.personalBrowser.accessAppliesTo", `${path}.personalBrowser.accessAppliesTo`);
284
- }
282
+ // Stated, never defaulted: an absent value read as all-navigation was a second spelling of one fact.
283
+ into.oneOf(browser.accessAppliesTo, ACCESS_APPLIES_TO, "manifest.personalBrowser.accessAppliesTo", `${path}.personalBrowser.accessAppliesTo`);
285
284
  }
286
285
  }
287
286
  if (value.dial !== undefined) {
@@ -568,8 +567,7 @@ function validateCustomCapabilities(value, path, into) {
568
567
  into.oneOf(custom.ui.control, CUSTOM_UI_CONTROLS, "task.custom.ui.control", `${at}.ui.control`);
569
568
  into.filled(custom.ui.label, "task.custom.ui.label", `${at}.ui.label`, "a custom control needs a label");
570
569
  into.oneOf(custom.ui.placement, CUSTOM_UI_PLACEMENTS, "task.custom.ui.placement", `${at}.ui.placement`);
571
- if (custom.ui.render !== undefined)
572
- into.oneOf(custom.ui.render, CUSTOM_RENDERS, "task.custom.ui.render", `${at}.ui.render`);
570
+ into.oneOf(custom.ui.render, CUSTOM_RENDERS, "task.custom.ui.render", `${at}.ui.render`);
573
571
  if (custom.prompt !== undefined) {
574
572
  if (!isPlainObject(custom.prompt) || !Array.isArray(custom.prompt.fields)) {
575
573
  into.add("task.custom.prompt.shape", `${at}.prompt`, "a prompt is an object with the fields the agent fills");
@@ -661,8 +659,8 @@ function validateLockedInto(value, rule, path, levels, into) {
661
659
  }
662
660
  /** What every resolved value carries: who set it, and who locked it if anyone. */
663
661
  function validateResolvedInto(value, rule, path, levels, into) {
664
- if (into.filled(value.setBy, `${rule}.setBy`, `${path}.setBy`, "setBy names who stated the value: a level, or provisioning")) {
665
- into.require(value.setBy === "provider" || levelIds(levels).includes(value.setBy), `${rule}.setBy.unknown`, `${path}.setBy`, `${String(value.setBy)} is neither provisioning nor a level this manifest declares`);
662
+ if (into.filled(value.setBy, `${rule}.setBy`, `${path}.setBy`, "setBy names who stated the value: a level, or provider")) {
663
+ into.require(value.setBy === "provider" || levelIds(levels).includes(value.setBy), `${rule}.setBy.unknown`, `${path}.setBy`, `${String(value.setBy)} is neither provider nor a level this manifest declares`);
666
664
  }
667
665
  if (value.lockedBy !== undefined)
668
666
  validateLockedByInto(value.lockedBy, `${rule}.lockedBy`, `${path}.lockedBy`, levels, into);
@@ -859,7 +857,11 @@ function validateHandlingHistory(value, path, into, task = {}) {
859
857
  /** Present only while consulting, and only on voice: elsewhere there is nobody to consult. */
860
858
  const TASK_MEDIA_STATES = membersOf({ started: true, ended: true });
861
859
  /** Real-time media is a voice affair, and its state is one of two words. */
862
- function validateTaskMedia(value, channel, phase, path, into) {
860
+ /** Whether a host dial is ringing the party: the party entry carries `stage: "ringing"` and the host's `dialId`. */
861
+ function partyRingingByHost(task) {
862
+ return Array.isArray(task.onCall) && task.onCall.some(entry => isPlainObject(entry) && entry.role === "party" && entry.stage === "ringing" && typeof entry.dialId === "string");
863
+ }
864
+ function validateTaskMedia(task, value, channel, phase, path, into) {
863
865
  if (value === undefined)
864
866
  return;
865
867
  if (!into.require(channel === "voice", "task.media.channel", path, `a ${channel} task carries no real-time media state`))
@@ -867,7 +869,9 @@ function validateTaskMedia(value, channel, phase, path, into) {
867
869
  if (into.oneOf(value, TASK_MEDIA_STATES, "task.media", path)) {
868
870
  // Nothing is acquired while pending, and a preview has placed no call: media names a task whose
869
871
  // work has begun, on a snapshot as on the event, or a host would open the microphone on an offer.
870
- into.require(!WORK_NOT_BEGUN.includes(phase), "task.media.beforeWork", path, `a ${String(phase)} task has no media: audio arrives once its work has begun`);
872
+ // The one task not yet at work that has audio is one whose party the host is dialling: ring-back
873
+ // is audio, and every host-placed dial's media starts on dialling.
874
+ into.require(!WORK_NOT_BEGUN.includes(phase) || partyRingingByHost(task), "task.media.beforeWork", path, `a ${String(phase)} task has no media: audio arrives once its work has begun, or once the host is dialling its party`);
871
875
  }
872
876
  }
873
877
  const WORK_NOT_BEGUN = ["pending", "confirmed", "preview"];
@@ -1100,7 +1104,7 @@ function validateTaskInto(task, context, path, into) {
1100
1104
  if (Array.isArray(task.onCall) && task.onCall.length > 0 && (task.phase === "completing" || task.media === "ended")) {
1101
1105
  into.add("task.onCall.ended", `${path}.onCall`, `the call has ended (${task.phase === "completing" ? "the task is completing" : "its media ended"}) and onCall still names people on it: the room is who is on the call now, and now nobody is`);
1102
1106
  }
1103
- validateTaskMedia(task.media, context.channel, task.phase, `${path}.media`, into);
1107
+ validateTaskMedia(task, task.media, context.channel, task.phase, `${path}.media`, into);
1104
1108
  validateLeadAssist(task.leadAssist, context.channel, `${path}.leadAssist`, into);
1105
1109
  validateAssisting(task.assisting, context.channel, `${path}.assisting`, into);
1106
1110
  validateMonitoring(task.monitoring, context.channel, `${path}.monitoring`, into);
@@ -1217,7 +1221,9 @@ function validateBreakState(value, path, into) {
1217
1221
  if (value.imposed !== undefined) {
1218
1222
  validateImposedBreak(value.imposed, `${path}.imposed`, into);
1219
1223
  // An imposed break is a break somebody placed; beside `not-requested` there is no break.
1220
- into.require(value.approval !== "not-requested", "break.imposed.approval", `${path}.imposed`, "an imposed break is a break in progress; not-requested says there is none");
1224
+ // An imposed break is a break in progress or about to be: it travels with in-effect or
1225
+ // starting-after-task and nothing else. Beside granted, the host would commit a break nobody asked for.
1226
+ into.require(value.approval === "in-effect" || value.approval === "starting-after-task", "break.imposed.approval", `${path}.imposed`, `an imposed break is in effect or starting after the task; ${String(value.approval)} says the agent asked, or that there is none`);
1221
1227
  }
1222
1228
  if (value.reasons === undefined)
1223
1229
  return;
@@ -1252,6 +1258,13 @@ function validateBreakState(value, path, into) {
1252
1258
  if (typeof value.activeReasonId === "string" && value.activeReasonId.length > 0) {
1253
1259
  into.require(seen.has(value.activeReasonId), "break.activeReasonId.known", `${path}.activeReasonId`, `activeReasonId names a reason the provider did not publish: ${value.activeReasonId}`);
1254
1260
  }
1261
+ // A break in effect, or about to be, on a provider that publishes reasons is on one of them: an
1262
+ // imposed one included, since the lead's place named it. A break of no kind is a break whose
1263
+ // rules -- who may monitor through it, whether it counts -- nobody can apply.
1264
+ ruleEvaluated("break.activeReasonId.required");
1265
+ if (seen.size > 0 && (value.approval === "in-effect" || value.approval === "starting-after-task")) {
1266
+ into.require(typeof value.activeReasonId === "string" && value.activeReasonId.length > 0, "break.activeReasonId.required", `${path}.activeReasonId`, `a break ${String(value.approval)} on a provider that publishes reasons names the one it is on`);
1267
+ }
1255
1268
  }
1256
1269
  export function validateTeamRoster(roster, path = "team", context = {}) {
1257
1270
  const into = new Collector();
@@ -1408,6 +1421,14 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
1408
1421
  // describes a state the agent cannot be in, whichever half is stale. The one exception is a
1409
1422
  // lead listening to a call during a break that is work of another sort -- coaching,
1410
1423
  // administrative, training -- and on no other.
1424
+ // The converse: a committed break with nothing outstanding has begun. starting-after-task beside
1425
+ // no task is a break waiting on work that does not exist, and an empty list reading as "still
1426
+ // finishing" is the plausible nought.
1427
+ ruleEvaluated("break.starting-after-task.tasks");
1428
+ if (isPlainObject(snapshot.break) && snapshot.break.approval === "starting-after-task"
1429
+ && Array.isArray(snapshot.tasks) && snapshot.tasks.length === 0) {
1430
+ into.add("break.starting-after-task.tasks", `${path}.break.approval`, "a break starting after the task waits on a task, and the snapshot carries none: with nothing outstanding the break is in-effect");
1431
+ }
1411
1432
  if (isPlainObject(snapshot.break) && snapshot.break.approval === "in-effect"
1412
1433
  && Array.isArray(snapshot.tasks) && snapshot.tasks.length > 0) {
1413
1434
  const listening = snapshot.tasks.every((task) => isPlainObject(task) && task.monitoring !== undefined);
@@ -1487,6 +1508,10 @@ function validateTaskOutcome(value, path, into) {
1487
1508
  into.filled(value.destinationId, "event.taskEnded.outcome.transferred", `${path}.destinationId`, "a destinationId must not be empty when present");
1488
1509
  }
1489
1510
  break;
1511
+ case "taken-over":
1512
+ // A lead is not a directory item: the ending names the lead, and the host resolves the name.
1513
+ into.require(isUserId(value.leadId), "event.taskEnded.outcome.takenOver", `${path}.leadId`, "a take-over names the lead who took the call, by user id");
1514
+ break;
1490
1515
  case "cancelled":
1491
1516
  // One word covered the agent declining, the provider withdrawing and the party abandoning the
1492
1517
  // ring; a record that cannot tell them apart is not a record. by says who, as completed does.
@@ -2186,7 +2211,8 @@ export function validateTaskCommand(command, task, path = "command") {
2186
2211
  const RESULT_STATUSES = {
2187
2212
  execute: { success: "applied", failure: "failed" },
2188
2213
  dial: { success: "dialling", failure: "failed" },
2189
- setCapacity: { success: "applied", failure: "failed" },
2214
+ // A statement, not a request: taken, never refused.
2215
+ setCapacity: { success: "applied", failure: undefined },
2190
2216
  requestBreak: { success: "requested", failure: "failed" },
2191
2217
  commitBreak: { success: "committed", failure: "failed" },
2192
2218
  cancelBreak: { success: "cancelled", failure: "failed" },
@@ -2229,7 +2255,7 @@ export function validateResult(result, method, path = "result", dialId) {
2229
2255
  into.require(result.dialId === undefined, "result.dialId.unexpected", `${path}.dialId`, `${statuses.success} dialled nothing and names no dial`);
2230
2256
  }
2231
2257
  }
2232
- else if (result.status === statuses.failure) {
2258
+ else if (statuses.failure !== undefined && result.status === statuses.failure) {
2233
2259
  if (result.failure === undefined) {
2234
2260
  into.add("result.failure.required", `${path}.failure`, `${statuses.failure} carries the failure that says why`);
2235
2261
  }