@xema/omni-protocol 0.1.111 → 0.1.113

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.
@@ -12,8 +12,8 @@ export declare function assertNoViolations(violations: readonly ProtocolViolatio
12
12
  * rule never evaluated is a visible gap, not a pass. The harness registers one for each exercise.
13
13
  */
14
14
  export declare function observeRules(listener: (rule: string) => void): () => void;
15
- /** Says a rule was evaluated, for code that decides outside a collector: the stream, the drive. */
16
- export declare function ruleEvaluated(...rules: readonly string[]): void;
15
+ /** Says a rule was evaluated, for code that decides outside a collector: the stream, the test. */
16
+ export declare function ruleTested(...rules: readonly string[]): void;
17
17
  /** Every field is optional, so this checks what is present rather than what is missing. */
18
18
  export declare function validateContact(contact: unknown, path?: string): ProtocolViolation[];
19
19
  export declare function validateScheduledActivity(activity: unknown, path?: string): ProtocolViolation[];
@@ -167,8 +167,8 @@ export function observeRules(listener) {
167
167
  ruleListeners.add(listener);
168
168
  return () => { ruleListeners.delete(listener); };
169
169
  }
170
- /** Says a rule was evaluated, for code that decides outside a collector: the stream, the drive. */
171
- export function ruleEvaluated(...rules) {
170
+ /** Says a rule was evaluated, for code that decides outside a collector: the stream, the test. */
171
+ export function ruleTested(...rules) {
172
172
  for (const rule of rules)
173
173
  for (const listener of ruleListeners)
174
174
  listener(rule);
@@ -176,11 +176,11 @@ export function ruleEvaluated(...rules) {
176
176
  class Collector {
177
177
  violations = [];
178
178
  add(rule, path, message) {
179
- ruleEvaluated(rule);
179
+ ruleTested(rule);
180
180
  this.violations.push({ rule, path, message });
181
181
  }
182
182
  require(condition, rule, path, message) {
183
- ruleEvaluated(rule);
183
+ ruleTested(rule);
184
184
  if (!condition)
185
185
  this.violations.push({ rule, path, message });
186
186
  return Boolean(condition);
@@ -719,7 +719,7 @@ const levelIds = (levels) => levels ?? DEFAULT_LEVEL_IDS;
719
719
  * formatting hides it; anything else by its text, case aside.
720
720
  */
721
721
  function validateNothingLeaksInto(task, locked, path, into) {
722
- ruleEvaluated("task.locked.leak");
722
+ ruleTested("task.locked.leak");
723
723
  const carries = (text, value) => {
724
724
  if (typeof text !== "string")
725
725
  return false;
@@ -1160,7 +1160,7 @@ function validateTaskInto(task, context, path, into) {
1160
1160
  // that created it, and says so whatever the provisioning: the desk shows no Accept for a call
1161
1161
  // the agent placed. The provisioning governs work the queue routes, and nothing else.
1162
1162
  const originated = task.phase === "pending" && originatedByTheAgent(task);
1163
- ruleEvaluated("task.acceptance.originated");
1163
+ ruleTested("task.acceptance.originated");
1164
1164
  if (task.acceptance !== undefined) {
1165
1165
  into.oneOf(task.acceptance, ACCEPTANCE_MODES, "task.acceptance", `${path}.acceptance`);
1166
1166
  if (task.phase !== "pending") {
@@ -1355,7 +1355,7 @@ export function validateBreakStatus(state, tasks, path = "snapshot") {
1355
1355
  // The converse: a committed break with nothing outstanding has begun. starting-after-task beside
1356
1356
  // no task is a break waiting on work that does not exist, and an empty list reading as "still
1357
1357
  // finishing" is the plausible nought.
1358
- ruleEvaluated("break.starting-after-task.tasks");
1358
+ ruleTested("break.starting-after-task.tasks");
1359
1359
  if (isPlainObject(state) && state.status === "starting-after-task"
1360
1360
  && Array.isArray(tasks) && tasks.length === 0) {
1361
1361
  into.add("break.starting-after-task.tasks", `${path}.break.status`, "a break starting after the task waits on a task, and the snapshot carries none: with nothing outstanding the break is on-break");
@@ -1619,7 +1619,7 @@ function validateBreakState(value, path, into) {
1619
1619
  // A break in effect, or about to be, on a provider that publishes reasons is on one of them: an
1620
1620
  // forced one included, since the lead's force-break command named it. A break of no kind is a break whose
1621
1621
  // rules -- who may listen through it, whether it counts -- nobody can apply.
1622
- ruleEvaluated("break.activeReasonId.required");
1622
+ ruleTested("break.activeReasonId.required");
1623
1623
  if (seen.size > 0 && (value.status === "on-break" || value.status === "starting-after-task")) {
1624
1624
  into.require(typeof value.activeReasonId === "string" && value.activeReasonId.length > 0, "break.activeReasonId.required", `${path}.activeReasonId`, `a break ${describeValue(value.status)} on a provider that publishes reasons names the one it is on`);
1625
1625
  }
@@ -3140,7 +3140,7 @@ const filled = (v) => typeof v === "string" && v.trim().length > 0;
3140
3140
  function collector() {
3141
3141
  const violations = [];
3142
3142
  const check = (ok, rule, path, message) => {
3143
- ruleEvaluated(`recording.${rule}`);
3143
+ ruleTested(`recording.${rule}`);
3144
3144
  if (!ok)
3145
3145
  violations.push({ rule: `recording.${rule}`, path, message });
3146
3146
  return Boolean(ok);
package/guide/agent.md CHANGED
@@ -234,7 +234,7 @@ would otherwise find the next customer under a reused handle and act on them. A
234
234
  of the task the adapter keeps, not a field held in memory beside it: it lives in the login's
235
235
  `store` with the task, or is derived from a platform handle that itself never reuses, so a rebuilt
236
236
  adapter comes back with the same assignment on the same task, or every late event it was minted to
237
- catch mismatches and the second adapter's snapshot no longer carries the task (`drive.reload.snapshot`).
237
+ catch mismatches and the second adapter's snapshot no longer carries the task (`test.reload.snapshot`).
238
238
 
239
239
  Assignment, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
240
240
  task's `acceptance`, moving the task from `pending` to `confirmed`. The provider reports
@@ -369,7 +369,7 @@ Migration from the earlier spellings:
369
369
  | command.complete.mode | `command.complete.wrapAllowance`: `complete` under either mode, refused only on `provider-automatic` with no wrap |
370
370
  | expiresInSeconds on task-offered | `Task.expiresInSeconds`, in `pending` (`task.pending.expiresInSeconds`, `.unexpected`; `event.taskOffered.expiresInSeconds.unexpected`) |
371
371
  | the expired outcome naming preview | gone: nothing expires a preview; withdrawn, it is `cancelled` by the provider |
372
- | schedule on voice alone | `schedule` on every channel, under a manifest that declares `calendar` (`task.capability.calendar.required`), bounded by `settleMs` (`drive.schedule.unsettled`) |
372
+ | schedule on voice alone | `schedule` on every channel, under a manifest that declares `calendar` (`task.capability.calendar.required`), bounded by `settleMs` (`test.schedule.unsettled`) |
373
373
  | a forced break requesting breaks on the other providers | `setCapacity({ count: 0 })` on every other usable provider (`assertForcedBreakStopsTheRest`) |
374
374
  | end-forced-break on a break the platform imposed | refused: `team.command.endForcedBreak.provider`, `omni.break-forced-by-provider` |
375
375
  | ForcedBreak.expectedDurationMs | `expectedEndsInSeconds`: seconds left, restated (`break.forced.expectedEndsInSeconds`, `.expectedDurationMs.renamed`); the command keeps `expectedDurationMs` |
@@ -383,6 +383,7 @@ Migration from the earlier spellings:
383
383
  | two clients on one login | the later wins; the first is ended with `recovery: "displaced"` |
384
384
  | the phone unspoken | `PhoneState` -- the device, its mute, its channels -- on `Snapshot.phone`, `phone-updated` and `TeamMember.phone`, from a manifest declaring `phoneStatus` (`snapshot.phone.required`, `.unexpected`, `phone.*`, `phone.channel.*`, `event.phone.capability`) |
385
385
  | the agent's readiness for the next call read into setCapacity | the agent's own queue: `capabilities.nextCall`, `requestNextCall`/`cancelNextCall`/`releaseLinedUp`, `Snapshot.nextCall` and `linedUp`, the `next-call` and `lined-up` events (`snapshot.nextCall.idle`, `nextCall.*`, `linedUp.*`) |
386
+ | a harness that exercised an adapter, drove a call and evaluated rules | the harness reads as a test: `testAdapter`, `withCall`, `timeoutMs`, `notTested`, `rulesTested`, rules `test.*` (`test.timeout`, `test.offer.expired`, `test.preview.deadline`, ...) |
386
387
 
387
388
  Update producers, consumers, saved task snapshots, and validation-rule assertions together.
388
389
  History and report rule names use `history` and `historyReport`; assignment rules use
@@ -588,8 +589,13 @@ as it begins and ends, and takes the provider's published entry as the closed ac
588
589
  it does not reopen the leg, replace the provider's timestamp, or overwrite the provider's final
589
590
  duration with a later report. A later agent mute is a new leg, never a reopening of the ended one.
590
591
 
591
- Correlate the provider-published `muted` entry using the provider-selected `at` acknowledged
592
- for the current report key; a final `seconds` closes that leg under the history contract.
592
+ **The provider assigns the instant.** `HistoryReportResult.at` is the provider's own instant for the
593
+ leg, on its own clock, written when the provider takes the leg into its record, never an echo of
594
+ the `at` the host reported: the host keeps the answer as the leg's identity from then on and names
595
+ it on the closing report, and a provider that echoes the host's instant has assigned nothing
596
+ (`test.recordStep.identity`). Correlate the provider-published `muted` entry using that
597
+ provider-selected `at` acknowledged for the current report key; a final `seconds` closes that leg
598
+ under the history contract.
593
599
  This is identity matching, not reconciliation between clocks. An old closed entry, an unrelated
594
600
  task, or a stale connection cannot end a newer local mute. If publication precedes the acknowledgment,
595
601
  retain the current provider view and apply the matching closure once correlation is available;
@@ -813,10 +819,10 @@ words are close in English and far apart on the desk: `nobody` is silence, `not-
813
819
  standing notice.
814
820
 
815
821
  One consequence for whoever builds the agent application: because a conformance run fails on `not-yet-read`, a
816
- conformant adapter never shows an agent application `not-yet-read` under test, and a clean `exerciseAdapter`
822
+ conformant adapter never shows an agent application `not-yet-read` under test, and a clean `testAdapter`
817
823
  result says nothing about how the agent application renders it. That rendering is tested against a fixture --
818
824
  a task published under `not-yet-read` shows the notice, the same task under `queue` shows none --
819
- and never against an adapter, even in principle. `exerciseAdapter` treats a
825
+ and never against an adapter, even in principle. `testAdapter` treats a
820
826
  task published under `not-yet-read` as a violation (`capabilitySource.notYetRead`), as it treats
821
827
  a diagnostic: a conformance run against a platform that cannot say what it permits fails loudly
822
828
  rather than passing with a note.
@@ -914,9 +920,9 @@ on the calendar.
914
920
  follows within the manifest's `settleMs`, as a `task-ended` follows an applied `complete`. Past
915
921
  the bound the agent application calls `snapshot()`: a calendar carrying an activity at that instant
916
922
  clears the wait; one without it shows the follow-up as unsettled -- "Scheduled... the provider has
917
- not confirmed" -- naming the command. The drive schedules one follow-up where the task offers it,
923
+ not confirmed" -- naming the command. The test schedules one follow-up where the task offers it,
918
924
  at a time it takes from the provider's own publication, and holds the provider to the same bound
919
- (`drive.schedule.unsettled`).
925
+ (`test.schedule.unsettled`).
920
926
 
921
927
  ### Custom capabilities
922
928
 
package/guide/breaks.md CHANGED
@@ -243,7 +243,7 @@ Capacity supersedes rather than accumulates: the latest value is the ceiling, an
243
243
  ordinary as an increase. A provider whose ceiling can only rise -- one that keeps the highest count
244
244
  it was ever told, or returns early on a small one -- cannot be told to take less work, and an agent application
245
245
  taking capacity away is answered `applied` while the work keeps coming. The harness moves the axis
246
- both ways after the drive, two then one then nought, and an offer after a lower count is caught
246
+ both ways after the test, two then one then nought, and an offer after a lower count is caught
247
247
  against it (`stream.taskOffered.overCapacity`).
248
248
 
249
249
  **Capacity gates what the provider assigns, not what the agent starts.** A call placed from the
package/guide/phone.md CHANGED
@@ -62,7 +62,7 @@ report is a task fact or a capacity fact; it is what the station can do, and the
62
62
  is the one that knows whether an agent without a microphone, or without a speaker, can work.
63
63
 
64
64
  The agent application's own obligations here — asking at connect, never publishing `not-asked` when it does,
65
- publishing a state and not a flicker — are Omni's tests' to hold. `exerciseAdapter` holds the
65
+ publishing a state and not a flicker — are Omni's tests' to hold. `testAdapter` holds the
66
66
  other side: it validates the shape of whatever agent application a test hands the adapter, requires `audio` on
67
67
  a softphone login and none elsewhere -- not on a desk phone, not off voice
68
68
  (`context.host.audio.required` / `.unexpected`), holds `host.mute` to the same line (`host.mute.required`
package/guide/voice.md CHANGED
@@ -131,7 +131,7 @@ stream that drops, a track that ends, a transport that disconnects, a microphone
131
131
  endpoint re-registering change nothing about the task until the provider says so. Structurally:
132
132
  `task-audio-started` and `task-audio-ended` alternate on a task whose work has begun, audio ends
133
133
  only where it arrived, what follows the audio ending is `completing` or `task-ended`, and every
134
- task is introduced once — `exerciseAdapter` holds the stream to that from the connect snapshot on,
134
+ task is introduced once — `testAdapter` holds the stream to that from the connect snapshot on,
135
135
  and `assertAudioFollowsTheTask` holds any sequence.
136
136
 
137
137
  ## Connecting back during completion
package/guide.md CHANGED
@@ -1646,9 +1646,9 @@ reload is the first client dying and a second coming up for the same login, and
1646
1646
  publishes the same truth about the same task as the first did, or the first was publishing
1647
1647
  something it made up: an assignment minted for the connection, audio remembered as a boolean, a
1648
1648
  phone the second never read. Three pieces of exactly that state were found in one adapter in an
1649
- afternoon, and none of it was visible until the adapter was built twice. The drive's `rebuild` is the test of this
1649
+ afternoon, and none of it was visible until the adapter was built twice. The test's `rebuild` is the test of this
1650
1650
  principle: given a way to build the adapter again, the run hands the login over and holds the
1651
- second to what the first published (`drive.reload.snapshot`, `.history`, `.openAudio`, and the
1651
+ second to what the first published (`test.reload.snapshot`, `.history`, `.openAudio`, and the
1652
1652
  stream's own rules across the resync). An adapter that cannot be built twice against its platform
1653
1653
  and publish the same task is not a conformant adapter yet, whatever a single run says.
1654
1654
 
@@ -2180,7 +2180,7 @@ Creates one live provider connection for the signed-in agent.
2180
2180
  | `autoAcceptTasks` | Agent local policy policy relayed to the provider at login, stated by the agent application on every connection and never assumed from its absence. When `true`, a pending task states its `acceptance`; when `false`, every task requires agent acceptance. Fixed for this connection, like everything else here: the provider states or omits `acceptance` by the value it was sent, and Omni validates by that same value, not by a policy that has since moved — a change reaches the provider through a fresh `connect()`. |
2181
2181
  | `timeZone` | The same value passed as `AuthenticationContext.timeZone`. The provider stores it on the agent and carries it on the identity. See **The agent's day**. |
2182
2182
  | `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. The type ties `host.mute` to it: a `softphone` login's agent application states what its Mute does, and a desk-phone or conversation login's agent application cannot, so the omission is a compile error rather than a live seat's discovery. See **The station is the agent application's** in `guide/phone.md`. |
2183
- | `store` | The login's operational store, kept by the agent application for the life of the login, across a reload of the agent application, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the interaction legs an agent application reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store an agent application may clear aggressively. **A task's keys carry its assignment id and go with the task**: a key written about a task names the assignment id in the key, and is deleted before the task's end is published, so that nothing of a closed task survives its ending and nothing is left for whatever comes next -- a record with legs the agent application never reported against it. A login-scoped key carries no assignment id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`drive.store.retained`) or written about the task after its end -- a persist hung off a timer that saw the task as it was (`drive.store.late`). The ordinary late write is the agent application's, not the adapter's: an agent application reports a leg without waiting for the answer, so an unmute can follow `complete` by a tick, and the adapter answers a report about a task that has ended `failed` with `omni.assignment-not-found` and writes nothing -- the agent application ends its own open legs at the task's end, so such a report is the agent application's error to see, and an agent application reads every `recordStep` answer and awaits the one for the leg it closes at a task's end, the only moment a refusal is expected, since a report nobody waits for is an error nobody can see; an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
2183
+ | `store` | The login's operational store, kept by the agent application for the life of the login, across a reload of the agent application, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the interaction legs an agent application reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store an agent application may clear aggressively. **A task's keys carry its assignment id and go with the task**: a key written about a task names the assignment id in the key, and is deleted before the task's end is published, so that nothing of a closed task survives its ending and nothing is left for whatever comes next -- a record with legs the agent application never reported against it. A login-scoped key carries no assignment id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`test.store.retained`) or written about the task after its end -- a persist hung off a timer that saw the task as it was (`test.store.late`). The ordinary late write is the agent application's, not the adapter's: an agent application reports a leg without waiting for the answer, so an unmute can follow `complete` by a tick, and the adapter answers a report about a task that has ended `failed` with `omni.assignment-not-found` and writes nothing -- the agent application ends its own open legs at the task's end, so such a report is the agent application's error to see, and an agent application reads every `recordStep` answer and awaits the one for the leg it closes at a task's end, the only moment a refusal is expected, since a report nobody waits for is an error nobody can see; an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
2184
2184
  | `host` | The agent application's report of the agent's station — devices, permissions, network — to consult before declaring the agent ready to the platform, and on every change. See **The agent application reports, the adapter decides** in `guide/phone.md`. |
2185
2185
  | `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
2186
2186
  | `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
@@ -2414,6 +2414,25 @@ and no count. A provider that publishes any of these instants therefore declares
2414
2414
  (`manifest.timeCheck.required`) and states `providerTime` on every snapshot
2415
2415
  (`snapshot.providerTime`). Two screens then differ by their delay and nothing else.
2416
2416
 
2417
+ **Seconds left are worked out at each publication.** `expiresInSeconds`, `previewEndsInSeconds`,
2418
+ `wrapEndsInSeconds` and `expectedEndsInSeconds` are each the provider's arithmetic at the instant
2419
+ of the publication that carries them, never a value copied from an earlier one. A provider that
2420
+ builds the next publication by spreading the last one forward hands a reloaded desk a countdown
2421
+ already spent -- sixty seconds shown beside a lead's screen showing twenty -- and the stream
2422
+ names it: between two publications of one countdown the value loses at least the seconds the
2423
+ provider's own clock says passed, within a second of rounding (`stream.countdown.copied`, on an
2424
+ update against the last publication, and on a snapshot from a provider with a clock against its
2425
+ `providerTime`). Under `agent-command` the wrap's value reaches `0` and stays there while the agent
2426
+ overruns; that is the true value, not a copy.
2427
+
2428
+ **A deadline stated is a deadline kept.** The provider that says how long an offer has ends it
2429
+ `expired` when it runs out, and the test holds it to that: where the first offer carries
2430
+ `expiresInSeconds` within the test's timeout, the test leaves that offer unanswered and expects
2431
+ the `expired` ending within the seconds plus `settleMs`, then tests the next offer
2432
+ (`test.offer.expired`). A preview's deadline is held to its `atDeadline` the same way: under
2433
+ `provider-dials` the provider dials when it runs out, under `host-dials` and `waits` the preview
2434
+ stands until the desk dials (`test.preview.deadline`).
2435
+
2417
2436
  #### Optional periodic provider time checks and agent application estimates
2418
2437
 
2419
2438
  A provider may declare `Manifest.timeCheck: true` and implement `Connection.checkTime(request)`.
@@ -2592,7 +2611,7 @@ break, which says so with `forced`: in effect at once, or `starting-after-task`
2592
2611
  finishes the call they are on (`stream.breakState.commitBeforeGrant`); and a break never moves backwards —
2593
2612
  from `on-break` or `starting-after-task` to a grant or a request, or from `granted` to
2594
2613
  `awaiting-approval` — a new request passes through `not-requested` (`stream.breakState.backwards`).
2595
- `exerciseAdapter` holds the stream to that from the connect snapshot on;
2614
+ `testAdapter` holds the stream to that from the connect snapshot on;
2596
2615
  `assertBreakFollowsItsRequests` holds any sequence.
2597
2616
 
2598
2617
  Adapters and hosts can call `validateBreakTransition(before, after)` from the package’s validation entry point
@@ -2656,7 +2675,7 @@ snapshots until it ends.
2656
2675
  **Every offer is owed an ending.** An assignment the provider introduced is an assignment it ends,
2657
2676
  with `task-ended` and an outcome, whatever became of the call: answered and completed, declined,
2658
2677
  withdrawn, abandoned in the ring, lapsed, taken over. An offer that is simply never mentioned
2659
- again leaves the desk holding a task nobody will close, and the conformance drive names it
2678
+ again leaves the desk holding a task nobody will close, and the test names it
2660
2679
  (`stream.taskOffered.unended`).
2661
2680
 
2662
2681
  ### `task-updated`
@@ -2711,8 +2730,8 @@ manifest's `settleMs`. A provider never answers `applied` for a completion it ha
2711
2730
  performed. Past the bound the agent application calls `snapshot()`: a snapshot still carrying the task is a task
2712
2731
  held open by a provider that said it was done, and the desk shows it as unsettled -- "Completing...
2713
2732
  the provider has not confirmed" -- naming the command; a snapshot no longer carrying it clears the
2714
- task, since the ending was owed and lost. The drive holds a provider to the same bound
2715
- (`drive.completion.unsettled`). The `task-audio-ended` event and the `completing` phase are likewise
2733
+ task, since the ending was owed and lost. The test holds a provider to the same bound
2734
+ (`test.completion.unsettled`). The `task-audio-ended` event and the `completing` phase are likewise
2716
2735
  non-terminal. A replacement
2717
2736
  snapshot that no longer contains the task also clears it. Repeated `task-ended` delivery with the
2718
2737
  same envelope ID is harmless, and a `task-ended` naming an assignment that has already ended is
@@ -2743,7 +2762,7 @@ already done the safe thing before it speaks, and nothing in the task or break m
2743
2762
  a diagnostic. It exists so that the loudness reaches a person — an agent application renders each one where the
2744
2763
  agent works and keeps a count an operator can read, because a healthy workspace over a shouting
2745
2764
  console is the silence problem one layer up. One event per occurrence; the agent application counts, the
2746
- provider does not batch. `exerciseAdapter` treats a diagnostic delivered during a run as a
2765
+ provider does not batch. `testAdapter` treats a diagnostic delivered during a run as a
2747
2766
  violation (`diagnostic.raised`): a conformance run against a platform that is breaking its rules
2748
2767
  fails loudly rather than passing with a note. A task published under `capabilitySource:
2749
2768
  "not-yet-read"` is the same kind of fact, stated on the task instead, and fails a run the same way
@@ -2856,7 +2875,7 @@ wider goes through this key.
2856
2875
 
2857
2876
  Whether two logins declare the same capabilities, field by field — key order aside, and with
2858
2877
  `team: {}` distinct from `team` absent. It is the comparison an adapter makes before republishing
2859
- `authenticated`, and the one `exerciseAdapter` holds `refreshing` to.
2878
+ `authenticated`, and the one `testAdapter` holds `refreshing` to.
2860
2879
 
2861
2880
  ## Runtime validation
2862
2881
 
@@ -2881,7 +2900,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
2881
2900
  | `validateHostReport(report)` | The agent application's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever agent application a test hands the adapter; `stillHost(report)` builds one that never changes. |
2882
2901
  | `validateHostMute(mute, softphone)` | What the agent application's Mute does, stated on a softphone login and nowhere else: `stream` or `station` (`host.mute`), required where the agent application holds a microphone (`host.mute.required`) and refused where it does not (`host.mute.unexpected`). The harness holds `ConnectContext.host.mute` to it. |
2883
2902
  | `validateLoginStore(store)` | The login's store the agent application hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
2884
- | `validateCapacity(capacity)` | What the agent application states as capacity: a whole number of zero or more (`capacity.count`), zero being agent application-stopped. The harness states one on connect and two, one and zero after the drive, each answered `applied`, and holds any offer to the count in force (`stream.taskOffered.overCapacity`). |
2903
+ | `validateCapacity(capacity)` | What the agent application states as capacity: a whole number of zero or more (`capacity.count`), zero being agent application-stopped. The harness states one on connect and two, one and zero after the test, each answered `applied`, and holds any offer to the count in force (`stream.taskOffered.overCapacity`). |
2885
2904
  | `validateAuthenticationResult(result, method)` | What `start()` or `complete()` answered: a challenge or a rejection, a login or a rejection. A rejection's failure is held to its rules -- an `omni.` code the contract lists, and `omni.phone-not-permitted` never retryable, since the agent's station is configuration. `validateAuthenticationFailure(failure)` is the same check on a failure alone. |
2886
2905
  | `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a conference's item, a schedule's time, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.interaction` for every control on the call or the conversation), and the state that has to stand: a lead requested, somebody else still on the call (`command.conference.remove.alone`). An agent application validates before sending and an adapter before acting. |
2887
2906
  | `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. |
@@ -2898,7 +2917,7 @@ carries that agent reports `team.member.self`, on a `team-member-updated` and a
2898
2917
  reports `team.unentitled`, and a team event to such a login reports `event.team.capability`; given
2899
2918
  `leadFeatures: true`, a snapshot without a team reports `team.required`, and given `leadFeatures: false`,
2900
2919
  anything of the team reaching the lead reports `team.unexpected` or `event.team.features`. Without them those rules are not checked, because they cannot be.
2901
- `exerciseAdapter` passes all three, holding the switch off until it has sent it. Given the manifest, a
2920
+ `testAdapter` passes all three, holding the switch off until it has sent it. Given the manifest, a
2902
2921
  snapshot or event that carries an instant the desk renders as a running duration under a manifest
2903
2922
  without `timeCheck` reports `manifest.timeCheck.required`.
2904
2923
 
@@ -2931,9 +2950,9 @@ A refusal has an aftermath on the desk and a report to the provider, and both ar
2931
2950
  side. The harness tells an adapter under test exactly as an agent application does
2932
2951
  (`connection.refused.required`, `connection.refused.rejected`).
2933
2952
 
2934
- ## Conformance helpers
2953
+ ## Testing an adapter
2935
2954
 
2936
- ### `exerciseAdapter(adapter, context, options?)`
2955
+ ### `testAdapter(adapter, context, options?)`
2937
2956
 
2938
2957
  Adapter conformance exercise from `@xema/omni-protocol/testing`.
2939
2958
 
@@ -2947,7 +2966,7 @@ By default it throws `ProtocolConformanceError` listing every violation. Pass
2947
2966
  `{ collectOnly: true }` to receive them on the result instead:
2948
2967
 
2949
2968
  ```ts
2950
- const result = await exerciseAdapter(adapter, context, { collectOnly: true });
2969
+ const result = await testAdapter(adapter, context, { collectOnly: true });
2951
2970
  expect(result.violations).toEqual([]);
2952
2971
  expect(result.disconnectWasClean).toBe(true);
2953
2972
  ```
@@ -2959,7 +2978,7 @@ latest the session published during the run, which differs only when the adapter
2959
2978
  `authentication.refreshing.identity`, a changed capability set `authentication.refreshing.capabilities`.
2960
2979
  A capability granted by a later login requires its methods just as one declared at sign-in does.
2961
2980
 
2962
- `result.notExercised` lists what the run never reached — one subject per family of rules: each
2981
+ `result.notTested` lists what the run never reached — one subject per family of rules: each
2963
2982
  optional part of a task (`task.browsers`, `task.history`, `task.leadAssist`, …), the break's
2964
2983
  `reasons` and `forced`, the team member list's `members` and a member's `request`, each declared contribution, and
2965
2984
  each event type (`event.task-ended`, …) — and so what a clean `violations` says nothing about.
@@ -2968,49 +2987,52 @@ no tasks exercises no task rule, and a pass over it reads as coverage it is not.
2968
2987
 
2969
2988
  **A static run never answers a call.** It states a capacity and disconnects, so everything
2970
2989
  downstream of `answer` -- the room, the stage, audio, every phase past `pending`, every dial
2971
- outcome -- stays in `notExercised` for every adapter, and a rule about a live call is enforced only
2972
- in each adopter's own tests. `{ drive: true }` closes that: the exercise takes the first task the
2990
+ outcome -- stays in `notTested` for every adapter, and a rule about a live call is enforced only
2991
+ in each adopter's own tests. `{ withCall: true }` closes that: the exercise takes the first task the
2973
2992
  provider offers through one ordinary lifecycle -- accept it, wait for its audio and open it on a
2974
2993
  softphone, hold and resume where the task offers `hold`, end the call where it offers `endCall`,
2975
2994
  complete it with an outcome where the agent completes -- and holds every step to the rules a
2976
2995
  agent application holds a provider to. Each command is validated against the task as published
2977
- (`drive.command.*`), each answer for its method, a refusal of a control the task offered is a
2978
- violation (`drive.command.failed`), and an event the provider owes and never sends is one too
2979
- (`drive.timeout`, after `driveTimeoutMs`, 5000 by default). Around the drive the exercise holds
2996
+ (`test.command.*`), each answer for its method, a refusal of a control the task offered is a
2997
+ violation (`test.command.failed`), and an event the provider owes and never sends is one too
2998
+ (`test.timeout`, after `timeoutMs`, 5000 by default). A first offer that says how long it has is left to
2999
+ lapse and expected to end `expired`, and a preview's deadline is held to its `atDeadline`
3000
+ (`test.offer.expired`, `test.preview.deadline`); see **Every screen counts from the provider's
3001
+ clock**. Around the test the exercise holds
2980
3002
  the run to what an agent application holds a provider to between commands: an offer before the agent application stated
2981
3003
  capacity, or beyond the count with nothing dialled on it, is named
2982
3004
  (`stream.taskOffered.beforeCapacity`, `.overCapacity`); every user the snapshot names is looked up
2983
3005
  through `getUserDetails` and the answer held to the shape, nobody unasked, nobody described as
2984
3006
  nothing (`getUserDetails.user.*`, `getUserDetails.unasked`, `connection.getUserDetails.empty`); and
2985
3007
  the second adapter a `rebuild` gives comes up signed in as the same login from the secrets alone
2986
- before it reads anything (`drive.reload.login`), and is then held to everything the first was on
3008
+ before it reads anything (`test.reload.login`), and is then held to everything the first was on
2987
3009
  connect: the methods its declarations call for, the agent application's report, a capacity stated to it, its
2988
3010
  snapshot read as the connect snapshot was, and its audio opened afresh on a task carried with
2989
3011
  `audio: "started"`, since the first client's session died with it. The result also says which rules the
2990
- run evaluated, pass or fail, in `rulesEvaluated`: the validators' as each was applied, the stream's
3012
+ run evaluated, pass or fail, in `rulesTested`: the validators' as each was applied, the stream's
2991
3013
  as each case was considered, so a test that needs a rule to have run asserts it there rather than
2992
3014
  inferring it from an empty `violations`, and a rule absent from it was never looked at, which is a
2993
3015
  gap and not a pass. With the audio open on a softphone,
2994
- the drive mutes it for one second and reports the leg through `recordStep`, begun and then ended,
2995
- expecting each report `recorded` with the provider-selected history `at` (`drive.recordStep.failed`, `.rejected`, `result.recordStep.at`); then it mutes again and
3016
+ the test mutes it for one second and reports the leg through `recordStep`, begun and then ended,
3017
+ expecting each report `recorded` with the provider-selected history `at` (`test.recordStep.failed`, `.rejected`, `result.recordStep.at`); then it mutes again and
2996
3018
  ends the call muted, as agents do, so the leg is open when the audio ends, the provider closes it
2997
3019
  in the completing publication or the open entry is refused (`task.history.muted.open`),
2998
- and the drive's closing report after the audio ended is expected `recorded` and to change nothing:
2999
- a record restated afterwards with that leg's duration altered is named (`drive.recordStep.overwritten`).
3020
+ and the test's closing report after the audio ended is expected `recorded` and to change nothing:
3021
+ a record restated afterwards with that leg's duration altered is named (`test.recordStep.overwritten`).
3000
3022
  Where the provider restates the task's record afterwards, each leg is in it or the hole is named
3001
- (`drive.recordStep.history`). Given `rebuild`, a way to build the adapter again as an agent application reload
3002
- does, the drive reloads the agent application as a reload happens: the first client is unsubscribed,
3003
- disconnected and its session closed (`drive.reload.handover`), and only then is a second adapter
3023
+ (`test.recordStep.history`). Given `rebuild`, a way to build the adapter again as an agent application reload
3024
+ does, the test reloads the agent application as a reload happens: the first client is unsubscribed,
3025
+ disconnected and its session closed (`test.reload.handover`), and only then is a second adapter
3004
3026
  built and connected for the same login with the same context and the same `store`; its snapshot
3005
3027
  must carry the task with that leg at its acknowledged provider timestamp and the reported actor, and the run goes on with the second as its
3006
3028
  connection to the end. A platform that holds the record hands it back, and an adapter that composed
3007
- the record in memory has nothing and is named (`drive.reload.snapshot`, `drive.reload.history`,
3029
+ the record in memory has nothing and is named (`test.reload.snapshot`, `test.reload.history`,
3008
3030
  `.rejected`). The second adapter's snapshot is taken as any resync is: held to what the stream knew
3009
3031
  before it replaces it, so a phase gone backwards, a record that shrank or audio forgotten on a task
3010
3032
  still at work is named by the stream's own rules (`stream.snapshot.phase`, `.history`,
3011
3033
  `.audio`) and a reload is a place those rules keep working, not one where they stop. The second
3012
3034
  adapter is held to what the first was: the same provider
3013
- (`drive.reload.manifest`), a snapshot that stands as any snapshot must, and a record that lost none
3035
+ (`test.reload.manifest`), a snapshot that stands as any snapshot must, and a record that lost none
3014
3036
  of the entries the first had published -- a record once read is not unread across a reload either.
3015
3037
  A reconnect on the same adapter object would prove nothing, since an in-process adapter's memory
3016
3038
  survives it; only a second object separates kept-in-the-store from never-lost. And because the
@@ -3019,31 +3041,31 @@ open task back to, and nothing about the reload is exempt from any rule. The sec
3019
3041
  `secrets`, so the reload is a restore before it is anything else: an adapter that holds a session
3020
3042
  has already put what would rebuild it into the secrets store, from the moment it was handed one,
3021
3043
  not only when a flow completes -- the session is the adapter's, the store is the agent application's, and a
3022
- agent application reload is exactly when no flow will run. And once the task has ended, the store the drive
3044
+ agent application reload is exactly when no flow will run. And once the task has ended, the store the test
3023
3045
  handed the adapter holds no key naming the task's assignment id, delimited, never as a run of characters inside
3024
- another id (`drive.store.retained`); an adapter whose keys never named the task leaves the rule
3046
+ another id (`test.store.retained`); an adapter whose keys never named the task leaves the rule
3025
3047
  unevaluated, and the result says so rather than passing it: a task's keys go
3026
3048
  with the task, or whatever comes next inherits them. Outside the
3027
3049
  interaction phases with `hold` still declared -- in
3028
3050
  `confirmed`, where the provider publishes it, and in `completing` once this agent's interaction has ended -- the
3029
- drive sends `hold` past the validator that would hold it back, and expects `failed`: the adapter
3051
+ test sends `hold` past the validator that would hold it back, and expects `failed`: the adapter
3030
3052
  is the second gate on a control on the contact, and one that applies it outside this task's
3031
- current interaction is named (`drive.command.interaction`). The drive cannot put a task into a phase the provider
3053
+ current interaction is named (`test.command.interaction`). The test cannot put a task into a phase the provider
3032
3054
  never publishes, so a provider that goes straight from `pending` to `in-progress` is checked in
3033
3055
  `completing` alone. A task the agent completes is
3034
- completed from wherever it stands once the drive has nothing left to do on it -- from `completing`
3056
+ completed from wherever it stands once the test has nothing left to do on it -- from `completing`
3035
3057
  after an `end-call`, or from `in-progress` where there is no call to end, which is every chat and
3036
3058
  email and a voice task offering no `endCall` -- so a conversation reaches its end as a call does.
3037
- The drive stops where the task offers no way on and says nothing about what it could not reach. A
3059
+ The test stops where the task offers no way on and says nothing about what it could not reach. A
3038
3060
  softphone adapter written for a browser needs its audio APIs supplied by whatever runs the
3039
- harness: the drive opens audio outside a browser, and a provider that quietly stopped carrying audio
3061
+ harness: the test opens audio outside a browser, and a provider that quietly stopped carrying audio
3040
3062
  where `AudioContext` was missing would be lying about the one thing the channel is for. It is off by default because it issues
3041
3063
  commands against whatever platform the adapter is connected to: turn it on against a test backend.
3042
3064
 
3043
3065
  ```ts
3044
- const driven = await exerciseAdapter(adapter, context, { collectOnly: true, drive: true });
3045
- expect(driven.violations).toEqual([]);
3046
- assertReached(driven, ["task.onCall", "task.audio", "event.task-ended"]);
3066
+ const tested = await testAdapter(adapter, context, { collectOnly: true, withCall: true });
3067
+ expect(tested.violations).toEqual([]);
3068
+ assertReached(tested, ["task.onCall", "task.audio", "event.task-ended"]);
3047
3069
  ```
3048
3070
  `assertReached(result, subjects)` is the paired assertion: it throws naming every subject the run
3049
3071
  never met, so a test that meant to check a team member list cannot pass on a fixture that never produced one.
@@ -3074,7 +3096,7 @@ cannot be established from TypeScript structure alone.
3074
3096
  | `assertCapabilityWithdrawal(states, snapshot, manifest)` | A capability withdrawn by a later `authenticated` state is gone from the next snapshot: no team member list for a login that no longer leads. Every state is validated on the way, `refreshing` must carry the login over, and the sequence passes only through usable states. |
3075
3097
  | `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. |
3076
3098
  | `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. |
3077
- | `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |
3099
+ | `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `testAdapter` result. |
3078
3100
  | `assertAuthenticationRestoreAndExpiry(states)` | A restored authenticated session can refresh and ends in expiry. Every state is validated. |
3079
3101
  | `assertReconnectWithMissedAssignments(before, reconnect, ids)` | A reconnect snapshot restores assignments received while offline. |
3080
3102
  | `stillHost(report?, guarantees?, mute?)` | An agent application that reports one thing and never changes, for a test context: `{ online: true }` by default, a report with audio for a softphone voice adapter, and never for a desk phone. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.111",
3
+ "version": "0.1.113",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",