@absolutejs/voice 0.0.22-beta.297 → 0.0.22-beta.298

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
@@ -4116,7 +4116,7 @@ app.use(
4116
4116
  );
4117
4117
  ```
4118
4118
 
4119
- The routes expose JSON at `/api/voice/provider-decisions`, HTML at `/voice/provider-decisions`, and Markdown at `/voice/provider-decisions.md`. Use this next to provider SLOs when a customer asks not just "is fallback working?" but "why did the system choose this provider for this call?".
4119
+ The routes expose JSON at `/api/voice/provider-decisions`, HTML at `/voice/provider-decisions`, and Markdown at `/voice/provider-decisions.md`. Use this next to provider SLOs when a customer asks not just "is fallback working?" but "why did the system choose this provider for this call?". For proof packs, gate fallback and degradation directly with `minFallbacks`, `minDegraded`, `requiredStatuses`, `requiredFallbackProviders`, and `requiredReasonIncludes` so deploy evidence fails when fallback behavior is missing or unexplained.
4120
4120
 
4121
4121
  Use `createVoiceProviderContractMatrixPreset(...)` when you want readiness proof for the whole provider stack without hand-writing every LLM, STT, and TTS contract row. The preset stays primitive: you still own provider lists, selected providers, latency budgets, env, capabilities, and route mounting.
4122
4122
 
package/dist/index.js CHANGED
@@ -13146,7 +13146,7 @@ var uniqueSorted = (values) => [
13146
13146
  ].sort();
13147
13147
  var createVoiceProviderDecisionTraceEvent = (input) => {
13148
13148
  const surface = input.surface ?? surfaceForKind(input.kind);
13149
- const reason = input.reason ?? (input.status === "fallback" ? `Fallback from ${input.provider} to ${input.fallbackProvider ?? input.selectedProvider ?? "next provider"}.` : input.status === "error" ? `Provider ${input.provider} errored before recovery.` : input.status === "skipped" ? `Provider ${input.provider} was skipped by policy.` : `Provider ${input.selectedProvider ?? input.provider} selected by policy.`);
13149
+ const reason = input.reason ?? (input.status === "degraded" ? `Provider ${input.provider} degraded to ${input.fallbackProvider ?? input.selectedProvider ?? "lower-fidelity fallback"}.` : input.status === "fallback" ? `Fallback from ${input.provider} to ${input.fallbackProvider ?? input.selectedProvider ?? "next provider"}.` : input.status === "error" ? `Provider ${input.provider} errored before recovery.` : input.status === "skipped" ? `Provider ${input.provider} was skipped by policy.` : `Provider ${input.selectedProvider ?? input.provider} selected by policy.`);
13150
13150
  return {
13151
13151
  at: input.at ?? Date.now(),
13152
13152
  payload: {
@@ -13170,7 +13170,7 @@ var listVoiceProviderDecisionTraces = (events) => {
13170
13170
  const provider = getString8(event.payload.provider);
13171
13171
  const status = getString8(event.payload.status);
13172
13172
  const surface = getString8(event.payload.surface);
13173
- if (!provider || !surface || status !== "error" && status !== "fallback" && status !== "selected" && status !== "skipped" && status !== "success") {
13173
+ if (!provider || !surface || status !== "error" && status !== "fallback" && status !== "degraded" && status !== "selected" && status !== "skipped" && status !== "success") {
13174
13174
  return;
13175
13175
  }
13176
13176
  return {
@@ -13237,6 +13237,18 @@ var buildVoiceProviderDecisionTraceReport = async (options) => {
13237
13237
  });
13238
13238
  }
13239
13239
  }
13240
+ const fallbackCount = decisions.filter((decision) => decision.status === "fallback").length;
13241
+ const degradedCount = decisions.filter((decision) => decision.status === "degraded").length;
13242
+ const statuses = new Set(decisions.map((decision) => decision.status));
13243
+ const providers = uniqueSorted(decisions.flatMap((decision) => [
13244
+ decision.provider,
13245
+ decision.selectedProvider,
13246
+ decision.fallbackProvider
13247
+ ]));
13248
+ const fallbackProviders = uniqueSorted(decisions.flatMap((decision) => [
13249
+ decision.fallbackProvider,
13250
+ decision.status === "fallback" || decision.status === "degraded" ? decision.selectedProvider : undefined
13251
+ ]));
13240
13252
  if (options.minDecisions !== undefined && decisions.length < options.minDecisions) {
13241
13253
  issues.push({
13242
13254
  code: "voice.provider_decision_trace.min_decisions",
@@ -13244,9 +13256,60 @@ var buildVoiceProviderDecisionTraceReport = async (options) => {
13244
13256
  status: "fail"
13245
13257
  });
13246
13258
  }
13259
+ if (options.minFallbacks !== undefined && fallbackCount < options.minFallbacks) {
13260
+ issues.push({
13261
+ code: "voice.provider_decision_trace.min_fallbacks",
13262
+ message: `Found ${String(fallbackCount)} provider fallback trace(s); expected at least ${String(options.minFallbacks)}.`,
13263
+ status: "fail"
13264
+ });
13265
+ }
13266
+ if (options.minDegraded !== undefined && degradedCount < options.minDegraded) {
13267
+ issues.push({
13268
+ code: "voice.provider_decision_trace.min_degraded",
13269
+ message: `Found ${String(degradedCount)} provider degradation trace(s); expected at least ${String(options.minDegraded)}.`,
13270
+ status: "fail"
13271
+ });
13272
+ }
13273
+ for (const status of options.requiredStatuses ?? []) {
13274
+ if (!statuses.has(status)) {
13275
+ issues.push({
13276
+ code: "voice.provider_decision_trace.status_missing",
13277
+ message: `Missing provider decision status: ${status}.`,
13278
+ status: "fail"
13279
+ });
13280
+ }
13281
+ }
13282
+ for (const provider of options.requiredProviders ?? []) {
13283
+ if (!providers.includes(provider)) {
13284
+ issues.push({
13285
+ code: "voice.provider_decision_trace.provider_missing",
13286
+ message: `Missing provider decision provider: ${provider}.`,
13287
+ status: "fail"
13288
+ });
13289
+ }
13290
+ }
13291
+ for (const provider of options.requiredFallbackProviders ?? []) {
13292
+ if (!fallbackProviders.includes(provider)) {
13293
+ issues.push({
13294
+ code: "voice.provider_decision_trace.fallback_provider_missing",
13295
+ message: `Missing provider decision fallback provider: ${provider}.`,
13296
+ status: "fail"
13297
+ });
13298
+ }
13299
+ }
13300
+ for (const phrase of options.requiredReasonIncludes ?? []) {
13301
+ if (!decisions.some((decision) => decision.reason.includes(phrase))) {
13302
+ issues.push({
13303
+ code: "voice.provider_decision_trace.reason_missing",
13304
+ message: `Missing provider decision reason containing: ${phrase}.`,
13305
+ status: "fail"
13306
+ });
13307
+ }
13308
+ }
13247
13309
  const surfaceReports = [...surfaces.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([surface, surfaceDecisions]) => {
13248
13310
  const surfaceIssues = issues.filter((issue) => issue.surface === surface);
13249
13311
  return {
13312
+ degraded: surfaceDecisions.filter((decision) => decision.status === "degraded").length,
13250
13313
  decisions: surfaceDecisions.length,
13251
13314
  errors: surfaceDecisions.filter((decision) => decision.status === "error").length,
13252
13315
  fallbacks: surfaceDecisions.filter((decision) => decision.status === "fallback").length,
@@ -13263,20 +13326,16 @@ var buildVoiceProviderDecisionTraceReport = async (options) => {
13263
13326
  surface
13264
13327
  };
13265
13328
  });
13266
- const providers = uniqueSorted(decisions.flatMap((decision) => [
13267
- decision.provider,
13268
- decision.selectedProvider,
13269
- decision.fallbackProvider
13270
- ]));
13271
13329
  return {
13272
13330
  checkedAt: now,
13273
13331
  decisions,
13274
13332
  issues,
13275
13333
  status: reportStatus(issues),
13276
13334
  summary: {
13335
+ degraded: degradedCount,
13277
13336
  decisions: decisions.length,
13278
13337
  errors: decisions.filter((decision) => decision.status === "error").length,
13279
- fallbacks: decisions.filter((decision) => decision.status === "fallback").length,
13338
+ fallbacks: fallbackCount,
13280
13339
  providers: providers.length,
13281
13340
  selected: decisions.filter((decision) => decision.status === "selected" || decision.status === "success").length,
13282
13341
  surfaces: surfaces.size
@@ -13291,11 +13350,12 @@ var renderVoiceProviderDecisionTraceMarkdown = (report) => [
13291
13350
  `Decisions: ${String(report.summary.decisions)}`,
13292
13351
  `Providers: ${String(report.summary.providers)}`,
13293
13352
  `Fallbacks: ${String(report.summary.fallbacks)}`,
13353
+ `Degraded: ${String(report.summary.degraded)}`,
13294
13354
  `Errors: ${String(report.summary.errors)}`,
13295
13355
  "",
13296
- "| Surface | Status | Decisions | Selected | Fallbacks | Errors | Providers |",
13297
- "| --- | --- | ---: | ---: | ---: | ---: | --- |",
13298
- ...report.surfaces.map((surface) => `| ${surface.surface} | ${surface.status} | ${String(surface.decisions)} | ${String(surface.selected)} | ${String(surface.fallbacks)} | ${String(surface.errors)} | ${surface.providers.join(", ")} |`),
13356
+ "| Surface | Status | Decisions | Selected | Fallbacks | Degraded | Errors | Providers |",
13357
+ "| --- | --- | ---: | ---: | ---: | ---: | ---: | --- |",
13358
+ ...report.surfaces.map((surface) => `| ${surface.surface} | ${surface.status} | ${String(surface.decisions)} | ${String(surface.selected)} | ${String(surface.fallbacks)} | ${String(surface.degraded)} | ${String(surface.errors)} | ${surface.providers.join(", ")} |`),
13299
13359
  "",
13300
13360
  ...report.issues.map((issue) => `- ${issue.status}: ${issue.message}`)
13301
13361
  ].join(`
