@absolutejs/voice 0.0.22-beta.167 → 0.0.22-beta.169

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.
@@ -3156,6 +3156,80 @@ var createVoiceProviderCapabilitiesStore = (path = "/api/provider-capabilities",
3156
3156
  }
3157
3157
  };
3158
3158
  };
3159
+ // src/client/providerContracts.ts
3160
+ var fetchVoiceProviderContracts = async (path = "/api/provider-contracts", options = {}) => {
3161
+ const fetchImpl = options.fetch ?? globalThis.fetch;
3162
+ const response = await fetchImpl(path);
3163
+ if (!response.ok) {
3164
+ throw new Error(`Voice provider contracts failed: HTTP ${response.status}`);
3165
+ }
3166
+ return await response.json();
3167
+ };
3168
+ var createVoiceProviderContractsStore = (path = "/api/provider-contracts", options = {}) => {
3169
+ const listeners = new Set;
3170
+ let closed = false;
3171
+ let timer;
3172
+ let snapshot = {
3173
+ error: null,
3174
+ isLoading: false
3175
+ };
3176
+ const emit = () => {
3177
+ for (const listener of listeners) {
3178
+ listener();
3179
+ }
3180
+ };
3181
+ const refresh = async () => {
3182
+ if (closed) {
3183
+ return snapshot.report;
3184
+ }
3185
+ snapshot = { ...snapshot, error: null, isLoading: true };
3186
+ emit();
3187
+ try {
3188
+ const report = await fetchVoiceProviderContracts(path, options);
3189
+ snapshot = {
3190
+ error: null,
3191
+ isLoading: false,
3192
+ report,
3193
+ updatedAt: Date.now()
3194
+ };
3195
+ emit();
3196
+ return report;
3197
+ } catch (error) {
3198
+ snapshot = {
3199
+ ...snapshot,
3200
+ error: error instanceof Error ? error.message : String(error),
3201
+ isLoading: false
3202
+ };
3203
+ emit();
3204
+ throw error;
3205
+ }
3206
+ };
3207
+ const close = () => {
3208
+ closed = true;
3209
+ if (timer) {
3210
+ clearInterval(timer);
3211
+ timer = undefined;
3212
+ }
3213
+ listeners.clear();
3214
+ };
3215
+ if (options.intervalMs && options.intervalMs > 0) {
3216
+ timer = setInterval(() => {
3217
+ refresh().catch(() => {});
3218
+ }, options.intervalMs);
3219
+ }
3220
+ return {
3221
+ close,
3222
+ getServerSnapshot: () => snapshot,
3223
+ getSnapshot: () => snapshot,
3224
+ refresh,
3225
+ subscribe: (listener) => {
3226
+ listeners.add(listener);
3227
+ return () => {
3228
+ listeners.delete(listener);
3229
+ };
3230
+ }
3231
+ };
3232
+ };
3159
3233
  // src/client/turnQuality.ts
3160
3234
  var fetchVoiceTurnQuality = async (path = "/api/turn-quality", options = {}) => {
3161
3235
  const fetchImpl = options.fetch ?? globalThis.fetch;
@@ -3939,10 +4013,116 @@ var defineVoiceProviderCapabilitiesElement = (tagName = "absolute-voice-provider
3939
4013
  }
3940
4014
  });
3941
4015
  };
3942
- // src/client/turnQualityWidget.ts
3943
- var DEFAULT_TITLE7 = "Turn Quality";
3944
- var DEFAULT_DESCRIPTION7 = "Per-turn STT confidence, fallback selection, corrections, and transcript coverage.";
4016
+ // src/client/providerContractsWidget.ts
4017
+ var DEFAULT_TITLE7 = "Provider Contracts";
4018
+ var DEFAULT_DESCRIPTION7 = "Production contract coverage for provider env, latency, fallback, streaming, and capabilities.";
3945
4019
  var escapeHtml9 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
