@xema/omni-protocol 0.1.13 → 0.1.15

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/README.md CHANGED
@@ -30,7 +30,8 @@ An adapter is loaded from a separate package and may be compiled against a diffe
30
30
  version, so its output is untrusted input. Every validator takes `unknown` and returns every
31
31
  violation it found rather than throwing on the first, so a caller reports all of them at once.
32
32
  Validating a snapshot before it replaces provider state is what stops a malformed task reaching
33
- the agent's workspace.
33
+ the agent's workspace; validating a result with `validateResult(result, method)` before acting on
34
+ it is what stops a status the host does not know being shown as an outcome.
34
35
 
35
36
  ```ts
36
37
  const violations = validateSnapshot(snapshot, manifest);
@@ -70,7 +71,8 @@ expect(result.disconnectWasClean).toBe(true);
70
71
  ```
71
72
 
72
73
  Run the contract scenarios beside it — authentication restore and expiry, capability withdrawal,
73
- reconnect with missed assignments, break denial and retry, wrap timeout, browser isolation.
74
+ reconnect with missed assignments, break denial and retry, a break asked for on a task, who a
75
+ break asks, wrap timeout, browser isolation.
74
76
 
75
77
  > **Assert both directions.** Every helper rejects a violating input as well as accepting a
76
78
  > conforming one. A suite that only asserts "this conforming case does not throw" passes unchanged
