@xema/omni-protocol 0.1.55 → 0.1.56

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/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Host, type HostGuarantees, type HostReport, type Manifest, type ProviderEvent } from "./index.js";
1
+ import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type Task, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Host, type HostGuarantees, type HostReport, type Manifest, type ProviderEvent } from "./index.js";
2
2
  import { type ProtocolViolation } from "./validation.js";
3
3
  export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation } from "./validation.js";
4
4
  /**
@@ -84,6 +84,20 @@ export declare function assertCommandRefusedAfterWithdrawal(result: {
84
84
  code: string;
85
85
  };
86
86
  }): void;
87
+ /**
88
+ * A task's capabilities are current, not fixed. A provider whose platform withdraws a permission
89
+ * mid-task republishes the task with the set as it now stands, and a command under the withdrawn
90
+ * capability is refused from then on. `tasks` is what the provider published for one task, first
91
+ * to last -- the offer, then each republish -- every one validated against the manifest, all the
92
+ * same id, with at least one capability offered by the first absent from the last. A locked
93
+ * control is present without permission, not withdrawn: it is still drawn, so it does not count.
94
+ * `command` is the command a host issues under a withdrawn capability: it must be clean against
95
+ * the first task and refused against the last for want of that capability and for nothing else.
96
+ * That pairing is what proves the refusal comes from the withdrawal rather than from a command
97
+ * that was never issuable. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's
98
+ * answer to the same command.
99
+ */
100
+ export declare function assertTaskCapabilityWithdrawal(tasks: readonly Task[], manifest: Manifest, command: Record<string, unknown>): void;
87
101
  /**
88
102
  * Throws unless the run reached every subject named: the paired assertion beside a clean result,
89
103
  * so a fixture that never produced a roster cannot pass a test that meant to check one.
package/dist/testing.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { browserSessionKey, effectiveLevels, sameCapabilities, } from "./index.js";
2
- import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateManifest, validateResult, validateSnapshot, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
2
+ import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateManifest, validateResult, validateSnapshot, validateTask, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
3
3
  export { ProtocolConformanceError, assertNoViolations } from "./validation.js";
4
4
  /**
5
5
  * A part of the contract a run may never reach: state nothing obliges an adapter to publish, so
@@ -575,6 +575,54 @@ export function assertCommandRefusedAfterWithdrawal(result) {
575
575
  throw new Error(`A command after its capability was withdrawn fails with omni.capability-not-enabled, not ${result.failure?.code}`);
576
576
  }
577
577
  }
578
+ /**
579
+ * A task's capabilities are current, not fixed. A provider whose platform withdraws a permission
580
+ * mid-task republishes the task with the set as it now stands, and a command under the withdrawn
581
+ * capability is refused from then on. `tasks` is what the provider published for one task, first
582
+ * to last -- the offer, then each republish -- every one validated against the manifest, all the
583
+ * same id, with at least one capability offered by the first absent from the last. A locked
584
+ * control is present without permission, not withdrawn: it is still drawn, so it does not count.
585
+ * `command` is the command a host issues under a withdrawn capability: it must be clean against
586
+ * the first task and refused against the last for want of that capability and for nothing else.
587
+ * That pairing is what proves the refusal comes from the withdrawal rather than from a command
588
+ * that was never issuable. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's
589
+ * answer to the same command.
590
+ */
591
+ export function assertTaskCapabilityWithdrawal(tasks, manifest, command) {
592
+ const first = tasks[0];
593
+ const last = tasks.at(-1);
594
+ if (tasks.length < 2 || first === undefined || last === undefined) {
595
+ throw new Error("Task capability withdrawal needs the task as offered and at least one republish of it");
596
+ }
597
+ const context = {
598
+ channel: manifest.channel,
599
+ levels: effectiveLevels(manifest.orgLevels).map(level => level.id),
600
+ dialOutcomesDeclared: manifest.dialOutcomes !== undefined,
601
+ };
602
+ tasks.forEach((task, index) => {
603
+ assertNoViolations(validateTask(task, context, `tasks[${index}]`), `Task capability withdrawal: tasks[${index}] must be a task the wire could carry`);
604
+ if (task.id !== first.id) {
605
+ throw new Error(`Task capability withdrawal must keep the task: tasks[${index}] is ${task.id}, and the offer was ${first.id}`);
606
+ }
607
+ });
608
+ const withdrawn = Object.keys(first.capabilities).filter(name => last.capabilities[name] === undefined);
609
+ if (withdrawn.length === 0) {
610
+ throw new Error("Task capability withdrawal must end with at least one capability the offer declared withdrawn; a locked control is present without permission, not withdrawn");
611
+ }
612
+ const before = validateTaskCommand(command, first, "command");
613
+ if (before.length > 0) {
614
+ throw new Error(`Task capability withdrawal: the command must be issuable against the task as offered, or its later refusal proves nothing (${before.map(v => v.rule).join(", ")})`);
615
+ }
616
+ const after = validateTaskCommand(command, last, "command");
617
+ const forWant = after.filter(v => withdrawn.some(name => v.rule === `command.capability.${name}`));
618
+ if (forWant.length === 0) {
619
+ throw new Error(`Task capability withdrawal: the command must be refused against the republished task for want of ${withdrawn.join(" or ")}; it was ${after.length === 0 ? "accepted" : `refused for ${after.map(v => v.rule).join(", ")}`}`);
620
+ }
621
+ const other = after.filter(v => !forWant.includes(v));
622
+ if (other.length > 0) {
623
+ throw new Error(`Task capability withdrawal: the republished task refuses the command for more than the withdrawal (${other.map(v => v.rule).join(", ")}); change one thing at a time`);
624
+ }
625
+ }
578
626
  /**
579
627
  * Throws unless the run reached every subject named: the paired assertion beside a clean result,
580
628
  * so a fixture that never produced a roster cannot pass a test that meant to check one.
package/guide.md CHANGED
@@ -2664,6 +2664,44 @@ browser that used it.
2664
2664
  Task capabilities belong to each `Task`. If `hold` is omitted on one task, Omni
2665
2665
  must not show or issue hold for that task even if another task from the same provider supports it.
2666
2666
 
2667
+ **A capability is a property of the task, and of nothing the task names.** A provider derives it
2668
+ from no other source -- not the queue the task came from, not the login, not its own configuration
2669
+ -- because each of those answers a question about this task from somewhere that does not know
2670
+ about this task, and a client with two sources and a rule for choosing between them is the shape
2671
+ that produces two consumers disagreeing about one fact. The task is the one source, and the
2672
+ provider puts on the task what the platform permits for it.
2673
+
2674
+ **A permission that changes while the task is open is republished on the task at the moment it
2675
+ changes.** An agent on a billing dispute may refund up to their own limit and no further. The
2676
+ customer wants more, the agent asks for a lead, and the moment the lead joins the call the task is
2677
+ republished with the Refund control the agent could not have a minute ago; when the lead leaves, it
2678
+ is republished without it. Nothing about the agent changed -- who is on the call did -- and the
2679
+ task said so both times, at the moment it became true. A call moves queues the same way: a caller
2680
+ identified as a priority customer is transferred to the priority queue, and the task arrives under
2681
+ that queue's terms -- a longer wrap allowance, a discount control the general queue never offered --
2682
+ restated on the task at the hand-over, not inferred from the queue it left. A capability stated
2683
+ once at offer or answer and never corrected is a fact with a shelf life and no expiry, and a host
2684
+ draws a control the provider will now refuse -- or withholds one the agent now has. So the provider republishes
2685
+ the task, with its capabilities as they now stand, and a host treats the last statement as current
2686
+ rather than re-deriving anything. This is the same shape as a room left full after the call ends
2687
+ -- a field describing the present, carried past the moment it stopped being true -- carried past
2688
+ this time not by a spread but by nobody sending the correction. A provider whose platform can
2689
+ change a permission mid-task and does not republish is in breach, however conformant its offer was.
2690
+ A capability set can shrink as well as grow, and a host that has only ever seen it grow meets a
2691
+ control that was there a moment ago and is gone; a command that arrives after its capability was
2692
+ withdrawn is refused with a reason, never acted on. The asymmetry decides which direction a
2693
+ provider gets right first: a control gained late is a nicety, a control withdrawn and not
2694
+ republished is a button that fails when pressed. `assertTaskCapabilityWithdrawal` is the test for that
2695
+ direction: the task as offered and as republished, and one command that was issuable under the
2696
+ first and is refused under the last for want of the capability withdrawn -- and nothing else.
2697
+
2698
+ **An empty capability set is a statement, not a shrug.** `capabilities: {}` says the platform
2699
+ permits nothing on this task. A provider that has not yet learned what the platform permits -- a
2700
+ queue's configuration that has not reached it -- knows nothing of the kind, and must not publish
2701
+ the task as if it did: it holds the task until it knows, or reports the gap as a `diagnostic`,
2702
+ because "no queue governs this call" is a fact and "the configuration has not arrived" is a fault,
2703
+ and a host cannot tell them apart from a value that carries neither.
2704
+
2667
2705
  ```ts
2668
2706
  const taskCapabilities = {
2669
2707
  channel: "voice",
@@ -4401,6 +4439,7 @@ cannot be established from TypeScript structure alone.
4401
4439
  | Helper | Contract checked |
4402
4440
  | --- | --- |
4403
4441
  | `assertCapabilityWithdrawal(states, snapshot, manifest)` | A capability withdrawn by a later `authenticated` state is gone from the next snapshot: no roster for a login that no longer leads, no requests for one that may no longer join. Every state is validated on the way, `refreshing` must carry the login over, and the sequence passes only through usable states. |
4442
+ | `assertTaskCapabilityWithdrawal(tasks, manifest, command)` | A capability withdrawn by a republish of the task is gone from the task: every task in the sequence is validated, all carry the offer's id, at least one capability the offer declared is absent at the end (a locked control is present, not withdrawn), and `command` is clean against the first task and refused against the last for want of a withdrawn capability and nothing else. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's answer. |
4404
4443
  | `assertCommandRefusedAfterWithdrawal(result)` | A command that arrives after its capability was withdrawn fails with `omni.capability-not-enabled`, named by the provider. The same assertion serves a command the provider never supported under a capability it declares. |
4405
4444
  | `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |
4406
4445
  | `assertAuthenticationRestoreAndExpiry(states)` | A restored authenticated session can refresh and ends in expiry. Every state is validated. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.55",
3
+ "version": "0.1.56",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",