4020
+ var formatProvider3 = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ") || provider;
4021
+ var formatStatus3 = (status) => status.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
4022
+ var contractDetail = (row) => {
4023
+ const failing = row.checks.filter((check) => check.status !== "pass");
4024
+ if (failing.length === 0) {
4025
+ return "Provider contract is production-ready.";
4026
+ }
4027
+ return failing.map((check) => `${check.label}: ${check.detail ?? check.status}`).join(" ");
4028
+ };
4029
+ var createVoiceProviderContractsViewModel = (snapshot, options = {}) => {
4030
+ const rows = (snapshot.report?.rows ?? []).map((row) => ({
4031
+ ...row,
4032
+ detail: contractDetail(row),
4033
+ label: `${formatProvider3(row.provider)} ${row.kind.toUpperCase()}`,
4034
+ remediations: row.checks.filter((check) => check.status !== "pass" && check.remediation).map((check) => ({
4035
+ detail: check.remediation?.detail ?? "",
4036
+ href: check.remediation?.href,
4037
+ label: check.remediation?.label ?? check.label
4038
+ })),
4039
+ rows: [
4040
+ { label: "Status", value: formatStatus3(row.status) },
4041
+ { label: "Selected", value: row.selected ? "Yes" : "No" },
4042
+ { label: "Configured", value: row.configured ? "Yes" : "No" },
4043
+ {
4044
+ label: "Checks",
4045
+ value: row.checks.map((check) => `${check.label}: ${formatStatus3(check.status)}`).join(", ")
4046
+ }
4047
+ ]
4048
+ }));
4049
+ const warningCount = snapshot.report ? snapshot.report.failed + snapshot.report.warned : rows.filter((row) => row.status !== "pass").length;
4050
+ return {
4051
+ description: options.description ?? DEFAULT_DESCRIPTION7,
4052
+ error: snapshot.error,
4053
+ isLoading: snapshot.isLoading,
4054
+ label: snapshot.error ? "Unavailable" : rows.length ? warningCount > 0 ? `${warningCount} needs attention` : `${rows.length} passing` : snapshot.isLoading ? "Checking" : "No contracts",
4055
+ rows,
4056
+ status: snapshot.error ? "error" : rows.length ? warningCount > 0 ? "warning" : "ready" : snapshot.isLoading ? "loading" : "empty",
4057
+ title: options.title ?? DEFAULT_TITLE7,
4058
+ updatedAt: snapshot.updatedAt
4059
+ };
4060
+ };
4061
+ var renderVoiceProviderContractsHTML = (snapshot, options = {}) => {
4062
+ const model = createVoiceProviderContractsViewModel(snapshot, options);
4063
+ const rows = model.rows.length ? `<div class="absolute-voice-provider-contracts__rows">${model.rows.map((row) => `<article class="absolute-voice-provider-contracts__row absolute-voice-provider-contracts__row--${escapeHtml9(row.status)}">
4064
+ <header>
4065
+ <strong>${escapeHtml9(row.label)}</strong>
4066
+ <span>${escapeHtml9(formatStatus3(row.status))}</span>
4067
+ </header>
4068
+ <p>${escapeHtml9(row.detail)}</p>
4069
+ ${row.remediations.length ? `<ul class="absolute-voice-provider-contracts__remediations">${row.remediations.map((remediation) => `<li>${remediation.href ? `<a href="${escapeHtml9(remediation.href)}">${escapeHtml9(remediation.label)}</a>` : `<strong>${escapeHtml9(remediation.label)}</strong>`}<span>${escapeHtml9(remediation.detail)}</span></li>`).join("")}</ul>` : ""}
4070
+ <dl>${row.rows.map((item) => `<div>
4071
+ <dt>${escapeHtml9(item.label)}</dt>
4072
+ <dd>${escapeHtml9(item.value)}</dd>
4073
+ </div>`).join("")}</dl>
4074
+ </article>`).join("")}</div>` : '<p class="absolute-voice-provider-contracts__empty">Configure provider contracts to see production coverage.</p>';
4075
+ return `<section class="absolute-voice-provider-contracts absolute-voice-provider-contracts--${escapeHtml9(model.status)}">
4076
+ <header class="absolute-voice-provider-contracts__header">
4077
+ <span class="absolute-voice-provider-contracts__eyebrow">${escapeHtml9(model.title)}</span>
4078
+ <strong class="absolute-voice-provider-contracts__label">${escapeHtml9(model.label)}</strong>
4079
+ </header>
4080
+ <p class="absolute-voice-provider-contracts__description">${escapeHtml9(model.description)}</p>
4081
+ ${rows}
4082
+ ${model.error ? `<p class="absolute-voice-provider-contracts__error">${escapeHtml9(model.error)}</p>` : ""}
4083
+ </section>`;
4084
+ };
4085
+ var getVoiceProviderContractsCSS = () => `.absolute-voice-provider-contracts{border:1px solid #b8dcc7;border-radius:20px;background:#f7fff9;color:#09140d;padding:18px;box-shadow:0 18px 40px rgba(21,83,45,.12);font-family:inherit}.absolute-voice-provider-contracts--error,.absolute-voice-provider-contracts--warning{border-color:#f2a7a7;background:#fff7f4}.absolute-voice-provider-contracts__header,.absolute-voice-provider-contracts__row header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-provider-contracts__eyebrow{color:#166534;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-provider-contracts__label{font-size:24px;line-height:1}.absolute-voice-provider-contracts__description,.absolute-voice-provider-contracts__row p,.absolute-voice-provider-contracts__row dt,.absolute-voice-provider-contracts__empty{color:#405448}.absolute-voice-provider-contracts__rows{display:grid;gap:12px;margin-top:14px}.absolute-voice-provider-contracts__row{background:#fff;border:1px solid #d6eadb;border-radius:16px;padding:14px}.absolute-voice-provider-contracts__row--pass{border-color:#86efac}.absolute-voice-provider-contracts__row--warn,.absolute-voice-provider-contracts__row--fail{border-color:#f2a7a7}.absolute-voice-provider-contracts__row p{margin:10px 0}.absolute-voice-provider-contracts__remediations{display:grid;gap:8px;list-style:none;margin:0 0 10px;padding:0}.absolute-voice-provider-contracts__remediations li{background:#fff7ed;border:1px solid #fed7aa;border-radius:12px;display:grid;gap:3px;padding:8px}.absolute-voice-provider-contracts__remediations a,.absolute-voice-provider-contracts__remediations strong{color:#9a3412}.absolute-voice-provider-contracts__remediations span{color:#7c2d12}.absolute-voice-provider-contracts__row dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:0}.absolute-voice-provider-contracts__row div{background:#f7fff9;border:1px solid #d6eadb;border-radius:12px;padding:8px}.absolute-voice-provider-contracts__row dt{font-size:12px}.absolute-voice-provider-contracts__row dd{font-weight:800;margin:4px 0 0}.absolute-voice-provider-contracts__error{color:#9f1239;font-weight:700}`;
4086
+ var mountVoiceProviderContracts = (element, path = "/api/provider-contracts", options = {}) => {
4087
+ const store = createVoiceProviderContractsStore(path, options);
4088
+ const render = () => {
4089
+ element.innerHTML = renderVoiceProviderContractsHTML(store.getSnapshot(), options);
4090
+ };
4091
+ const unsubscribe = store.subscribe(render);
4092
+ render();
4093
+ store.refresh().catch(() => {});
4094
+ return {
4095
+ close: () => {
4096
+ unsubscribe();
4097
+ store.close();
4098
+ },
4099
+ refresh: store.refresh
4100
+ };
4101
+ };
4102
+ var defineVoiceProviderContractsElement = (tagName = "absolute-voice-provider-contracts") => {
4103
+ if (typeof window === "undefined" || typeof customElements === "undefined" || customElements.get(tagName)) {
4104
+ return;
4105
+ }
4106
+ customElements.define(tagName, class AbsoluteVoiceProviderContractsElement extends HTMLElement {
4107
+ mounted;
4108
+ connectedCallback() {
4109
+ const intervalMs = Number(this.getAttribute("interval-ms") ?? 5000);
4110
+ this.mounted = mountVoiceProviderContracts(this, this.getAttribute("path") ?? "/api/provider-contracts", {
4111
+ description: this.getAttribute("description") ?? undefined,
4112
+ intervalMs: Number.isFinite(intervalMs) ? intervalMs : 5000,
4113
+ title: this.getAttribute("title") ?? undefined
4114
+ });
4115
+ }
4116
+ disconnectedCallback() {
4117
+ this.mounted?.close();
4118
+ this.mounted = undefined;
4119
+ }
4120
+ });
4121
+ };
4122
+ // src/client/turnQualityWidget.ts
4123
+ var DEFAULT_TITLE8 = "Turn Quality";
4124
+ var DEFAULT_DESCRIPTION8 = "Per-turn STT confidence, fallback selection, corrections, and transcript coverage.";
4125
+ var escapeHtml10 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
3946
4126
  var formatConfidence = (value) => typeof value === "number" ? `${Math.round(value * 100)}%` : "n/a";
