@absolutejs/voice 0.0.22-beta.360 → 0.0.22-beta.362

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -31,8 +31,8 @@ export { assertVoiceCompetitiveCoverage, buildVoiceCompetitiveCoverageReport, cr
31
31
  export type { VoiceCompetitiveCoverageAssertionInput, VoiceCompetitiveCoverageAssertionReport, VoiceCompetitiveCoverageIssue, VoiceCompetitiveCoverageLevel, VoiceCompetitiveCoverageReport, VoiceCompetitiveCoverageReportInput, VoiceCompetitiveCoverageRoutesOptions, VoiceCompetitiveCoverageStatus, VoiceCompetitiveCoverageSummary, VoiceCompetitiveDepthLevel, VoiceCompetitiveEvidence, VoiceCompetitiveSurface } from './competitiveCoverage';
32
32
  export type { VoicePlatformCoverageAssertionInput, VoicePlatformCoverageAssertionReport, VoicePlatformCoverageEvidence, VoicePlatformCoverageRoutesOptions, VoicePlatformCoverageStatus, VoicePlatformCoverageSummary, VoicePlatformCoverageSummaryInput, VoicePlatformCoverageSurface } from './platformCoverage';
33
33
  export { assertVoiceProofTrendEvidence, buildEmptyVoiceProofTrendReport, buildVoiceProofTrendProfileSummaries, buildVoiceProofTrendRecommendationReport, buildVoiceProofTrendReportFromRealCallProfiles, buildVoiceProofTrendReport, buildVoiceRealCallProfileDefaults, buildVoiceRealCallProfileHistoryReport, createVoiceProofTrendRecommendationRoutes, createVoiceProofTrendRoutes, createVoiceRealCallProfileHistoryRoutes, DEFAULT_VOICE_PROOF_TREND_PROFILE_DEFINITIONS, DEFAULT_VOICE_PROOF_TRENDS_MAX_AGE_MS, evaluateVoiceProofTrendEvidence, formatVoiceProofTrendAge, normalizeVoiceProofTrendReport, readVoiceProofTrendReportFile, renderVoiceProofTrendRecommendationHTML, renderVoiceProofTrendRecommendationMarkdown, renderVoiceRealCallProfileHistoryHTML, renderVoiceRealCallProfileHistoryMarkdown, resolveVoiceRealCallProfileProviderRoute } from './proofTrends';
34
- export { applyVoiceProfileSwitchGuard, createVoiceProfileSwitchPolicyProofRoutes, recommendVoiceProfileSwitch, renderVoiceProfileSwitchPolicyProofHTML, runVoiceProfileSwitchPolicyProof } from './profileSwitchRecommendation';
35
- export type { VoiceProfileSwitchGuardAction, VoiceProfileSwitchGuardDecision, VoiceProfileSwitchGuardMode, VoiceProfileSwitchGuardOptions, VoiceProfileSwitchObservedSignals, VoiceProfileSwitchPolicyProofCase, VoiceProfileSwitchPolicyProofCaseResult, VoiceProfileSwitchPolicyProofOptions, VoiceProfileSwitchPolicyProofReport, VoiceProfileSwitchPolicyProofRoutesOptions, VoiceProfileSwitchRecommendation, VoiceProfileSwitchRecommendationOptions } from './profileSwitchRecommendation';
34
+ export { applyVoiceProfileSwitchGuard, buildVoiceProfileSwitchReadinessReport, buildVoiceProfileSwitchLiveDecisionReport, createVoiceProfileSwitchLiveDecisionRoutes, createVoiceProfileSwitchPolicyProofRoutes, createVoiceProfileSwitchReadinessRoutes, recommendVoiceProfileSwitch, renderVoiceProfileSwitchLiveDecisionHTML, renderVoiceProfileSwitchPolicyProofHTML, renderVoiceProfileSwitchReadinessHTML, runVoiceProfileSwitchPolicyProof } from './profileSwitchRecommendation';
35
+ export type { VoiceProfileSwitchGuardAction, VoiceProfileSwitchGuardDecision, VoiceProfileSwitchGuardMode, VoiceProfileSwitchGuardOptions, VoiceProfileSwitchObservedSignals, VoiceProfileSwitchLiveDecisionEvidence, VoiceProfileSwitchLiveDecisionReport, VoiceProfileSwitchLiveDecisionReportOptions, VoiceProfileSwitchLiveDecisionRoutesOptions, VoiceProfileSwitchLiveDecisionSession, VoiceProfileSwitchPolicyProofCase, VoiceProfileSwitchPolicyProofCaseResult, VoiceProfileSwitchPolicyProofOptions, VoiceProfileSwitchPolicyProofReport, VoiceProfileSwitchPolicyProofRoutesOptions, VoiceProfileSwitchReadinessIssue, VoiceProfileSwitchReadinessOptions, VoiceProfileSwitchReadinessReport, VoiceProfileSwitchReadinessRoutesOptions, VoiceProfileSwitchReadinessStatus, VoiceProfileSwitchRecommendation, VoiceProfileSwitchRecommendationOptions } from './profileSwitchRecommendation';
36
36
  export { buildVoiceProviderDecisionTraceReport, createVoiceProviderDecisionTraceEvent, createVoiceProviderDecisionTraceRoutes, listVoiceProviderDecisionTraces, renderVoiceProviderDecisionTraceHTML, renderVoiceProviderDecisionTraceMarkdown } from './providerDecisionTraces';
37
37
  export type { VoiceProviderDecisionStatus, VoiceProviderDecisionSurfaceReport, VoiceProviderDecisionTrace, VoiceProviderDecisionTraceInput, VoiceProviderDecisionTraceIssue, VoiceProviderDecisionTraceReport, VoiceProviderDecisionTraceReportOptions, VoiceProviderDecisionTraceRoutesOptions } from './providerDecisionTraces';
38
38
  export type { VoiceProofTrendAssertionInput, VoiceProofTrendAssertionReport, VoiceProofTrendCycle, VoiceProofTrendProfileDefinition, VoiceProofTrendProfileRecommendation, VoiceProofTrendProfileSummaryOptions, VoiceProofTrendProfileSummary, VoiceProofTrendProviderRecommendation, VoiceProofTrendProviderSummary, VoiceProofTrendRecommendation, VoiceProofTrendRecommendationOptions, VoiceProofTrendRecommendationReport, VoiceProofTrendRecommendationRoutesOptions, VoiceProofTrendRecommendationStatus, VoiceProofTrendRecommendationSurface, VoiceProofTrendRealCallProfileEvidence, VoiceProofTrendRealCallProfileReportOptions, VoiceProofTrendReport, VoiceProofTrendReportInput, VoiceProofTrendRoutesOptions, VoiceProofTrendRuntimeChannelSummary, VoiceProofTrendStatus, VoiceProofTrendSummary, VoiceRealCallProfileDefault, VoiceRealCallProfileDefaultsOptions, VoiceRealCallProfileDefaultsReport, VoiceRealCallProfileHistoryOptions, VoiceRealCallProfileHistoryReport, VoiceRealCallProfileHistoryRoutesOptions, VoiceRealCallProfileProviderRouteOptions } from './proofTrends';
package/dist/index.js CHANGED
@@ -5681,6 +5681,289 @@ var createVoiceProfileSwitchPolicyProofRoutes = (options) => {
5681
5681
  }
5682
5682
  return routes;
5683
5683
  };