@@ -13326,12 +13386,13 @@ code{background:#e2e8f0;border-radius:8px;padding:2px 6px}
13326
13386
  <article class="card"><strong>${String(report.summary.decisions)}</strong><p>decisions</p></article>
13327
13387
  <article class="card"><strong>${String(report.summary.providers)}</strong><p>providers</p></article>
13328
13388
  <article class="card"><strong>${String(report.summary.fallbacks)}</strong><p>fallbacks</p></article>
13389
+ <article class="card"><strong>${String(report.summary.degraded)}</strong><p>degraded</p></article>
13329
13390
  <article class="card"><strong>${String(report.summary.errors)}</strong><p>errors</p></article>
13330
13391
  </section>
13331
13392
  <section class="surfaces">
13332
13393
  ${report.surfaces.map((surface) => `<article class="surface">
13333
13394
  <header><strong>${escapeHtml17(surface.surface)}</strong> <span class="status ${surface.status}">${escapeHtml17(surface.status)}</span></header>
13334
- <p>${String(surface.decisions)} decision(s), ${String(surface.fallbacks)} fallback(s), ${String(surface.errors)} error(s).</p>
13395
+ <p>${String(surface.decisions)} decision(s), ${String(surface.fallbacks)} fallback(s), ${String(surface.degraded)} degraded decision(s), ${String(surface.errors)} error(s).</p>
13335
13396
  <p class="muted">Providers: ${escapeHtml17(surface.providers.join(", ") || "none")}</p>
13336
13397
  <p>${surface.reasons.map((reason) => `<code>${escapeHtml17(reason)}</code>`).join(" ")}</p>
13337
13398
  </article>`).join(`
@@ -1,7 +1,7 @@
1
1
  import { Elysia } from 'elysia';
2
2
  import { type VoiceRoutingEventKind } from './resilienceRoutes';
3
3
  import type { StoredVoiceTraceEvent, VoiceTraceEvent, VoiceTraceEventStore } from './trace';
4
- export type VoiceProviderDecisionStatus = 'error' | 'fallback' | 'selected' | 'skipped' | 'success';
4
+ export type VoiceProviderDecisionStatus = 'degraded' | 'error' | 'fallback' | 'selected' | 'skipped' | 'success';
5
5
  export type VoiceProviderDecisionTrace = {
6
6
  at: number;
7
7
  elapsedMs?: number;
@@ -31,6 +31,7 @@ export type VoiceProviderDecisionTraceIssue = {
31
31
  surface?: string;
32
32
  };
33
33
  export type VoiceProviderDecisionSurfaceReport = {
34
+ degraded: number;
34
35
  decisions: number;
35
36
  errors: number;
36
37
  fallbacks: number;
@@ -48,6 +49,7 @@ export type VoiceProviderDecisionTraceReport = {
48
49
  issues: VoiceProviderDecisionTraceIssue[];
49
50
  status: 'fail' | 'pass' | 'warn';
50
51
  summary: {
52
+ degraded: number;
51
53
  decisions: number;
52
54
  errors: number;
53
55
  fallbacks: number;
@@ -60,9 +62,15 @@ export type VoiceProviderDecisionTraceReport = {
60
62
  export type VoiceProviderDecisionTraceReportOptions = {
61
63
  events?: StoredVoiceTraceEvent[] | VoiceProviderDecisionTrace[];
62
64
  maxAgeMs?: number;
65
+ minDegraded?: number;
63
66
  minDecisions?: number;
67
+ minFallbacks?: number;
64
68
  now?: number;
69
+ requiredFallbackProviders?: readonly string[];
70
+ requiredProviders?: readonly string[];
71
+ requiredReasonIncludes?: readonly string[];
65
72
  requiredSurfaces?: readonly string[];
73
+ requiredStatuses?: readonly VoiceProviderDecisionStatus[];
66
74
  sessionId?: string;
67
75
  store?: VoiceTraceEventStore;
68
76
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.297",
3
+ "version": "0.0.22-beta.298",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",