package/dist/testing.d.ts CHANGED
@@ -95,6 +95,33 @@ export declare function assertReconnectWithMissedAssignments<C extends Channel>(
95
95
  * both.
96
96
  */
97
97
  export declare function assertDeniedAndRetriedBreak(approvals: readonly BreakApproval[]): void;
98
+ /** One provider as the host sees it when freezing a break attempt's participant set. */
99
+ export interface BreakCandidate {
100
+ id: string;
101
+ authentication: AuthenticationState["status"];
102
+ /** Whether the agent can currently receive work from it: connected, with a capacity stated. */
103
+ holdsCapacity: boolean;
104
+ }
105
+ /**
106
+ * The participant set of a break attempt is every connected provider from which the agent can
107
+ * currently receive work. A provider whose login is not usable -- `expired` above all -- is not
108
+ * one, whatever else is true of it: nothing can be asked of it, and a host that waits on it
109
+ * stalls the break for everyone. A usable provider holding capacity is one, and cannot be left
110
+ * out. `refreshing` is usable: identity and capabilities remain available and work continues.
111
+ */
112
+ export declare function assertBreakParticipants(candidates: readonly BreakCandidate[], participants: readonly string[]): void;
113
+ /** One published moment of a break asked for on a task: the approval, and how many tasks were outstanding. */
114
+ export interface BreakOnTaskStep {
115
+ approval: BreakApproval;
116
+ outstanding: number;
117
+ }
118
+ /**
119
+ * A break asked for on a task begins when the work ends. `steps` is the sequence the provider
120
+ * published, first to last: the request is made while work is outstanding, the commit is reported
121
+ * as `starting-after-task` while it remains, and `in-effect` arrives only once nothing is
122
+ * outstanding -- never beside a task, and never later than the step that has none.
123
+ */
124
+ export declare function assertBreakBeginsAfterTask(steps: readonly BreakOnTaskStep[]): void;
98
125
  /**
99
126
  * Validates the deadline derived from media end and the task's fixed wrap allowance.
100
127
  *
package/dist/testing.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { browserSessionKey, sameCapabilities, } from "./index.js";
2
- import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateManifest, validateSnapshot, } from "./validation.js";
2
+ import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateManifest, validateResult, validateSnapshot, } 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
@@ -245,7 +245,10 @@ export async function exerciseAdapter(adapter, context, options = {}) {
245
245
  // Capacity is stated, not requested: nothing may be allocated until it is, so a connection
246
246
  // that will not accept one is a connection nothing can be given to.
247
247
  const capacity = await connection.setCapacity({ count: 1 });
248
- if (capacity.status === "failed") {
248
+ const malformed = validateResult(capacity, "setCapacity", "connection.setCapacity");
249
+ violations.push(...malformed);
250
+ // A refusal is read only from a result that has the shape of one.
251
+ if (malformed.length === 0 && capacity.status === "failed") {
249
252
  violations.push({
250
253
  rule: "connection.setCapacity.failed",
251
254
  path: "connection.setCapacity",
@@ -487,6 +490,58 @@ export function assertDeniedAndRetriedBreak(approvals) {
487
490
  throw new Error(`Break retry scenario must end granted or in effect, ended ${String(last)}`);
488
491
  }
489
492
  }
493
+ const usableLogin = (status) => status === "authenticated" || status === "refreshing";
494
+ /**
495
+ * The participant set of a break attempt is every connected provider from which the agent can
496
+ * currently receive work. A provider whose login is not usable -- `expired` above all -- is not
497
+ * one, whatever else is true of it: nothing can be asked of it, and a host that waits on it
498
+ * stalls the break for everyone. A usable provider holding capacity is one, and cannot be left
499
+ * out. `refreshing` is usable: identity and capabilities remain available and work continues.
500
+ */
501
+ export function assertBreakParticipants(candidates, participants) {
502
+ const chosen = new Set(participants);
503
+ for (const id of participants) {
504
+ if (!candidates.some(candidate => candidate.id === id))
505
+ throw new Error(`${id} is not a provider the host knows`);
506
+ }
507
+ for (const candidate of candidates) {
508
+ const expected = usableLogin(candidate.authentication) && candidate.holdsCapacity;
509
+ if (expected && !chosen.has(candidate.id)) {
510
+ throw new Error(`${candidate.id} can give the agent work and must be a participant`);
511
+ }
512
+ if (!expected && chosen.has(candidate.id)) {
513
+ const why = usableLogin(candidate.authentication) ? "holds no capacity" : `is ${candidate.authentication}`;
514
+ throw new Error(`${candidate.id} ${why} and is not a participant: nothing can be asked of it`);
515
+ }
516
+ }
517
+ }
518
+ /**
519
+ * A break asked for on a task begins when the work ends. `steps` is the sequence the provider
520
+ * published, first to last: the request is made while work is outstanding, the commit is reported
521
+ * as `starting-after-task` while it remains, and `in-effect` arrives only once nothing is
522
+ * outstanding -- never beside a task, and never later than the step that has none.
523
+ */
524
+ export function assertBreakBeginsAfterTask(steps) {
525
+ const asked = steps.findIndex(step => step.approval === "awaiting-decision" || step.approval === "granted");
526
+ if (asked < 0)
527
+ throw new Error("Break-on-task scenario requires a request");
528
+ if ((steps[asked]?.outstanding ?? 0) < 1)
529
+ throw new Error("Break-on-task scenario requires the request to be made while a task is outstanding");
530
+ const committed = steps.findIndex((step, index) => index > asked && step.approval === "starting-after-task");
531
+ if (committed < 0)
532
+ throw new Error("A break committed on a task is reported as starting-after-task while the work remains");
533
+ steps.forEach((step, index) => {
534
+ if (step.approval === "in-effect" && step.outstanding > 0) {
535
+ throw new Error(`steps[${index}] reports in-effect with ${step.outstanding} task(s) outstanding: a break begins when the work ends`);
536
+ }
537
+ if (step.approval === "starting-after-task" && step.outstanding < 1) {
538
+ throw new Error(`steps[${index}] reports starting-after-task with nothing outstanding: the break should have begun`);
539
+ }
540
+ });
541
+ if (steps.at(-1)?.approval !== "in-effect") {
542
+ throw new Error(`Break-on-task scenario must end in effect, ended ${String(steps.at(-1)?.approval)}`);
543
+ }
544
+ }
490
545
  /**
491
546
  * Validates the deadline derived from media end and the task's fixed wrap allowance.
492
547
  *
@@ -34,4 +34,13 @@ export interface ReaderContext {
34
34
  export declare function validateTeamRoster(roster: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
35
35
  export declare function validateSnapshot(snapshot: unknown, manifest: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
36
36
  export declare function validateEventEnvelope(envelope: unknown, manifest: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
37
+ /** The connection methods whose results `validateResult` knows. */
38
+ export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeamBreak" | "executeTeamConsult" | "openMedia";
39
+ /**
40
+ * Validates what a connection method answered. A result is untrusted for the same reason a
41
+ * snapshot is: it comes from an adapter that may be compiled against another version, and Omni
42
+ * shows the agent what it says. A status the method does not answer, a failure status without a
43
+ * failure, a success carrying one, or an `omni.` code the contract lacks are each refused.
44
+ */
45
+ export declare function validateResult(result: unknown, method: ResultMethod, path?: string): ProtocolViolation[];
37
46
  export declare function validateAuthenticationState(state: unknown, path?: string): ProtocolViolation[];
@@ -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_SUPPORTED_PROTOCOL_VERSIONS, negotiateProtocolVersion, } from "./index.js";
12
+ import { ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, IDLE_CAPABILITIES, OMNI_FAILURE_CODES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, negotiateProtocolVersion, } from "./index.js";
13
13
  export class ProtocolConformanceError extends Error {
14
14
  violations;
15
15
  constructor(violations, summary = "Adapter violates the Omni protocol") {
@@ -808,6 +808,12 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
808
808
  }
809
809
  });
