@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.
- package/dist/index.d.ts +49 -30
- package/dist/index.js +8 -3
- package/dist/testing.d.ts +6 -2
- package/dist/testing.js +220 -107
- package/dist/validation.js +41 -15
- package/guide.md +115 -72
- package/package.json +1 -1
package/dist/validation.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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
|
|
665
|
-
into.require(value.setBy === "provider" || levelIds(levels).includes(value.setBy), `${rule}.setBy.unknown`, `${path}.setBy`, `${String(value.setBy)} is neither
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|