3947
4127
  var formatMaybe = (value) => value === undefined || value === "" ? "n/a" : String(value);
3948
4128
  var getTurnDetail = (turn) => {
@@ -3980,37 +4160,37 @@ var createVoiceTurnQualityViewModel = (snapshot, options = {}) => {
3980
4160
  const warningCount = snapshot.report?.warnings ?? turns.filter((turn) => turn.status === "warn").length;
3981
4161
  const failedCount = snapshot.report?.failed ?? turns.filter((turn) => turn.status === "fail").length;
3982
4162
  return {
3983
- description: options.description ?? DEFAULT_DESCRIPTION7,
4163
+ description: options.description ?? DEFAULT_DESCRIPTION8,
3984
4164
  error: snapshot.error,
3985
4165
  isLoading: snapshot.isLoading,
3986
4166
  label: snapshot.error ? "Unavailable" : turns.length ? failedCount > 0 ? `${failedCount} failed` : warningCount > 0 ? `${warningCount} warnings` : `${turns.length} healthy` : snapshot.isLoading ? "Checking" : "No turns",
3987
4167
  status: snapshot.error ? "error" : turns.length ? failedCount > 0 || warningCount > 0 ? "warning" : "ready" : snapshot.isLoading ? "loading" : "empty",
3988
- title: options.title ?? DEFAULT_TITLE7,
4168
+ title: options.title ?? DEFAULT_TITLE8,
3989
4169
  turns,
3990
4170
  updatedAt: snapshot.updatedAt
3991
4171
  };
3992
4172
  };
3993
4173
  var renderVoiceTurnQualityHTML = (snapshot, options = {}) => {
3994
4174
  const model = createVoiceTurnQualityViewModel(snapshot, options);
3995
- const turns = model.turns.length ? `<div class="absolute-voice-turn-quality__turns">${model.turns.map((turn) => `<article class="absolute-voice-turn-quality__turn absolute-voice-turn-quality__turn--${escapeHtml9(turn.status)}">
4175
+ const turns = model.turns.length ? `<div class="absolute-voice-turn-quality__turns">${model.turns.map((turn) => `<article class="absolute-voice-turn-quality__turn absolute-voice-turn-quality__turn--${escapeHtml10(turn.status)}">
3996
4176
  <header>
3997
- <strong>${escapeHtml9(turn.label)}</strong>
3998
- <span>${escapeHtml9(turn.status)}</span>
4177
+ <strong>${escapeHtml10(turn.label)}</strong>
4178
+ <span>${escapeHtml10(turn.status)}</span>
3999
4179
  </header>
4000
- <p>${escapeHtml9(turn.detail)}</p>
4180
+ <p>${escapeHtml10(turn.detail)}</p>
4001
4181
  <dl>${turn.rows.map((row) => `<div>
4002
- <dt>${escapeHtml9(row.label)}</dt>
4003
- <dd>${escapeHtml9(row.value)}</dd>
4182
+ <dt>${escapeHtml10(row.label)}</dt>
4183
+ <dd>${escapeHtml10(row.value)}</dd>
4004
4184
  </div>`).join("")}</dl>
4005
4185
  </article>`).join("")}</div>` : '<p class="absolute-voice-turn-quality__empty">Complete a voice turn to see STT quality diagnostics.</p>';
4006
- return `<section class="absolute-voice-turn-quality absolute-voice-turn-quality--${escapeHtml9(model.status)}">
4186
+ return `<section class="absolute-voice-turn-quality absolute-voice-turn-quality--${escapeHtml10(model.status)}">
4007
4187
  <header class="absolute-voice-turn-quality__header">
4008
- <span class="absolute-voice-turn-quality__eyebrow">${escapeHtml9(model.title)}</span>
4009
- <strong class="absolute-voice-turn-quality__label">${escapeHtml9(model.label)}</strong>
4188
+ <span class="absolute-voice-turn-quality__eyebrow">${escapeHtml10(model.title)}</span>
4189
+ <strong class="absolute-voice-turn-quality__label">${escapeHtml10(model.label)}</strong>
4010
4190
  </header>
4011
- <p class="absolute-voice-turn-quality__description">${escapeHtml9(model.description)}</p>
4191
+ <p class="absolute-voice-turn-quality__description">${escapeHtml10(model.description)}</p>
4012
4192
  ${turns}
4013
- ${model.error ? `<p class="absolute-voice-turn-quality__error">${escapeHtml9(model.error)}</p>` : ""}
4193
+ ${model.error ? `<p class="absolute-voice-turn-quality__error">${escapeHtml10(model.error)}</p>` : ""}
4014
4194
  </section>`;
4015
4195
  };
4016
4196
  var getVoiceTurnQualityCSS = () => `.absolute-voice-turn-quality{border:1px solid #e4d1a3;border-radius:20px;background:#fff9eb;color:#17120a;padding:18px;box-shadow:0 18px 40px rgba(73,48,14,.12);font-family:inherit}.absolute-voice-turn-quality--error,.absolute-voice-turn-quality--warning{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-turn-quality__header,.absolute-voice-turn-quality__turn header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-turn-quality__eyebrow{color:#8a5a0a;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-turn-quality__label{font-size:24px;line-height:1}.absolute-voice-turn-quality__description,.absolute-voice-turn-quality__turn p,.absolute-voice-turn-quality__turn dt,.absolute-voice-turn-quality__empty{color:#5a4930}.absolute-voice-turn-quality__turns{display:grid;gap:12px;margin-top:14px}.absolute-voice-turn-quality__turn{background:#fff;border:1px solid #f0dfba;border-radius:16px;padding:14px}.absolute-voice-turn-quality__turn--pass{border-color:#86efac}.absolute-voice-turn-quality__turn--warn,.absolute-voice-turn-quality__turn--unknown{border-color:#fbbf24}.absolute-voice-turn-quality__turn--fail{border-color:#f2a7a7}.absolute-voice-turn-quality__turn p{margin:10px 0}.absolute-voice-turn-quality__turn dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:0}.absolute-voice-turn-quality__turn div{background:#fff9eb;border:1px solid #f0dfba;border-radius:12px;padding:8px}.absolute-voice-turn-quality__turn dt{font-size:12px}.absolute-voice-turn-quality__turn dd{font-weight:800;margin:4px 0 0}.absolute-voice-turn-quality__empty{margin:14px 0 0}.absolute-voice-turn-quality__error{color:#9f1239;font-weight:700}`;
@@ -4051,10 +4231,10 @@ var defineVoiceTurnQualityElement = (tagName = "absolute-voice-turn-quality") =>
4051
4231
  });
4052
4232
  };