5684
+ var readStringField = (record, key) => typeof record?.[key] === "string" ? record[key] : undefined;
5685
+ var readNumberField = (record, key) => typeof record?.[key] === "number" && Number.isFinite(record[key]) ? record[key] : undefined;
5686
+ var readBooleanField = (record, key) => typeof record?.[key] === "boolean" ? record[key] : undefined;
5687
+ var auditEventToLiveDecision = (event) => {
5688
+ if (event.type !== "profile.switch" || !event.sessionId) {
5689
+ return;
5690
+ }
5691
+ const payload = event.payload;
5692
+ return {
5693
+ action: readStringField(payload, "action") ?? event.action.replace(/^profile\.switch\./, ""),
5694
+ at: event.at,
5695
+ auditEventId: event.id,
5696
+ autoApplied: readBooleanField(payload, "autoApplied"),
5697
+ autoSwitchCount: readNumberField(payload, "autoSwitchCount"),
5698
+ blockedByPolicy: readStringField(payload, "blockedByPolicy"),
5699
+ confidence: readNumberField(payload, "confidence"),
5700
+ maxAutoSwitchesPerSession: readNumberField(payload, "maxAutoSwitchesPerSession"),
5701
+ minConfidence: readNumberField(payload, "minConfidence"),
5702
+ mode: readStringField(payload, "mode"),
5703
+ outcome: event.outcome,
5704
+ previousProfileId: readStringField(payload, "previousProfileId"),
5705
+ recommendedProfileId: readStringField(payload, "recommendedProfileId"),
5706
+ selectedProfileId: readStringField(payload, "selectedProfileId") ?? event.resource?.id,
5707
+ sessionId: event.sessionId,
5708
+ source: "audit",
5709
+ status: readStringField(payload, "status"),
5710
+ traceId: event.traceId
5711
+ };
5712
+ };
5713
+ var traceEventToLiveDecision = (event) => {
5714
+ if (event.type !== "provider.decision" || event.metadata?.source !== "profile-switch-guard") {
5715
+ return;
5716
+ }
5717
+ const payload = event.payload;
5718
+ return {
5719
+ action: readStringField(payload, "action"),
5720
+ at: event.at,
5721
+ autoApplied: readBooleanField(payload, "autoApplied"),
5722
+ autoSwitchCount: readNumberField(payload, "autoSwitchCount"),
5723
+ blockedByPolicy: readStringField(payload, "blockedByPolicy"),
5724
+ confidence: readNumberField(payload, "confidence"),
5725
+ maxAutoSwitchesPerSession: readNumberField(payload, "maxAutoSwitchesPerSession"),
5726
+ minConfidence: readNumberField(payload, "minConfidence"),
5727
+ mode: readStringField(payload, "mode"),
5728
+ previousProfileId: readStringField(payload, "previousProfileId"),
5729
+ reason: readStringField(payload, "reason"),
5730
+ recommendedProfileId: readStringField(payload, "recommendedProfileId"),
5731
+ scenarioId: event.scenarioId,
5732
+ selectedProfileId: readStringField(payload, "selectedProfileId"),
5733
+ sessionId: event.sessionId,
5734
+ source: "trace",
5735
+ status: readStringField(payload, "status"),
5736
+ traceEventId: event.id,
5737
+ traceId: event.traceId
5738
+ };
5739
+ };
5740
+ var buildVoiceProfileSwitchLiveDecisionReport = async (options) => {
5741
+ const [auditEvents, traceEvents] = await Promise.all([
5742
+ options.audit?.list({
5743
+ limit: options.limit,
5744
+ sessionId: options.sessionId,
5745
+ type: "profile.switch"
5746
+ }) ?? [],
5747
+ options.trace?.list({
5748
+ limit: options.limit,
5749
+ sessionId: options.sessionId,
5750
+ type: "provider.decision"
5751
+ }) ?? []
5752
+ ]);
5753
+ const decisions = [
5754
+ ...auditEvents.map(auditEventToLiveDecision).filter((decision) => Boolean(decision)),
5755
+ ...traceEvents.map(traceEventToLiveDecision).filter((decision) => Boolean(decision))
5756
+ ].sort((left, right) => right.at - left.at).slice(0, options.limit);
5757
+ const sessions = Array.from(decisions.reduce((map, decision) => {
5758
+ const existing = map.get(decision.sessionId) ?? {
5759
+ actionCounts: {},
5760
+ actions: [],
5761
+ autoApplied: 0,
5762
+ blockedByPolicy: [],
5763
+ decisions: [],
5764
+ firstSeenAt: decision.at,
5765
+ lastSeenAt: decision.at,
5766
+ sessionId: decision.sessionId
5767
+ };
5768
+ existing.decisions.push(decision);
5769
+ existing.firstSeenAt = Math.min(existing.firstSeenAt, decision.at);
5770
+ existing.lastSeenAt = Math.max(existing.lastSeenAt, decision.at);
5771
+ if (decision.action) {
5772
+ existing.actionCounts[decision.action] = (existing.actionCounts[decision.action] ?? 0) + 1;
5773
+ }
5774
+ if (decision.autoApplied) {
5775
+ existing.autoApplied += 1;
5776
+ }
5777
+ if (decision.blockedByPolicy && !existing.blockedByPolicy.includes(decision.blockedByPolicy)) {
5778
+ existing.blockedByPolicy.push(decision.blockedByPolicy);
5779
+ }
5780
+ map.set(decision.sessionId, existing);
5781
+ return map;
5782
+ }, new Map)).map(([, session]) => ({
5783
+ ...session,
5784
+ actions: Object.keys(session.actionCounts).sort(),
5785
+ decisions: session.decisions.sort((left, right) => right.at - left.at),
5786
+ lastDecision: session.decisions.sort((left, right) => right.at - left.at)[0]
5787
+ }));
5788
+ const actionCount = (action) => decisions.filter((decision) => decision.action === action).length;
5789
+ return {
5790
+ generatedAt: new Date().toISOString(),
5791
+ ok: decisions.length > 0,
5792
+ sessions: sessions.sort((left, right) => right.lastSeenAt - left.lastSeenAt),
5793
+ summary: {
5794
+ auditEvents: auditEvents.length,
5795
+ autoApplied: decisions.filter((decision) => decision.autoApplied).length,
5796
+ blocked: actionCount("blocked"),
5797
+ decisions: decisions.length,
5798
+ recommendations: actionCount("recommend"),
5799
+ sessions: sessions.length,
5800
+ switches: actionCount("switch"),
5801
+ traceEvents: traceEvents.length
5802
+ }
5803
+ };
5804
+ };
5805
+ var renderVoiceProfileSwitchLiveDecisionHTML = (report, options = {}) => {
5806
+ const title = options.title ?? "Voice Profile Switch Live Decisions";
5807
+ const rows = report.sessions.flatMap((session) => session.decisions.map((decision) => `<tr>
5808
+ <td><strong>${escapeHtml3(decision.sessionId)}</strong><p>${escapeHtml3(new Date(decision.at).toISOString())}</p></td>
5809
+ <td>${escapeHtml3(decision.source)}</td>
5810
+ <td>${escapeHtml3(decision.action ?? "unknown")}</td>
5811
+ <td>${escapeHtml3(decision.selectedProfileId ?? "none")}</td>
5812
+ <td>${escapeHtml3(decision.blockedByPolicy ?? "none")}</td>
5813
+ <td>${typeof decision.confidence === "number" ? `${Math.round(decision.confidence * 100)}%` : "n/a"}</td>
5814
+ <td>${escapeHtml3(decision.reason ?? decision.outcome ?? "recorded")}</td>
5815
+ </tr>`)).join("");
5816
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml3(title)}</title><style>:root{color-scheme:dark;background:#11100b;color:#fff7ed;font-family:ui-sans-serif,system-ui,sans-serif}body{margin:0;padding:32px;background:radial-gradient(circle at top right,rgba(251,146,60,.18),transparent 34%),#11100b}main{max-width:1180px;margin:0 auto}.hero,.card{border:1px solid rgba(251,191,36,.24);border-radius:24px;background:rgba(28,25,23,.78);box-shadow:0 24px 90px rgba(0,0,0,.24);padding:24px;margin-bottom:18px}.metric-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px}.metric{border:1px solid rgba(251,191,36,.22);border-radius:18px;padding:16px;background:rgba(120,53,15,.26)}.metric span{display:block;color:#fed7aa;font-size:.75rem;text-transform:uppercase;letter-spacing:.08em}.metric strong{display:block;margin-top:8px;font-size:1.7rem}table{width:100%;border-collapse:collapse}th,td{padding:13px;text-align:left;border-bottom:1px solid rgba(251,191,36,.18);vertical-align:top}th{color:#fde68a;text-transform:uppercase;font-size:.74rem;letter-spacing:.08em}td p{margin:.35rem 0 0;color:#fdba74}pre{white-space:pre-wrap;overflow:auto;border-radius:18px;background:#020617;padding:18px;color:#ffedd5}.empty{color:#fdba74}</style></head><body><main><section class="hero"><h1>${escapeHtml3(title)}</h1><p>This page summarizes real profile switch guard evidence from audit and trace stores. Use it beside policy proof to show bounded policy plus actual session decisions.</p><div class="metric-grid"><div class="metric"><span>Sessions</span><strong>${String(report.summary.sessions)}</strong></div><div class="metric"><span>Decisions</span><strong>${String(report.summary.decisions)}</strong></div><div class="metric"><span>Switches</span><strong>${String(report.summary.switches)}</strong></div><div class="metric"><span>Blocked</span><strong>${String(report.summary.blocked)}</strong></div><div class="metric"><span>Auto applied</span><strong>${String(report.summary.autoApplied)}</strong></div></div></section><section class="card"><h2>Live Guard Decisions</h2>${rows ? `<table><thead><tr><th>Session</th><th>Source</th><th>Action</th><th>Selected</th><th>Blocked by</th><th>Confidence</th><th>Reason</th></tr></thead><tbody>${rows}</tbody></table>` : '<p class="empty">No profile switch guard decisions recorded yet. Start a voice session with profileSwitchGuard enabled.</p>'}</section><section class="card"><h2>Raw Report</h2><pre>${stringifyForHtml(report)}</pre></section></main></body></html>`;
5817
+ };
5818
+ var createVoiceProfileSwitchLiveDecisionRoutes = (options) => {
5819
+ const path = options.path ?? "/api/voice/profile-switch-live-decisions";
5820
+ const htmlPath = options.htmlPath === undefined ? "/voice/profile-switch-live-decisions" : options.htmlPath;
5821
+ const routes = new Elysia({
5822
+ name: options.name ?? "absolutejs-voice-profile-switch-live-decisions"
5823
+ }).get(path, async ({ query }) => buildVoiceProfileSwitchLiveDecisionReport({
5824
+ audit: options.audit,
5825
+ limit: typeof query.limit === "string" && Number.isFinite(Number(query.limit)) ? Number(query.limit) : options.limit,
5826
+ sessionId: typeof query.sessionId === "string" && query.sessionId.trim() ? query.sessionId.trim() : options.sessionId,
5827
+ trace: options.trace
5828
+ }));
5829
+ if (htmlPath) {
5830
+ routes.get(htmlPath, async ({ query }) => {
5831
+ const report = await buildVoiceProfileSwitchLiveDecisionReport({
5832
+ audit: options.audit,
5833
+ limit: typeof query.limit === "string" && Number.isFinite(Number(query.limit)) ? Number(query.limit) : options.limit,
5834
+ sessionId: typeof query.sessionId === "string" && query.sessionId.trim() ? query.sessionId.trim() : options.sessionId,
5835
+ trace: options.trace
5836
+ });
5837
+ const render = options.render ?? ((input) => renderVoiceProfileSwitchLiveDecisionHTML(input, {
5838
+ title: options.title
5839
+ }));
5840
+ return new Response(await render(report), {
5841
+ headers: { "Content-Type": "text/html; charset=utf-8" }
5842
+ });
5843
+ });
5844
+ }
5845
+ return routes;
5846
+ };
5847
+ var statusFromIssues = (issues) => issues.some((issue) => issue.status === "fail") ? "fail" : issues.length > 0 ? "warn" : "pass";
5848
+ var buildVoiceProfileSwitchReadinessReport = async (options) => {
5849
+ const live = await buildVoiceProfileSwitchLiveDecisionReport(options);
5850
+ const policy = options.policyProof === false || options.policyProof === undefined ? undefined : await runVoiceProfileSwitchPolicyProof(options.policyProof);
5851
+ const issues = [];
5852
+ const requireLiveDecisions = options.requireLiveDecisions ?? true;
5853
+ const requireAudit = options.requireAudit ?? Boolean(options.autoMode);
5854
+ const requireTrace = options.requireTrace ?? true;
5855
+ const requirePolicyProof = options.requirePolicyProof ?? Boolean(options.policyProof);
5856
+ const requireAllowedProfilePolicy = options.requireAllowedProfilePolicy ?? Boolean(options.autoMode);
5857
+ const maxBlockedRatio = options.maxBlockedRatio ?? 0.5;
5858
+ const maxAutoAppliedRatio = options.maxAutoAppliedRatio ?? 0.75;
5859
+ const decisions = live.summary.decisions;
5860
+ const blockedRatio = decisions > 0 ? live.summary.blocked / decisions : 0;
5861
+ const autoAppliedRatio = decisions > 0 ? live.summary.autoApplied / decisions : 0;
5862
+ if (requireLiveDecisions && decisions === 0) {
5863
+ issues.push({
5864
+ code: "voice.profile_switch.live_decisions_missing",
5865
+ message: "No live profile switch guard decisions have been recorded.",
5866
+ status: "fail"
5867
+ });
5868
+ }
5869
+ if (requireAudit && live.summary.auditEvents === 0) {
5870
+ issues.push({
5871
+ code: "voice.profile_switch.audit_missing",
5872
+ message: "Profile switch guard readiness requires audit evidence.",
5873
+ status: "fail"
5874
+ });
5875
+ }
5876
+ if (requireTrace && live.summary.traceEvents === 0) {
5877
+ issues.push({
5878
+ code: "voice.profile_switch.trace_missing",
5879
+ message: "Profile switch guard readiness requires trace evidence.",
5880
+ status: "warn"
5881
+ });
5882
+ }
5883
+ if (requirePolicyProof && !policy) {
5884
+ issues.push({
5885
+ code: "voice.profile_switch.policy_proof_missing",
5886
+ message: "Profile switch policy proof was not configured.",
5887
+ status: "fail"
5888
+ });
5889
+ }
5890
+ if (policy && !policy.ok) {
5891
+ issues.push({
5892
+ code: "voice.profile_switch.policy_proof_failed",
5893
+ message: "Profile switch policy proof has failing cases.",
5894
+ status: "fail"
5895
+ });
5896
+ }
5897
+ if (requireAllowedProfilePolicy && (options.policyProof === false || !options.policyProof?.allowedProfileIds || options.policyProof.allowedProfileIds.length === 0)) {
5898
+ issues.push({
5899
+ code: "voice.profile_switch.allowed_policy_missing",
5900
+ message: "Auto profile switching should declare an allowedProfileIds policy.",
5901
+ status: "warn"
5902
+ });
5903
+ }
5904
+ if (decisions > 0 && blockedRatio > maxBlockedRatio) {
5905
+ issues.push({
5906
+ code: "voice.profile_switch.blocked_ratio_high",
5907
+ message: `Blocked profile switch decisions are ${Math.round(blockedRatio * 100)}%, above the ${Math.round(maxBlockedRatio * 100)}% threshold.`,
5908
+ status: "warn"
5909
+ });
5910
+ }
5911
+ if (decisions > 0 && autoAppliedRatio > maxAutoAppliedRatio) {
5912
+ issues.push({
5913
+ code: "voice.profile_switch.auto_applied_ratio_high",
5914
+ message: `Auto-applied profile switch decisions are ${Math.round(autoAppliedRatio * 100)}%, above the ${Math.round(maxAutoAppliedRatio * 100)}% threshold.`,
5915
+ status: "warn"
5916
+ });
5917
+ }
5918
+ return {
5919
+ generatedAt: new Date().toISOString(),
5920
+ issues,
5921
+ live,
5922
+ policy,
5923
+ status: statusFromIssues(issues),
5924
+ summary: {
5925
+ auditEvents: live.summary.auditEvents,
5926
+ autoApplied: live.summary.autoApplied,
5927
+ blocked: live.summary.blocked,
5928
+ decisions,
5929
+ policyCases: policy?.summary.total,
5930
+ sessions: live.summary.sessions,
5931
+ switches: live.summary.switches,
5932
+ traceEvents: live.summary.traceEvents
5933
+ }
5934
+ };
5935
+ };
5936
+ var renderVoiceProfileSwitchReadinessHTML = (report, options = {}) => {
5937
+ const title = options.title ?? "Voice Profile Switch Readiness";
5938
+ const issues = report.issues.map((issue) => `<li class="${escapeHtml3(issue.status)}"><strong>${escapeHtml3(issue.code)}</strong><p>${escapeHtml3(issue.message)}</p></li>`).join("");
5939
+ const sessions = report.live.sessions.map((session) => `<tr><td>${escapeHtml3(session.sessionId)}</td><td>${escapeHtml3(session.actions.join(", ") || "none")}</td><td>${String(session.decisions.length)}</td><td>${String(session.autoApplied)}</td><td>${escapeHtml3(session.blockedByPolicy.join(", ") || "none")}</td></tr>`).join("");
5940
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml3(title)}</title><style>:root{color-scheme:dark;background:#0b1020;color:#eff6ff;font-family:ui-sans-serif,system-ui,sans-serif}body{margin:0;padding:32px;background:radial-gradient(circle at top left,rgba(59,130,246,.2),transparent 34%),#0b1020}main{max-width:1140px;margin:0 auto}.hero,.card{background:rgba(15,23,42,.82);border:1px solid rgba(147,197,253,.24);border-radius:24px;margin-bottom:18px;padding:24px}.status{border-radius:999px;display:inline-flex;font-weight:900;padding:8px 12px}.pass{background:rgba(34,197,94,.18);color:#bbf7d0}.warn{background:rgba(245,158,11,.18);color:#fde68a}.fail{background:rgba(239,68,68,.18);color:#fecaca}.grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin-top:16px}.metric{background:#0f172a;border:1px solid rgba(147,197,253,.2);border-radius:18px;padding:16px}.metric span{color:#93c5fd;display:block;font-size:.75rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.metric strong{display:block;font-size:1.8rem;margin-top:8px}ul{display:grid;gap:10px;list-style:none;padding:0}li{border-radius:16px;padding:14px}li p{margin:.35rem 0 0}table{border-collapse:collapse;width:100%}td,th{border-bottom:1px solid rgba(147,197,253,.18);padding:12px;text-align:left}pre{background:#020617;border-radius:18px;color:#dbeafe;overflow:auto;padding:18px}</style></head><body><main><section class="hero"><h1>${escapeHtml3(title)}</h1><p>Deploy-facing readiness for profile switching: policy proof, audit evidence, trace evidence, and live session behavior.</p><p class="status ${escapeHtml3(report.status)}">${escapeHtml3(report.status.toUpperCase())}</p><div class="grid"><div class="metric"><span>Sessions</span><strong>${String(report.summary.sessions)}</strong></div><div class="metric"><span>Decisions</span><strong>${String(report.summary.decisions)}</strong></div><div class="metric"><span>Policy cases</span><strong>${String(report.summary.policyCases ?? 0)}</strong></div><div class="metric"><span>Audit</span><strong>${String(report.summary.auditEvents)}</strong></div><div class="metric"><span>Trace</span><strong>${String(report.summary.traceEvents)}</strong></div></div></section><section class="card"><h2>Issues</h2>${issues ? `<ul>${issues}</ul>` : '<p class="pass status">No readiness issues.</p>'}</section><section class="card"><h2>Live Sessions</h2>${sessions ? `<table><thead><tr><th>Session</th><th>Actions</th><th>Decisions</th><th>Auto applied</th><th>Blocked by</th></tr></thead><tbody>${sessions}</tbody></table>` : "<p>No live guard sessions found.</p>"}</section><section class="card"><h2>Raw Report</h2><pre>${stringifyForHtml(report)}</pre></section></main></body></html>`;
5941
+ };
5942
+ var createVoiceProfileSwitchReadinessRoutes = (options) => {
5943
+ const path = options.path ?? "/api/voice/profile-switch-readiness";
5944
+ const htmlPath = options.htmlPath === undefined ? "/voice/profile-switch-readiness" : options.htmlPath;
5945
+ const routes = new Elysia({
5946
+ name: options.name ?? "absolutejs-voice-profile-switch-readiness"
5947
+ }).get(path, async ({ query }) => buildVoiceProfileSwitchReadinessReport({
5948
+ ...options,
5949
+ limit: typeof query.limit === "string" && Number.isFinite(Number(query.limit)) ? Number(query.limit) : options.limit,
5950
+ sessionId: typeof query.sessionId === "string" && query.sessionId.trim() ? query.sessionId.trim() : options.sessionId
5951
+ }));
5952
+ if (htmlPath) {
5953
+ routes.get(htmlPath, async ({ query }) => {
5954
+ const report = await buildVoiceProfileSwitchReadinessReport({
5955
+ ...options,
5956
+ limit: typeof query.limit === "string" && Number.isFinite(Number(query.limit)) ? Number(query.limit) : options.limit,
5957
+ sessionId: typeof query.sessionId === "string" && query.sessionId.trim() ? query.sessionId.trim() : options.sessionId
5958
+ });
5959
+ const render = options.render ?? ((input) => renderVoiceProfileSwitchReadinessHTML(input, { title: options.title }));
5960
+ return new Response(await render(report), {
5961
+ headers: { "Content-Type": "text/html; charset=utf-8" }
5962
+ });
5963
+ });
5964
+ }
5965
+ return routes;
5966
+ };
5684
5967
 