810
810
  }
811
+ // A break in effect begins when the work ends, so it holds no task. A snapshot reporting both
812
+ // describes a state the agent cannot be in, whichever half is stale.
813
+ if (isPlainObject(snapshot.break) && snapshot.break.approval === "in-effect"
814
+ && Array.isArray(snapshot.tasks) && snapshot.tasks.length > 0) {
815
+ into.add("break.in-effect.tasks", `${path}.tasks`, "a break in effect holds no task: it begins when the work ends, and until then the state is starting-after-task");
816
+ }
811
817
  // Presence is the permission, and it cuts both ways: data a provider never declared a
812
818
  // capability for is data Omni would show against a control the agent does not have.
813
819
  const idle = isPlainObject(manifest) && isPlainObject(manifest.idleCapabilities) ? manifest.idleCapabilities : {};
@@ -889,9 +895,7 @@ function validateTaskOutcome(value, path, into) {
889
895
  into.add("event.taskEnded.outcome.failed", `${path}.failure`, "a failed outcome must carry a failure");
890
896
  }
891
897
  else {
892
- into.filled(value.failure.code, "failure.code", `${path}.failure.code`, "a failure needs a code");
893
- into.filled(value.failure.message, "failure.message", `${path}.failure.message`, "a failure needs a message");
894
- into.require(typeof value.failure.retryable === "boolean", "failure.retryable", `${path}.failure.retryable`, "a failure must say whether it is retryable");
898
+ validateFailureInto(value.failure, `${path}.failure`, into);
895
899
  }
896
900
  break;
897
901
  default:
@@ -1013,6 +1017,75 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1013
1017
  return into.violations;
1014
1018
  }
1015
1019
  // ---------------------------------------------------------------------------