4053
4233
  // src/client/turnLatencyWidget.ts
4054
- var DEFAULT_TITLE8 = "Turn Latency";
4055
- var DEFAULT_DESCRIPTION8 = "Per-turn timing from first transcript to commit and assistant response start.";
4234
+ var DEFAULT_TITLE9 = "Turn Latency";
4235
+ var DEFAULT_DESCRIPTION9 = "Per-turn timing from first transcript to commit and assistant response start.";
4056
4236
  var DEFAULT_PROOF_LABEL = "Run latency proof";
4057
- var escapeHtml10 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
4237
+ var escapeHtml11 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
4058
4238
  var formatMs = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
4059
4239
  var createVoiceTurnLatencyViewModel = (snapshot, options = {}) => {
4060
4240
  const turns = (snapshot.report?.turns ?? []).map((turn) => ({
@@ -4068,39 +4248,39 @@ var createVoiceTurnLatencyViewModel = (snapshot, options = {}) => {
4068
4248
  const warningCount = snapshot.report?.warnings ?? turns.filter((turn) => turn.status === "warn").length;
4069
4249
  const failedCount = snapshot.report?.failed ?? turns.filter((turn) => turn.status === "fail").length;
4070
4250
  return {
4071
- description: options.description ?? DEFAULT_DESCRIPTION8,
4251
+ description: options.description ?? DEFAULT_DESCRIPTION9,
4072
4252
  error: snapshot.error,
4073
4253
  isLoading: snapshot.isLoading,
4074
4254
  label: snapshot.error ? "Unavailable" : turns.length ? failedCount > 0 ? `${failedCount} slow` : warningCount > 0 ? `${warningCount} warnings` : `avg ${formatMs(snapshot.report?.averageTotalMs)}` : snapshot.isLoading ? "Checking" : "No turns",
4075
4255
  proofLabel: options.proofPath ? options.proofLabel ?? DEFAULT_PROOF_LABEL : undefined,
4076
4256
  showProofAction: Boolean(options.proofPath),
4077
4257
  status: snapshot.error ? "error" : turns.length ? failedCount > 0 || warningCount > 0 ? "warning" : "ready" : snapshot.isLoading ? "loading" : "empty",
4078
- title: options.title ?? DEFAULT_TITLE8,
4258
+ title: options.title ?? DEFAULT_TITLE9,
4079
4259
  turns,
4080
4260
  updatedAt: snapshot.updatedAt
4081
4261
  };
4082
4262
  };
4083
4263
  var renderVoiceTurnLatencyHTML = (snapshot, options = {}) => {
4084
4264
  const model = createVoiceTurnLatencyViewModel(snapshot, options);
4085
- const turns = model.turns.length ? `<div class="absolute-voice-turn-latency__turns">${model.turns.map((turn) => `<article class="absolute-voice-turn-latency__turn absolute-voice-turn-latency__turn--${escapeHtml10(turn.status)}">
4265
+ const turns = model.turns.length ? `<div class="absolute-voice-turn-latency__turns">${model.turns.map((turn) => `<article class="absolute-voice-turn-latency__turn absolute-voice-turn-latency__turn--${escapeHtml11(turn.status)}">
4086
4266
  <header>
4087
- <strong>${escapeHtml10(turn.label)}</strong>
4088
- <span>${escapeHtml10(turn.status)}</span>
4267
+ <strong>${escapeHtml11(turn.label)}</strong>
4268
+ <span>${escapeHtml11(turn.status)}</span>
4089
4269
  </header>
4090
4270
  <dl>${turn.rows.map((row) => `<div>
4091
- <dt>${escapeHtml10(row.label)}</dt>
4092
- <dd>${escapeHtml10(row.value)}</dd>
4271
+ <dt>${escapeHtml11(row.label)}</dt>
4272
+ <dd>${escapeHtml11(row.value)}</dd>
4093
4273
  </div>`).join("")}</dl>
4094
4274
  </article>`).join("")}</div>` : '<p class="absolute-voice-turn-latency__empty">Complete a voice turn to see latency diagnostics.</p>';
4095
- return `<section class="absolute-voice-turn-latency absolute-voice-turn-latency--${escapeHtml10(model.status)}">
4275
+ return `<section class="absolute-voice-turn-latency absolute-voice-turn-latency--${escapeHtml11(model.status)}">
4096
4276
  <header class="absolute-voice-turn-latency__header">
4097
- <span class="absolute-voice-turn-latency__eyebrow">${escapeHtml10(model.title)}</span>
4098
- <strong class="absolute-voice-turn-latency__label">${escapeHtml10(model.label)}</strong>
4277
+ <span class="absolute-voice-turn-latency__eyebrow">${escapeHtml11(model.title)}</span>
4278
+ <strong class="absolute-voice-turn-latency__label">${escapeHtml11(model.label)}</strong>
4099
4279
  </header>
4100
- <p class="absolute-voice-turn-latency__description">${escapeHtml10(model.description)}</p>
4101
- ${model.showProofAction ? `<button class="absolute-voice-turn-latency__proof" data-absolute-voice-turn-latency-proof type="button">${escapeHtml10(model.proofLabel ?? DEFAULT_PROOF_LABEL)}</button>` : ""}
4280
+ <p class="absolute-voice-turn-latency__description">${escapeHtml11(model.description)}</p>
4281
+ ${model.showProofAction ? `<button class="absolute-voice-turn-latency__proof" data-absolute-voice-turn-latency-proof type="button">${escapeHtml11(model.proofLabel ?? DEFAULT_PROOF_LABEL)}</button>` : ""}
4102
4282
  ${turns}
4103
- ${model.error ? `<p class="absolute-voice-turn-latency__error">${escapeHtml10(model.error)}</p>` : ""}
4283
+ ${model.error ? `<p class="absolute-voice-turn-latency__error">${escapeHtml11(model.error)}</p>` : ""}
4104
4284
  </section>`;
4105
4285
  };
4106
4286
  var mountVoiceTurnLatency = (element, path = "/api/turn-latency", options = {}) => {
@@ -4150,9 +4330,9 @@ var defineVoiceTurnLatencyElement = (tagName = "absolute-voice-turn-latency") =>
4150
4330
  });
4151
4331
  };
4152
4332
  // src/client/traceTimelineWidget.ts
4153
- var DEFAULT_TITLE9 = "Voice Traces";
4154
- var DEFAULT_DESCRIPTION9 = "Latest call timelines with provider latency, fallbacks, handoffs, and errors from your self-hosted trace store.";
4155
- var escapeHtml11 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
4333
+ var DEFAULT_TITLE10 = "Voice Traces";
4334
+ var DEFAULT_DESCRIPTION10 = "Latest call timelines with provider latency, fallbacks, handoffs, and errors from your self-hosted trace store.";
4335
+ var escapeHtml12 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
4156
4336
  var formatMs2 = (value) => typeof value === "number" ? `${value}ms` : "n/a";
4157
4337
  var formatProviders = (session) => session.providers.length ? session.providers.map((provider) => provider.provider).join(", ") : "No providers";
4158
4338
  var createVoiceTraceTimelineViewModel = (snapshot, options = {}) => {
@@ -4166,34 +4346,34 @@ var createVoiceTraceTimelineViewModel = (snapshot, options = {}) => {
4166
4346
  const failed = sessions.filter((session) => session.status === "failed").length;
4167
4347
  const warnings = sessions.filter((session) => session.status === "warning").length;
4168
4348
  return {
4169
- description: options.description ?? DEFAULT_DESCRIPTION9,
4349
+ description: options.description ?? DEFAULT_DESCRIPTION10,
4170
4350
  error: snapshot.error,
4171
4351
  isLoading: snapshot.isLoading,
4172
4352
  label: snapshot.error ? "Unavailable" : failed > 0 ? `${failed} failed` : warnings > 0 ? `${warnings} warning` : sessions.length ? `${sessions.length} recent` : snapshot.isLoading ? "Checking" : "No traces yet",
4173
4353
  sessions,
4174
4354
  status: snapshot.error ? "error" : failed > 0 ? "failed" : warnings > 0 ? "warning" : sessions.length ? "ready" : snapshot.isLoading ? "loading" : "empty",
4175
- title: options.title ?? DEFAULT_TITLE9,
4355
+ title: options.title ?? DEFAULT_TITLE10,
4176
4356
  updatedAt: snapshot.updatedAt
4177
4357
  };
4178
4358
  };
4179
4359
  var renderVoiceTraceTimelineWidgetHTML = (snapshot, options = {}) => {
4180
4360
  const model = createVoiceTraceTimelineViewModel(snapshot, options);
4181
- const sessions = model.sessions.length ? `<div class="absolute-voice-trace-timeline__sessions">${model.sessions.map((session) => `<article class="absolute-voice-trace-timeline__session absolute-voice-trace-timeline__session--${escapeHtml11(session.status)}">
4361
+ const sessions = model.sessions.length ? `<div class="absolute-voice-trace-timeline__sessions">${model.sessions.map((session) => `<article class="absolute-voice-trace-timeline__session absolute-voice-trace-timeline__session--${escapeHtml12(session.status)}">
4182
4362
  <header>
4183
- <strong>${escapeHtml11(session.sessionId)}</strong>
4184
- <span>${escapeHtml11(session.status)}</span>
4363
+ <strong>${escapeHtml12(session.sessionId)}</strong>
4364
+ <span>${escapeHtml12(session.status)}</span>
4185
4365
  </header>
4186
- <p>${escapeHtml11(session.label)} \xB7 ${escapeHtml11(session.durationLabel)} \xB7 ${escapeHtml11(session.providerLabel)}</p>
4187
- <a href="${escapeHtml11(session.detailHref)}">Open timeline</a>
4366
+ <p>${escapeHtml12(session.label)} \xB7 ${escapeHtml12(session.durationLabel)} \xB7 ${escapeHtml12(session.providerLabel)}</p>
4367
+ <a href="${escapeHtml12(session.detailHref)}">Open timeline</a>
4188
4368
  </article>`).join("")}</div>` : '<p class="absolute-voice-trace-timeline__empty">Run a voice session to see call timelines.</p>';
4189
- return `<section class="absolute-voice-trace-timeline absolute-voice-trace-timeline--${escapeHtml11(model.status)}">
4369
+ return `<section class="absolute-voice-trace-timeline absolute-voice-trace-timeline--${escapeHtml12(model.status)}">
4190
4370
  <header class="absolute-voice-trace-timeline__header">
4191
- <span class="absolute-voice-trace-timeline__eyebrow">${escapeHtml11(model.title)}</span>
4192
- <strong class="absolute-voice-trace-timeline__label">${escapeHtml11(model.label)}</strong>
4371
+ <span class="absolute-voice-trace-timeline__eyebrow">${escapeHtml12(model.title)}</span>
4372
+ <strong class="absolute-voice-trace-timeline__label">${escapeHtml12(model.label)}</strong>
4193
4373
  </header>
4194
- <p class="absolute-voice-trace-timeline__description">${escapeHtml11(model.description)}</p>
4374
+ <p class="absolute-voice-trace-timeline__description">${escapeHtml12(model.description)}</p>
4195
4375
  ${sessions}
4196
- ${model.error ? `<p class="absolute-voice-trace-timeline__error">${escapeHtml11(model.error)}</p>` : ""}
4376
+ ${model.error ? `<p class="absolute-voice-trace-timeline__error">${escapeHtml12(model.error)}</p>` : ""}
4197
4377
  </section>`;
4198
4378
  };
4199
4379
  var getVoiceTraceTimelineCSS = () => `.absolute-voice-trace-timeline{border:1px solid #bad7d3;border-radius:20px;background:#f3fffb;color:#09201c;padding:18px;box-shadow:0 18px 40px rgba(9,32,28,.12);font-family:inherit}.absolute-voice-trace-timeline--error,.absolute-voice-trace-timeline--failed{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-trace-timeline--warning{border-color:#fbbf24;background:#fffaf0}.absolute-voice-trace-timeline__header,.absolute-voice-trace-timeline__session header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-trace-timeline__eyebrow{color:#17665b;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-trace-timeline__label{font-size:24px;line-height:1}.absolute-voice-trace-timeline__description,.absolute-voice-trace-timeline__session p,.absolute-voice-trace-timeline__empty{color:#35544f}.absolute-voice-trace-timeline__sessions{display:grid;gap:12px;margin-top:14px}.absolute-voice-trace-timeline__session{background:#fff;border:1px solid #cfe7e2;border-radius:16px;padding:14px}.absolute-voice-trace-timeline__session--failed{border-color:#f2a7a7}.absolute-voice-trace-timeline__session--warning{border-color:#fbbf24}.absolute-voice-trace-timeline__session p{margin:10px 0}.absolute-voice-trace-timeline__session a{color:#0f766e;font-weight:800}.absolute-voice-trace-timeline__empty{margin:14px 0 0}.absolute-voice-trace-timeline__error{color:#9f1239;font-weight:700}`;
@@ -4325,6 +4505,7 @@ export {
4325
4505
  renderVoiceRoutingStatusHTML,
4326
4506
  renderVoiceProviderStatusHTML,
4327
4507
  renderVoiceProviderSimulationControlsHTML,
4508
+ renderVoiceProviderContractsHTML,
4328
4509
  renderVoiceProviderCapabilitiesHTML,
4329
4510
  renderVoiceOpsStatusHTML,
4330
4511
  renderVoiceOpsActionHistoryWidgetHTML,
@@ -4337,6 +4518,7 @@ export {
4337
4518
  mountVoiceRoutingStatus,
4338
4519
  mountVoiceProviderStatus,
4339
4520
  mountVoiceProviderSimulationControls,
4521
+ mountVoiceProviderContracts,
4340
4522
  mountVoiceProviderCapabilities,
4341
4523
  mountVoiceOpsStatus,
4342
4524
  mountVoiceOpsActionHistory,
@@ -4346,6 +4528,7 @@ export {
4346
4528
  getVoiceTraceTimelineCSS,
4347
4529
  getVoiceRoutingStatusCSS,
4348
4530
  getVoiceProviderStatusCSS,
4531
+ getVoiceProviderContractsCSS,
4349
4532
  getVoiceProviderCapabilitiesCSS,
4350
4533
  getVoiceOpsStatusLabel,
4351
4534
  getVoiceOpsStatusCSS,
@@ -4358,6 +4541,7 @@ export {
4358
4541
  fetchVoiceTraceTimeline,
4359
4542
  fetchVoiceRoutingStatus,
4360
4543
  fetchVoiceProviderStatus,
4544
+ fetchVoiceProviderContracts,
4361
4545
  fetchVoiceProviderCapabilities,
4362
4546
  fetchVoiceOpsStatus,
4363
4547
  fetchVoiceOpsActionHistory,
@@ -4369,6 +4553,7 @@ export {
4369
4553
  defineVoiceRoutingStatusElement,
4370
4554
  defineVoiceProviderStatusElement,
4371
4555
  defineVoiceProviderSimulationControlsElement,
4556
+ defineVoiceProviderContractsElement,
4372
4557
  defineVoiceProviderCapabilitiesElement,
4373
4558
  defineVoiceOpsStatusElement,
4374
4559
  defineVoiceOpsActionCenterElement,
@@ -4388,6 +4573,8 @@ export {
4388
4573
  createVoiceProviderStatusStore,
4389
4574
  createVoiceProviderSimulationControlsViewModel,
4390
4575
  createVoiceProviderSimulationControlsStore,
4576
+ createVoiceProviderContractsViewModel,
4577
+ createVoiceProviderContractsStore,
4391
4578
  createVoiceProviderCapabilitiesViewModel,
4392
4579
  createVoiceProviderCapabilitiesStore,
4393
4580
  createVoiceOpsStatusViewModel,
@@ -0,0 +1,19 @@
1
+ import type { VoiceProviderContractMatrixReport } from '../providerStackRecommendations';
2
+ export type VoiceProviderContractsClientOptions = {
3
+ fetch?: typeof fetch;
4
+ intervalMs?: number;
5
+ };
6
+ export type VoiceProviderContractsSnapshot<TProvider extends string = string> = {
7
+ error: string | null;
8
+ isLoading: boolean;
9
+ report?: VoiceProviderContractMatrixReport<TProvider>;
10
+ updatedAt?: number;
11
+ };
12
+ export declare const fetchVoiceProviderContracts: <TProvider extends string = string>(path?: string, options?: Pick<VoiceProviderContractsClientOptions, "fetch">) => Promise<VoiceProviderContractMatrixReport<TProvider>>;
13
+ export declare const createVoiceProviderContractsStore: <TProvider extends string = string>(path?: string, options?: VoiceProviderContractsClientOptions) => {
14
+ close: () => void;
15
+ getServerSnapshot: () => VoiceProviderContractsSnapshot<TProvider>;
16
+ getSnapshot: () => VoiceProviderContractsSnapshot<TProvider>;
17
+ refresh: () => Promise<VoiceProviderContractMatrixReport<TProvider> | undefined>;
18
+ subscribe: (listener: () => void) => () => void;
19
+ };
@@ -0,0 +1,37 @@
1
+ import type { VoiceProviderContractMatrixRow } from '../providerStackRecommendations';
2
+ import { type VoiceProviderContractsClientOptions, type VoiceProviderContractsSnapshot } from './providerContracts';
3
+ export type VoiceProviderContractRowView<TProvider extends string = string> = VoiceProviderContractMatrixRow<TProvider> & {
4
+ detail: string;
5
+ label: string;
6
+ remediations: Array<{
7
+ detail: string;
8
+ href?: string;
9
+ label: string;
10
+ }>;
11
+ rows: Array<{
12
+ label: string;
13
+ value: string;
14
+ }>;
15
+ };
16
+ export type VoiceProviderContractsViewModel<TProvider extends string = string> = {
17
+ description: string;
18
+ error: string | null;
19
+ isLoading: boolean;
20
+ label: string;
21
+ rows: VoiceProviderContractRowView<TProvider>[];
22
+ status: 'empty' | 'error' | 'loading' | 'ready' | 'warning';
23
+ title: string;
24
+ updatedAt?: number;
25
+ };
26
+ export type VoiceProviderContractsWidgetOptions = VoiceProviderContractsClientOptions & {
27
+ description?: string;
28
+ title?: string;
29
+ };
30
+ export declare const createVoiceProviderContractsViewModel: <TProvider extends string = string>(snapshot: VoiceProviderContractsSnapshot<TProvider>, options?: VoiceProviderContractsWidgetOptions) => VoiceProviderContractsViewModel<TProvider>;
31
+ export declare const renderVoiceProviderContractsHTML: <TProvider extends string = string>(snapshot: VoiceProviderContractsSnapshot<TProvider>, options?: VoiceProviderContractsWidgetOptions) => string;
32
+ export declare const getVoiceProviderContractsCSS: () => string;
33
+ export declare const mountVoiceProviderContracts: <TProvider extends string = string>(element: Element, path?: string, options?: VoiceProviderContractsWidgetOptions) => {
34
+ close: () => void;
35
+ refresh: () => Promise<import("..").VoiceProviderContractMatrixReport<TProvider> | undefined>;
36
+ };
37
+ export declare const defineVoiceProviderContractsElement: (tagName?: string) => void;
package/dist/index.js CHANGED
@@ -21437,36 +21437,72 @@ var buildVoiceProviderContractMatrix = (input) => {
21437
21437
  detail: configured ? "Provider is configured for this deployment." : "Provider is declared but not configured.",
21438
21438
  key: "configured",
21439
21439
  label: "Configured",
21440
+ remediation: configured ? undefined : {
21441
+ code: "provider.configure",
21442
+ detail: "Enable this provider or remove it from the contract matrix for this deployment.",
21443
+ href: contract.remediationHref,
21444
+ label: "Configure provider"
21445
+ },
21440
21446
  status: configured ? "pass" : "fail"
21441
21447
  },
21442
21448
  {
21443
21449
  detail: missingEnv.length === 0 ? "Required environment is present." : `Missing env: ${missingEnv.join(", ")}.`,
21444
21450
  key: "env",
21445
21451
  label: "Required env",
21452
+ remediation: missingEnv.length === 0 ? undefined : {
21453
+ code: "provider.env",
21454
+ detail: `Set ${missingEnv.join(", ")} before deploying this provider.`,
21455
+ href: contract.remediationHref,
21456
+ label: "Add missing env"
21457
+ },
21446
21458
  status: missingEnv.length === 0 ? "pass" : "fail"
21447
21459
  },
21448
21460
  {
21449
21461
  detail: contract.latencyBudgetMs !== undefined ? `Latency budget is ${contract.latencyBudgetMs}ms.` : "No latency budget declared.",
21450
21462
  key: "latencyBudget",
21451
21463
  label: "Latency budget",
21464
+ remediation: contract.latencyBudgetMs !== undefined ? undefined : {
21465
+ code: "provider.latency_budget",
21466
+ detail: "Declare latencyBudgetMs so readiness can distinguish expected latency from regressions.",
21467
+ href: contract.remediationHref,
21468
+ label: "Declare latency budget"
21469
+ },
21452
21470
  status: contract.latencyBudgetMs !== undefined ? "pass" : "warn"
21453
21471
  },
21454
21472
  {
21455
21473
  detail: (contract.fallbackProviders ?? []).length > 0 ? `Fallback providers: ${contract.fallbackProviders?.join(", ")}.` : "No fallback provider declared.",
21456
21474
  key: "fallback",
21457
21475
  label: "Fallback",
21476
+ remediation: (contract.fallbackProviders ?? []).length > 0 ? undefined : {
21477
+ code: "provider.fallback",
21478
+ detail: "Declare at least one fallback provider for this lane or mark this provider as intentionally single-provider.",
21479
+ href: contract.remediationHref,
21480
+ label: "Add fallback provider"
21481
+ },
21458
21482
  status: (contract.fallbackProviders ?? []).length > 0 ? "pass" : "warn"
21459
21483
  },
21460
21484
  {
21461
21485
  detail: contract.streaming ? "Streaming is supported." : "Streaming support is not declared.",
21462
21486
  key: "streaming",
21463
21487
  label: "Streaming",
21488
+ remediation: contract.streaming ? undefined : {
21489
+ code: "provider.streaming",
21490
+ detail: "Use a streaming-capable adapter for realtime voice, or route this provider only to non-realtime workflows.",
21491
+ href: contract.remediationHref,
21492
+ label: "Add streaming support"
21493
+ },
21464
21494
  status: contract.streaming ? "pass" : "warn"
21465
21495
  },
21466
21496
  {
21467
21497
  detail: missingCapabilities.length === 0 ? "Required capabilities are declared." : `Missing capabilities: ${missingCapabilities.join(", ")}.`,
21468
21498
  key: "capabilities",
21469
21499
  label: "Capabilities",
21500
+ remediation: missingCapabilities.length === 0 ? undefined : {
21501
+ code: "provider.capabilities",
21502
+ detail: `Declare or implement capabilities: ${missingCapabilities.join(", ")}.`,
21503
+ href: contract.remediationHref,
21504
+ label: "Add capability coverage"
21505
+ },
21470
21506
  status: missingCapabilities.length === 0 ? "pass" : "warn"
21471
21507
  }
21472
21508
  ];
@@ -21495,7 +21531,7 @@ var resolveProviderContractMatrixInput = async (matrix) => typeof matrix === "fu
21495
21531
  var renderVoiceProviderContractMatrixHTML = (report, options = {}) => {
21496
21532
  const title = options.title ?? "Voice Provider Contract Matrix";
21497
21533
  const rows = report.rows.map((row) => {
21498
- const checks = row.checks.map((check) => `<li class="${escapeHtml36(check.status)}"><strong>${escapeHtml36(check.label)}</strong><span>${escapeHtml36(check.detail ?? check.status)}</span></li>`).join("");
21534
+ const checks = row.checks.map((check) => `<li class="${escapeHtml36(check.status)}"><strong>${escapeHtml36(check.label)}</strong><span>${escapeHtml36(check.detail ?? check.status)}</span>${check.remediation ? `<em>${check.remediation.href ? `<a href="${escapeHtml36(check.remediation.href)}">${escapeHtml36(check.remediation.label)}</a>` : escapeHtml36(check.remediation.label)}: ${escapeHtml36(check.remediation.detail)}</em>` : ""}</li>`).join("");
21499
21535
  return `<article class="row ${escapeHtml36(row.status)}">
21500
21536
  <div>
21501
21537
  <p class="eyebrow">${escapeHtml36(row.kind)}${row.selected ? " \xB7 selected" : ""}</p>
@@ -21505,7 +21541,7 @@ var renderVoiceProviderContractMatrixHTML = (report, options = {}) => {
21505
21541
  <ul>${checks}</ul>
21506
21542
  </article>`;
21507
21543
  }).join("");
21508
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml36(title)}</title><style>body{background:#0f1412;color:#f7f3e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.row{background:#17201b;border:1px solid #2d3b32;border-radius:24px;margin-bottom:16px;padding:22px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.16),rgba(125,211,252,.12))}.eyebrow{color:#86efac;font-size:.78rem;font-weight:900;letter-spacing:.1em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill,.status{border:1px solid #3f4f45;border-radius:999px;display:inline-flex;padding:8px 12px}.status.pass,.row.pass,.pass{border-color:rgba(34,197,94,.65)}.status.warn,.row.warn,.warn{border-color:rgba(245,158,11,.7)}.status.fail,.row.fail,.fail{border-color:rgba(239,68,68,.75)}.row{display:grid;gap:20px;grid-template-columns:minmax(180px,.45fr) 1fr}.row ul{display:grid;gap:10px;list-style:none;margin:0;padding:0}.row li{background:#111814;border:1px solid #2d3b32;border-radius:16px;display:grid;gap:4px;padding:12px}.row li span{color:#b8c2ba}@media(max-width:760px){main{padding:18px}.row{grid-template-columns:1fr}}</style></head><body><main><section class="hero"><p class="eyebrow">Provider contracts</p><h1>${escapeHtml36(title)}</h1><p>Self-hosted provider proof for configured state, required env, latency budgets, fallback, streaming, and declared capabilities.</p><div class="summary"><span class="pill">${String(report.passed)} passing</span><span class="pill">${String(report.warned)} warning</span><span class="pill">${String(report.failed)} failing</span><span class="pill">${String(report.total)} total</span></div></section>${rows || '<article class="row"><p>No provider contracts configured.</p></article>'}</main></body></html>`;
21544
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml36(title)}</title><style>body{background:#0f1412;color:#f7f3e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.row{background:#17201b;border:1px solid #2d3b32;border-radius:24px;margin-bottom:16px;padding:22px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.16),rgba(125,211,252,.12))}.eyebrow{color:#86efac;font-size:.78rem;font-weight:900;letter-spacing:.1em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill,.status{border:1px solid #3f4f45;border-radius:999px;display:inline-flex;padding:8px 12px}.status.pass,.row.pass,.pass{border-color:rgba(34,197,94,.65)}.status.warn,.row.warn,.warn{border-color:rgba(245,158,11,.7)}.status.fail,.row.fail,.fail{border-color:rgba(239,68,68,.75)}.row{display:grid;gap:20px;grid-template-columns:minmax(180px,.45fr) 1fr}.row ul{display:grid;gap:10px;list-style:none;margin:0;padding:0}.row li{background:#111814;border:1px solid #2d3b32;border-radius:16px;display:grid;gap:4px;padding:12px}.row li span{color:#b8c2ba}.row li em{color:#f9d77e;font-style:normal}.row li a{color:#86efac}@media(max-width:760px){main{padding:18px}.row{grid-template-columns:1fr}}</style></head><body><main><section class="hero"><p class="eyebrow">Provider contracts</p><h1>${escapeHtml36(title)}</h1><p>Self-hosted provider proof for configured state, required env, latency budgets, fallback, streaming, and declared capabilities.</p><div class="summary"><span class="pill">${String(report.passed)} passing</span><span class="pill">${String(report.warned)} warning</span><span class="pill">${String(report.failed)} failing</span><span class="pill">${String(report.total)} total</span></div></section>${rows || '<article class="row"><p>No provider contracts configured.</p></article>'}</main></body></html>`;
21509
21545
  };
21510
21546
  var createVoiceProviderContractMatrixJSONHandler = (matrix) => async () => buildVoiceProviderContractMatrix(await resolveProviderContractMatrixInput(matrix));
21511
21547
  var createVoiceProviderContractMatrixHTMLHandler = (options) => async () => {
@@ -37,10 +37,17 @@ export type VoiceProviderStackCapabilityGapInput<TProvider extends string = stri
37
37
  required?: Partial<Record<VoiceProviderStackKind, readonly string[]>>;
38
38
  };
39
39
  export type VoiceProviderContractCheckStatus = 'fail' | 'pass' | 'warn';
40
+ export type VoiceProviderContractRemediation = {
41
+ code: string;
42
+ detail: string;
43
+ href?: string;
44
+ label: string;
45
+ };
40
46
  export type VoiceProviderContractCheck = {
41
47
  detail?: string;
42
48
  key: string;
43
49
  label: string;
50
+ remediation?: VoiceProviderContractRemediation;
44
51
  status: VoiceProviderContractCheckStatus;
45
52
  };
46
53
  export type VoiceProviderContractDefinition<TProvider extends string = string> = {
@@ -53,6 +60,7 @@ export type VoiceProviderContractDefinition<TProvider extends string = string> =
53
60
  provider: TProvider;
54
61
  requiredCapabilities?: readonly string[];
55
62
  requiredEnv?: readonly string[];
63
+ remediationHref?: string;
56
64
  selected?: boolean;
57
65
  streaming?: boolean;
58
66
  };
@@ -0,0 +1,6 @@
1
+ import { type VoiceProviderContractsWidgetOptions } from '../client/providerContractsWidget';
2
+ export type VoiceProviderContractsProps = VoiceProviderContractsWidgetOptions & {
3
+ className?: string;
4
+ path?: string;
5
+ };
6
+ export declare const VoiceProviderContracts: ({ className, path, ...options }: VoiceProviderContractsProps) => import("react/jsx-runtime").JSX.Element;