5685
5968
  // src/plugin.ts
5686
5969
  var resolveQueryScenario = (query) => {
@@ -37661,7 +37944,9 @@ export {
37661
37944
  renderVoiceProviderCapabilityHTML,
37662
37945
  renderVoiceProofTrendRecommendationMarkdown,
37663
37946
  renderVoiceProofTrendRecommendationHTML,
37947
+ renderVoiceProfileSwitchReadinessHTML,
37664
37948
  renderVoiceProfileSwitchPolicyProofHTML,
37949
+ renderVoiceProfileSwitchLiveDecisionHTML,
37665
37950
  renderVoiceProductionReadinessHTML,
37666
37951
  renderVoicePostCallAnalysisMarkdown,
37667
37952
  renderVoicePhoneAgentProductionSmokeHTML,
@@ -37915,7 +38200,9 @@ export {
37915
38200
  createVoiceProviderCapabilityHTMLHandler,
37916
38201
  createVoiceProofTrendRoutes,
37917
38202
  createVoiceProofTrendRecommendationRoutes,
38203
+ createVoiceProfileSwitchReadinessRoutes,
37918
38204
  createVoiceProfileSwitchPolicyProofRoutes,
38205
+ createVoiceProfileSwitchLiveDecisionRoutes,
37919
38206
  createVoiceProductionReadinessRoutes,
37920
38207
  createVoicePostgresTraceSinkDeliveryStore,
37921
38208
  createVoicePostgresTraceEventStore,
@@ -38116,6 +38403,8 @@ export {
38116
38403
  buildVoiceProofTrendReport,
38117
38404
  buildVoiceProofTrendRecommendationReport,
38118
38405
  buildVoiceProofTrendProfileSummaries,
38406
+ buildVoiceProfileSwitchReadinessReport,
38407
+ buildVoiceProfileSwitchLiveDecisionReport,
38119
38408
  buildVoiceProductionReadinessReport,
38120
38409
  buildVoiceProductionReadinessGate,
38121
38410
  buildVoicePostCallAnalysisReport,
@@ -1,6 +1,7 @@
1
1
  import { Elysia } from 'elysia';
2
2
  import type { VoiceRealCallProfileDefault, VoiceRealCallProfileDefaultsReport, VoiceRealCallProfileHistoryReport } from './proofTrends';
3
3
  import { type StoredVoiceAuditEvent, type VoiceAuditActor, type VoiceAuditEventStore } from './audit';
4
+ import type { VoiceTraceEventStore } from './trace';
4
5
  export type VoiceProfileSwitchObservedSignals = {
5
6
  currentProfileId?: string;
6
7
  fallbackUsed?: boolean;
@@ -110,6 +111,109 @@ export type VoiceProfileSwitchPolicyProofRoutesOptions = VoiceProfileSwitchPolic
110
111
  path?: string;
111
112
  render?: (report: VoiceProfileSwitchPolicyProofReport) => string | Promise<string>;
112
113
  };
114
+ export type VoiceProfileSwitchLiveDecisionEvidence = {
115
+ action?: VoiceProfileSwitchGuardAction | string;
116
+ at: number;
117
+ auditEventId?: string;
118
+ autoApplied?: boolean;
119
+ autoSwitchCount?: number;
120
+ blockedByPolicy?: string;
121
+ confidence?: number;
122
+ maxAutoSwitchesPerSession?: number;
123
+ minConfidence?: number;
124
+ mode?: VoiceProfileSwitchGuardMode | string;
125
+ outcome?: string;
126
+ previousProfileId?: string;
127
+ reason?: string;
128
+ recommendedProfileId?: string;
129
+ scenarioId?: string;
130
+ selectedProfileId?: string;
131
+ sessionId: string;
132
+ source: 'audit' | 'trace';
133
+ status?: string;
134
+ traceEventId?: string;
135
+ traceId?: string;
136
+ };
137
+ export type VoiceProfileSwitchLiveDecisionSession = {
138
+ actionCounts: Record<string, number>;
139
+ actions: string[];
140
+ autoApplied: number;
141
+ blockedByPolicy: string[];
142
+ decisions: VoiceProfileSwitchLiveDecisionEvidence[];
143
+ firstSeenAt: number;
144
+ lastDecision?: VoiceProfileSwitchLiveDecisionEvidence;
145
+ lastSeenAt: number;
146
+ sessionId: string;
147
+ };
148
+ export type VoiceProfileSwitchLiveDecisionReport = {
149
+ generatedAt: string;
150
+ ok: boolean;
151
+ sessions: VoiceProfileSwitchLiveDecisionSession[];
152
+ summary: {
153
+ auditEvents: number;
154
+ autoApplied: number;
155
+ blocked: number;
156
+ decisions: number;
157
+ recommendations: number;
158
+ sessions: number;
159
+ switches: number;
160
+ traceEvents: number;
161
+ };
162
+ };
163
+ export type VoiceProfileSwitchLiveDecisionReportOptions = {
164
+ audit?: VoiceAuditEventStore;
165
+ limit?: number;
166
+ sessionId?: string;
167
+ trace?: VoiceTraceEventStore;
168
+ };
169
+ export type VoiceProfileSwitchLiveDecisionRoutesOptions = VoiceProfileSwitchLiveDecisionReportOptions & {
170
+ htmlPath?: false | string;
171
+ name?: string;
172
+ path?: string;
173
+ render?: (report: VoiceProfileSwitchLiveDecisionReport) => string | Promise<string>;
174
+ title?: string;
175
+ };
176
+ export type VoiceProfileSwitchReadinessStatus = 'fail' | 'pass' | 'warn';
177
+ export type VoiceProfileSwitchReadinessIssue = {
178
+ code: string;
179
+ message: string;
180
+ status: Exclude<VoiceProfileSwitchReadinessStatus, 'pass'>;
181
+ };
182
+ export type VoiceProfileSwitchReadinessReport = {
183
+ generatedAt: string;
184
+ issues: VoiceProfileSwitchReadinessIssue[];
185
+ live: VoiceProfileSwitchLiveDecisionReport;
186
+ policy?: VoiceProfileSwitchPolicyProofReport;
187
+ status: VoiceProfileSwitchReadinessStatus;
188
+ summary: {
189
+ auditEvents: number;
190
+ autoApplied: number;
191
+ blocked: number;
192
+ decisions: number;
193
+ policyCases?: number;
194
+ sessions: number;
195
+ switches: number;
196
+ traceEvents: number;
197
+ };
198
+ };
199
+ export type VoiceProfileSwitchReadinessOptions = VoiceProfileSwitchLiveDecisionReportOptions & {
200
+ autoMode?: boolean;
201
+ maxBlockedRatio?: number;
202
+ maxAutoAppliedRatio?: number;
203
+ policyProof?: false | VoiceProfileSwitchPolicyProofOptions;
204
+ requireAudit?: boolean;
205
+ requireAllowedProfilePolicy?: boolean;
206
+ requireLiveDecisions?: boolean;
207
+ requirePolicyProof?: boolean;
208
+ requireTrace?: boolean;
209
+ };
210
+ export type VoiceProfileSwitchReadinessRoutesOptions = VoiceProfileSwitchReadinessOptions & {
211
+ htmlPath?: false | string;
212
+ name?: string;
213
+ path?: string;
214
+ render?: (report: VoiceProfileSwitchReadinessReport) => string | Promise<string>;
215
+ title?: string;
216
+ };
113
217
  export declare const recommendVoiceProfileSwitch: (options: VoiceProfileSwitchRecommendationOptions) => VoiceProfileSwitchRecommendation;
114
218
  export declare const applyVoiceProfileSwitchGuard: (options: VoiceProfileSwitchGuardOptions) => Promise<VoiceProfileSwitchGuardDecision>;
115
219
  export declare const runVoiceProfileSwitchPolicyProof: (options: VoiceProfileSwitchPolicyProofOptions) => Promise<VoiceProfileSwitchPolicyProofReport>;
@@ -156,3 +260,91 @@ export declare const createVoiceProfileSwitchPolicyProofRoutes: (options: VoiceP
156
260
  standaloneSchema: {};
157
261
  response: {};
158
262
  }>;
263
+ export declare const buildVoiceProfileSwitchLiveDecisionReport: (options: VoiceProfileSwitchLiveDecisionReportOptions) => Promise<VoiceProfileSwitchLiveDecisionReport>;
264
+ export declare const renderVoiceProfileSwitchLiveDecisionHTML: (report: VoiceProfileSwitchLiveDecisionReport, options?: {
265
+ title?: string;
266
+ }) => string;
267
+ export declare const createVoiceProfileSwitchLiveDecisionRoutes: (options: VoiceProfileSwitchLiveDecisionRoutesOptions) => Elysia<"", {
268
+ decorator: {};
269
+ store: {};
270
+ derive: {};
271
+ resolve: {};
272
+ }, {
273
+ typebox: {};
274
+ error: {};
275
+ }, {
276
+ schema: {};
277
+ standaloneSchema: {};
278
+ macro: {};
279
+ macroFn: {};
280
+ parser: {};
281
+ response: {};
282
+ }, {
283
+ [x: string]: {
284
+ get: {
285
+ body: unknown;
286
+ params: {};
287
+ query: unknown;
288
+ headers: unknown;
289
+ response: {
290
+ 200: VoiceProfileSwitchLiveDecisionReport;
291
+ };
292
+ };
293
+ };
294
+ }, {
295
+ derive: {};
296
+ resolve: {};
297
+ schema: {};
298
+ standaloneSchema: {};
299
+ response: {};
300
+ }, {
301
+ derive: {};
302
+ resolve: {};
303
+ schema: {};
304
+ standaloneSchema: {};
305
+ response: {};
306
+ }>;
307
+ export declare const buildVoiceProfileSwitchReadinessReport: (options: VoiceProfileSwitchReadinessOptions) => Promise<VoiceProfileSwitchReadinessReport>;
308
+ export declare const renderVoiceProfileSwitchReadinessHTML: (report: VoiceProfileSwitchReadinessReport, options?: {
309
+ title?: string;
310
+ }) => string;
311
+ export declare const createVoiceProfileSwitchReadinessRoutes: (options: VoiceProfileSwitchReadinessRoutesOptions) => Elysia<"", {
312
+ decorator: {};
313
+ store: {};
314
+ derive: {};
315
+ resolve: {};
316
+ }, {
317
+ typebox: {};
318
+ error: {};
319
+ }, {
320
+ schema: {};
321
+ standaloneSchema: {};
322
+ macro: {};
323
+ macroFn: {};
324
+ parser: {};
325
+ response: {};
326
+ }, {
327
+ [x: string]: {
328
+ get: {
329
+ body: unknown;
330
+ params: {};
331
+ query: unknown;
332
+ headers: unknown;
333
+ response: {
334
+ 200: VoiceProfileSwitchReadinessReport;
335
+ };
336
+ };
337
+ };
338
+ }, {
339
+ derive: {};
340
+ resolve: {};
341
+ schema: {};
342
+ standaloneSchema: {};
343
+ response: {};
344
+ }, {
345
+ derive: {};
346
+ resolve: {};
347
+ schema: {};
348
+ standaloneSchema: {};
349
+ response: {};
350
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.360",
3
+ "version": "0.0.22-beta.362",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",