1020
+ // Results. A result crosses the same boundary a snapshot does, from an adapter that may be
1021
+ // compiled against another version, and Omni shows the agent what it says.
1022
+ // ---------------------------------------------------------------------------
1023
+ /** A `ProtocolFailure`, wherever one appears: on a result, or on a task's failed outcome. */
1024
+ function validateFailureInto(value, path, into) {
1025
+ if (!isPlainObject(value)) {
1026
+ into.add("failure.shape", path, "a failure must be an object");
1027
+ return;
1028
+ }
1029
+ if (into.filled(value.code, "failure.code", `${path}.code`, "a failure needs a code")) {
1030
+ // A provider names its own codes freely; the `omni.` namespace is the contract's, and a code
1031
+ // in it that the contract lacks is one Omni would show without knowing what it means.
1032
+ if (value.code.startsWith("omni.")) {
1033
+ into.require(OMNI_FAILURE_CODES.includes(value.code), "failure.code.unknown", `${path}.code`, `not a contract failure code: ${value.code}`);
1034
+ }
1035
+ }
1036
+ into.filled(value.message, "failure.message", `${path}.message`, "a failure needs a message");
1037
+ into.require(typeof value.retryable === "boolean", "failure.retryable", `${path}.retryable`, "a failure must say whether it is retryable");
1038
+ if (value.retryAfterMs !== undefined) {
1039
+ into.require(typeof value.retryAfterMs === "number" && Number.isFinite(value.retryAfterMs) && value.retryAfterMs >= 0, "failure.retryAfterMs", `${path}.retryAfterMs`, "retryAfterMs must be a non-negative number when present");
1040
+ }
1041
+ }
1042
+ // Pinned to the result unions: each method's one success status, and the status that carries a
1043
+ // failure. A method added to `Connection` without a row here is a compile error at the call site.
1044
+ const RESULT_STATUSES = {
1045
+ execute: { success: "applied", failure: "failed" },
1046
+ dial: { success: "dialled", failure: "failed" },
1047
+ setCapacity: { success: "accepted", failure: "failed" },
1048
+ requestBreak: { success: "requested", failure: "failed" },
1049
+ commitBreak: { success: "committed", failure: "failed" },
1050
+ cancelBreak: { success: "cancelled", failure: "failed" },
1051
+ endBreak: { success: "ended", failure: "failed" },
1052
+ executeTeamBreak: { success: "applied", failure: "failed" },
1053
+ executeTeamConsult: { success: "applied", failure: "failed" },
1054
+ openMedia: { success: "opened", failure: "unavailable" },
1055
+ };
1056
+ /**
1057
+ * Validates what a connection method answered. A result is untrusted for the same reason a
1058
+ * snapshot is: it comes from an adapter that may be compiled against another version, and Omni
1059
+ * shows the agent what it says. A status the method does not answer, a failure status without a
1060
+ * failure, a success carrying one, or an `omni.` code the contract lacks are each refused.
1061
+ */
1062
+ export function validateResult(result, method, path = "result") {
1063
+ const into = new Collector();
1064
+ const statuses = RESULT_STATUSES[method];
1065
+ if (!isPlainObject(result)) {
1066
+ into.add("result.shape", path, `${method} must answer an object`);
1067
+ return into.violations;
1068
+ }
1069
+ if (result.status === statuses.success) {
1070
+ into.require(result.failure === undefined, "result.failure.unexpected", `${path}.failure`, `${statuses.success} carries no failure`);
1071
+ if (method === "openMedia") {
1072
+ into.require(isPlainObject(result.session), "result.session", `${path}.session`, "opened carries the media session");
1073
+ }
1074
+ }
1075
+ else if (result.status === statuses.failure) {
1076
+ if (result.failure === undefined) {
1077
+ into.add("result.failure.required", `${path}.failure`, `${statuses.failure} carries the failure that says why`);
1078
+ }
1079
+ else {
1080
+ validateFailureInto(result.failure, `${path}.failure`, into);
1081
+ }
1082
+ }
1083
+ else {
1084
+ into.add("result.status", `${path}.status`, `${method} answers ${statuses.success} or ${statuses.failure}, not ${String(result.status)}`);
1085
+ }
1086
+ return into.violations;
1087
+ }
1088
+ // ---------------------------------------------------------------------------
1016
1089
  // Authentication.
1017
1090
  // ---------------------------------------------------------------------------
1018
1091
  function validateUser(value, rule, path, into) {
package/guide.md CHANGED
@@ -1002,7 +1002,6 @@ Request/response polling does not have those properties and is not a transport f
1002
1002
  | `@xema/omni-protocol` | Provider adapter contract and shared domain types |
1003
1003
  | `@xema/omni-protocol/testing` | Adapter conformance helpers |
1004
1004
  | `@xema/omni-protocol/validation` | Runtime validators Omni and adapters both use to reject malformed data |
1005
- | `@xema/omni-protocol/design` | Host design-language integration. Specified separately; no part of it is a provider surface. |
1006
1005
 
1007
1006
  ## Declaring an adapter
1008
1007
 
@@ -2226,7 +2225,8 @@ Starts one outbound call from the idle dialpad. It is present only when the voic
2226
2225
  declares `dial`.
2227
2226
 
2228
2227
  - `destination` is the original number selected or entered by the agent.
2229
- - `source` is `contact` or `manual` and must comply with `destinationPolicy`.
2228
+ - The provider holds `destination` to its declared `destinationPolicy`: under `contacts-only`, a
2229
+ number that is not one of its contacts answers `failed` with `omni.destination-not-permitted`.
2230
2230
  - `dialled` confirms that outbound call creation completed.
2231
2231
  - `failed` contains a `ProtocolFailure` and confirms no call was placed.
2232
2232
 
@@ -2260,7 +2260,7 @@ rendering one as the other tells an agent to wait for somebody who is never comi
2260
2260
  | `awaiting-decision` | A person has to decide. The agent is waiting on somebody. |
2261
2261
  | `granted` | A person decided yes. Omni may now tell this provider to stop the agent; until it does, work continues normally, and this says nothing about why Omni has not. |
2262
2262
  | `starting-after-task` | Omni has told the provider to stop; the break begins when the current task ends. No new work arrives meanwhile, and nobody needs to act. |
2263
- | `in-effect` | The agent is on the break now. |
2263
+ | `in-effect` | The agent is on the break now. It holds no task: a break begins when the work ends, so a snapshot reporting `in-effect` beside a task is refused as `break.in-effect.tasks`. |
2264
2264
 
2265
2265
  A denial is a decision, not a standing approval state. The provider transitions the request directly
2266
2266
  to `not-requested`; Omni returns the agent to idle and never asks again on their behalf. They saw the
@@ -2365,6 +2365,11 @@ Requests permission to stop the agent later; it does not itself stop work. The p
2365
2365
  offering work and reports `awaiting-decision` or `granted` through `break-state` events. If the request is denied, the
2366
2366
  provider reports `not-requested` directly, with `decisionReason` when one was supplied.
2367
2367
 
2368
+ **An agent asks from anywhere — idle or on a task** — and Omni offers the request in the task
2369
+ workspace as it does on the idle dashboard. Asked on a task, the break is decided and committed like
2370
+ any other and begins when the work ends: that is `starting-after-task`, and
2371
+ `assertBreakBeginsAfterTask` is the scenario that holds a provider to it.
2372
+
2368
2373
  #### Break reasons
2369
2374
 
2370
2375
  A provider that defines not-ready reason codes publishes them on `Snapshot.break.reasons`,
@@ -2480,6 +2485,11 @@ Omni coordinates one attempt as follows:
2480
2485
 
2481
2486
  1. Freeze the participant set to every connected provider from which the agent can currently
2482
2487
  receive work. A provider joining during the attempt is given no capacity until it finishes.
2488
+ A provider whose authentication is `expired` is not one the agent can receive work from and is
2489
+ not a participant: nothing is asked of it, the break proceeds without it, and when the login
2490
+ is restored it is reconciled from its snapshot as a set-aside provider is, with no capacity
2491
+ until then. `refreshing` keeps a provider in — its identity and capabilities remain available
2492
+ and work continues. `assertBreakParticipants` holds a host to this set.
2483
2493
  2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
2484
2494
  3. Send one `requestBreak` to every participant. A provider reports `awaiting-decision` or
2485
2495
  `granted`; neither state stops work. A denial transitions directly to `not-requested` and
@@ -2705,6 +2715,12 @@ provider whose lead is already at the ceiling answers the join `failed`.
2705
2715
  they joined -- is answered `failed`, whatever their ceiling; the request stands for another lead,
2706
2716
  or until it is withdrawn or declined.
2707
2717
 
2718
+ **A lead on a break does not join.** A break is a reported state in which the agent is not working,
2719
+ and a join is work. Omni offers Join to a lead only while their own `BreakState.approval` is neither
2720
+ `starting-after-task` nor `in-effect` -- a committed break waiting for the lead's current work to
2721
+ finish is not given more -- and a provider answers a `join` from a lead on such a break `failed`.
2722
+ The request stands for another lead, as it does when this one is already on a call.
2723
+
2708
2724
  **On `decline`, or a request the agent withdraws with `{ type: "lead", action: "cancel" }`, the
2709
2725
  provider clears `lead` from the agent's task** and drops the request from every roster. Nothing
2710
2726
  else changes; the agent is still on the call.
@@ -2919,6 +2935,11 @@ Applies a `TaskCommandRequest` to one provider-local task.
2919
2935
  deciding a member's break that another lead has already decided is `applied` when the decisions
2920
2936
  agree and `failed`, saying so in `message`, when they differ. `commitBreak()` on a break already
2921
2937
  in effect is `committed` for the same reason.
2938
+ - **A result is untrusted for the same reason a snapshot is.** It comes from an adapter that may be
2939
+ compiled against another version, and Omni shows the agent what it says. Omni validates it at
2940
+ the boundary with `validateResult(result, "execute")` — a status the method does not answer, a
2941
+ `failed` without its failure, a success carrying one, or an `omni.` code this contract lacks is
2942
+ refused, and the command is treated as unsettled.
2922
2943
  - **A settled result is a fact; an unsettled promise is not.** Transport uncertainty may reject the
2923
2944
  promise with no result at all, and that means *unknown*, not *failed*, and a snapshot follows —
2924
2945
  see **An unsettled result is unknown**. `failed` must never be returned for something the
@@ -3161,6 +3182,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
3161
3182
  | `validateEventEnvelope(envelope, manifest)` | Envelope identity, timestamp, and the payload for each event type. |
3162
3183
  | `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
3163
3184
  | `validateScheduledActivity(activity)` | Required activity fields and start/end ordering. |
3185
+ | `validateResult(result, method)` | What a connection method answered: the status it gives, a failure where the status says so and nowhere else, the failure's shape, and that an `omni.` code is one this contract names. |
3164
3186
  | `validateAuthenticationState(state)` | The identity each state must carry, the capabilities a usable login declares, and the expiry that only `authenticated` may. |
3165
3187
 
3166
3188
  Each returns `ProtocolViolation[]` rather than throwing, so a caller can report every problem at
@@ -3246,6 +3268,8 @@ cannot be established from TypeScript structure alone.
3246
3268
  | `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |
3247
3269
  | `assertAuthenticationRestoreAndExpiry(states)` | A restored authenticated session can refresh and ends in expiry. Every state is validated. |
3248
3270
  | `assertReconnectWithMissedAssignments(before, reconnect, ids)` | A reconnect snapshot restores assignments received while offline. |
3271
+ | `assertBreakParticipants(candidates, participants)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
3272
+ | `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `in-effect` only once nothing is outstanding — never beside a task, never later than the step that has none. |
3249
3273
  | `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
3250
3274
  | `assertWrapTimeout(task, mediaEndedAt, deadline, toleranceMs?)` | The wrap deadline equals media end plus the task allowance, within a tolerance that defaults to 1000ms; a task with no allowance has no deadline, and one observed is the violation. |
3251
3275
  | `assertBrowserIsolationAndReuse(left, right, expected)` | Browser reuse follows only the declared isolation scheme. |
@@ -3259,11 +3283,8 @@ Adapters should run the relevant scenarios against deterministic test state befo
3259
3283
 
3260
3284
  ## A provider does not style the workspace
3261
3285
 
3262
- The package ships a `design` entry point, and none of it is for adapters. It is host UI
3263
- extensibility how a deployment themes Omni and it is specified with the host, not here, which
3264
- is why no part of it is declared under **Shapes**.
3265
-
3266
- What belongs in this contract is the boundary. A provider says what a control **is** through its
3286
+ How a deployment themes Omni is the host's concern and is specified with the host, not here. What
3287
+ belongs in this contract is the boundary. A provider says what a control **is** through its
3267
3288
  capabilities and what its work is **called** through `phaseLabels` and `taskTypePresentation`; how
3268
3289
  any of it is drawn is Omni's. A task cannot select a design language, inject a component, or
3269
3290
  override the agent's theme and font preferences.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,10 +18,6 @@
18
18
  "types": "./dist/index.d.ts",
19
19
  "import": "./dist/index.js"
20
20
  },
21
- "./design": {
22
- "types": "./dist/design.d.ts",
23
- "import": "./dist/design.js"
24
- },
25
21
  "./testing": {
26
22
  "types": "./dist/testing.d.ts",
27
23
  "import": "./dist/testing.js"
package/dist/design.d.ts DELETED
@@ -1,49 +0,0 @@
1
- export type ThemePreference = "system" | "light" | "dark";
2
- export type ResolvedTheme = Exclude<ThemePreference, "system">;
3
- export type Density = "compact" | "comfortable" | "spacious";
4
- /** Semantic values consumed by Omni's layout, independent of a CSS framework. */
5
- export interface DesignTokens {
6
- accent: string;
7
- accentText: string;
8
- surface: string;
9
- surfaceMuted: string;
10
- selected: string;
11
- text: string;
12
- mutedText: string;
13
- border: string;
14
- info: string;
15
- success: string;
16
- warning: string;
17
- danger: string;
18
- radius: string;
19
- radiusLarge: string;
20
- shadow: string;
21
- controlFont: string;
22
- controlWeight: string;
23
- controlTracking: string;
24
- controlHeight: string;
25
- }
26
- export type ControlKind = "button" | "icon-button" | "checkbox" | "input" | "textarea" | "select" | "tabs" | "menu" | "badge" | "card" | "progress";
27
- export interface DesignLanguageManifest {
28
- id: string;
29
- displayName: string;
30
- supportedThemes: ReadonlyArray<ResolvedTheme>;
31
- supportedControls: ReadonlyArray<ControlKind>;
32
- defaultDensity: Density;
33
- }
34
- export interface DesignLanguage {
35
- manifest: DesignLanguageManifest;
36
- tokens: Record<ResolvedTheme, DesignTokens>;
37
- }
38
- /**
39
- * A framework bridge can use any native control representation: an Angular
40
- * component type, a React component, or an Omni DOM renderer. The protocol
41
- * deliberately does not make one UI framework part of the ABI.
42
- */
43
- export interface DesignLanguageAdapter<TControl = unknown> {
44
- readonly language: DesignLanguage;
45
- resolveControl(kind: ControlKind): TControl | Promise<TControl>;
46
- }
47
- export declare function defineDesignLanguage<TControl, T extends DesignLanguageAdapter<TControl>>(adapter: T): T;
48
- /** Maps semantic tokens to the stable CSS custom properties understood by Omni. */
49
- export declare function designTokenProperties(tokens: DesignTokens): Record<string, string>;
package/dist/design.js DELETED
@@ -1,27 +0,0 @@
1
- export function defineDesignLanguage(adapter) {
2
- return adapter;
3
- }
4
- /** Maps semantic tokens to the stable CSS custom properties understood by Omni. */
5
- export function designTokenProperties(tokens) {
6
- return {
7
- "--omni-accent": tokens.accent,
8
- "--omni-accent-text": tokens.accentText,
9
- "--omni-surface": tokens.surface,
10
- "--omni-surface-muted": tokens.surfaceMuted,
11
- "--omni-selected": tokens.selected,
12
- "--omni-text": tokens.text,
13
- "--omni-muted-text": tokens.mutedText,
14
- "--omni-border": tokens.border,
15
- "--omni-info": tokens.info,
16
- "--omni-success": tokens.success,
17
- "--omni-warning": tokens.warning,
18
- "--omni-danger": tokens.danger,
19
- "--omni-radius": tokens.radius,
20
- "--omni-radius-large": tokens.radiusLarge,
21
- "--omni-shadow": tokens.shadow,
22
- "--omni-control-font": tokens.controlFont,
23
- "--omni-control-weight": tokens.controlWeight,
24
- "--omni-control-tracking": tokens.controlTracking,
25
- "--omni-control-height": tokens.controlHeight,
26
- };
27
- }