@absolutejs/voice 0.0.22-beta.435 → 0.0.22-beta.437

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.
@@ -1,5 +1,6 @@
1
1
  import { Elysia } from 'elysia';
2
2
  import { type VoiceOperationsRecord, type VoiceOperationsRecordOptions } from './operationsRecord';
3
+ import type { VoiceIncidentRecoveryOutcomeReport } from './incidentTimeline';
3
4
  import { type VoiceTraceRedactionConfig } from './trace';
4
5
  export type VoiceIncidentBundleFormat = 'json' | 'markdown';
5
6
  export type VoiceIncidentBundle = {
@@ -8,6 +9,7 @@ export type VoiceIncidentBundle = {
8
9
  formatVersion: 1;
9
10
  markdown: string;
10
11
  record: VoiceOperationsRecord;
12
+ recoveryOutcomes?: VoiceIncidentRecoveryOutcomeReport;
11
13
  redacted: boolean;
12
14
  sessionId: string;
13
15
  summary: VoiceIncidentBundleSummary;
@@ -46,6 +48,7 @@ export type VoiceIncidentBundleSummary = {
46
48
  };
47
49
  export type VoiceIncidentBundleOptions = VoiceOperationsRecordOptions & {
48
50
  redact?: VoiceTraceRedactionConfig;
51
+ recoveryOutcomes?: VoiceIncidentRecoveryOutcomeReport;
49
52
  title?: string;
50
53
  };
51
54
  export type VoiceIncidentBundleRoutesOptions = Omit<VoiceIncidentBundleOptions, 'sessionId'> & {
@@ -38,6 +38,31 @@ export type VoiceIncidentRecoveryActionHandlerInput = {
38
38
  request: Request;
39
39
  };
40
40
  export type VoiceIncidentRecoveryActionHandler = (input: VoiceIncidentRecoveryActionHandlerInput) => Promise<VoiceIncidentRecoveryActionResult> | VoiceIncidentRecoveryActionResult;
41
+ export type VoiceIncidentRecoveryOutcome = 'failed' | 'improved' | 'regressed' | 'unchanged';
42
+ export type VoiceIncidentRecoveryOutcomeEntry = {
43
+ actionId: string;
44
+ afterStatus?: VoiceIncidentTimelineStatus;
45
+ at: number;
46
+ beforeStatus?: VoiceIncidentTimelineStatus;
47
+ detail?: string;
48
+ eventId: string;
49
+ outcome: VoiceIncidentRecoveryOutcome;
50
+ status?: number;
51
+ traceId?: string;
52
+ };
53
+ export type VoiceIncidentRecoveryOutcomeReport = {
54
+ checkedAt: number;
55
+ entries: VoiceIncidentRecoveryOutcomeEntry[];
56
+ failed: number;
57
+ improved: number;
58
+ regressed: number;
59
+ total: number;
60
+ unchanged: number;
61
+ };
62
+ export type VoiceIncidentRecoveryOutcomeOptions = {
63
+ audit?: VoiceAuditEventStore;
64
+ limit?: number;
65
+ };
41
66
  export type VoiceIncidentTimelineEvent = {
42
67
  action?: VoiceIncidentTimelineAction;
43
68
  at: number;
@@ -102,9 +127,15 @@ export type VoiceIncidentTimelineRoutesOptions = VoiceIncidentTimelineOptions &
102
127
  name?: string;
103
128
  path?: string;
104
129
  render?: (report: VoiceIncidentTimelineReport) => string | Promise<string>;
130
+ recoveryOutcomeHtmlPath?: false | string;
131
+ recoveryOutcomePath?: false | string;
105
132
  title?: string;
106
133
  trace?: VoiceTraceEventStore;
107
134
  };
135
+ export declare const buildVoiceIncidentRecoveryOutcomeReport: (options: VoiceIncidentRecoveryOutcomeOptions) => Promise<VoiceIncidentRecoveryOutcomeReport>;
136
+ export declare const renderVoiceIncidentRecoveryOutcomeHTML: (report: VoiceIncidentRecoveryOutcomeReport, options?: {
137
+ title?: string;
138
+ }) => string;
108
139
  export declare const buildVoiceIncidentTimelineReport: (options: VoiceIncidentTimelineOptions) => Promise<VoiceIncidentTimelineReport>;
109
140
  export declare const renderVoiceIncidentTimelineMarkdown: (report: VoiceIncidentTimelineReport, options?: {
110
141
  title?: string;
package/dist/index.d.ts CHANGED
@@ -52,7 +52,7 @@ export { assertVoiceLiveOpsControlEvidence, assertVoiceLiveOpsEvidence, buildVoi
52
52
  export type { VoiceLiveOpsAction, VoiceLiveOpsActionInput, VoiceLiveOpsActionResult, VoiceLiveOpsControllerOptions, VoiceLiveOpsControlState, VoiceLiveOpsControlStatus, VoiceLiveOpsControlStore, VoiceLiveOpsControlEvidenceInput, VoiceLiveOpsControlEvidenceReport, VoiceLiveOpsEvidenceInput, VoiceLiveOpsEvidenceReport, VoiceLiveOpsRoutesOptions } from './liveOps';
53
53
  export { buildVoiceDeliveryRuntimeReport, createVoiceDeliveryRuntime, createVoiceDeliveryRuntimePresetConfig, createVoiceDeliveryRuntimeRoutes, renderVoiceDeliveryRuntimeHTML } from './deliveryRuntime';
54
54
  export { buildVoiceOperationalStatusReport, createVoiceOperationalStatusRoutes, renderVoiceOperationalStatusHTML } from './operationalStatus';
55
- export { buildVoiceIncidentTimelineReport, createVoiceIncidentTimelineRoutes, renderVoiceIncidentTimelineHTML, renderVoiceIncidentTimelineMarkdown } from './incidentTimeline';
55
+ export { buildVoiceIncidentRecoveryOutcomeReport, buildVoiceIncidentTimelineReport, createVoiceIncidentTimelineRoutes, renderVoiceIncidentRecoveryOutcomeHTML, renderVoiceIncidentTimelineHTML, renderVoiceIncidentTimelineMarkdown } from './incidentTimeline';
56
56
  export { applyVoiceDataRetentionPolicy, assertVoiceDataControlEvidence, buildVoiceDataControlReport, buildVoiceDataRetentionPlan, createVoiceDataControlRoutes, createVoiceZeroRetentionPolicy, evaluateVoiceDataControlEvidence, renderVoiceDataControlHTML, renderVoiceDataControlMarkdown, voiceComplianceRedactionDefaults } from './dataControl';
57
57
  export type { VoiceDataControlAssertionInput, VoiceDataControlAssertionReport, VoiceDataControlProviderKeySurface, VoiceDataControlReport, VoiceDataControlRoutesOptions, VoiceDataControlStorageSurface, VoiceDataRetentionPolicy, VoiceDataRetentionReport, VoiceDataRetentionScope, VoiceDataRetentionScopeReport, VoiceDataRetentionStores } from './dataControl';
58
58
  export type { VoiceDemoReadyReport, VoiceDemoReadyRoutesOptions, VoiceDemoReadySection, VoiceDemoReadyStatus } from './demoReadyRoutes';
@@ -60,7 +60,7 @@ export type { VoiceDeliverySinkDescriptor, VoiceDeliverySinkDescriptorInput, Voi
60
60
  export type { VoiceOpsActionAuditRecord, VoiceOpsActionAuditRoutesOptions, VoiceOpsActionHistoryEntry, VoiceOpsActionHistoryReport } from './opsActionAuditRoutes';
61
61
  export type { VoiceDeliveryRuntime, VoiceDeliveryRuntimeAuditConfig, VoiceDeliveryRuntimeConfig, VoiceDeliveryRuntimeFilePresetOptions, VoiceDeliveryRuntimePresetLeaseConfig, VoiceDeliveryRuntimePresetMode, VoiceDeliveryRuntimePresetOptions, VoiceDeliveryRuntimeReport, VoiceDeliveryRuntimeRoutesOptions, VoiceDeliveryRuntimeS3PresetOptions, VoiceDeliveryRuntimeSummary, VoiceDeliveryRuntimeTickResult, VoiceDeliveryRuntimeTraceConfig, VoiceDeliveryRuntimeWebhookPresetOptions } from './deliveryRuntime';
62
62
  export type { VoiceOperationalStatus, VoiceOperationalStatusCheck, VoiceOperationalStatusOptions, VoiceOperationalStatusReport, VoiceOperationalStatusRoutesOptions, VoiceOperationalStatusValue } from './operationalStatus';
63
- export type { VoiceIncidentRecoveryAction, VoiceIncidentRecoveryActionHandler, VoiceIncidentRecoveryActionHandlerInput, VoiceIncidentRecoveryActionResult, VoiceIncidentTimelineAction, VoiceIncidentTimelineEvent, VoiceIncidentTimelineLinks, VoiceIncidentTimelineOptions, VoiceIncidentTimelineReport, VoiceIncidentTimelineRoutesOptions, VoiceIncidentTimelineSeverity, VoiceIncidentTimelineStatus, VoiceIncidentTimelineValue } from './incidentTimeline';
63
+ export type { VoiceIncidentRecoveryAction, VoiceIncidentRecoveryActionHandler, VoiceIncidentRecoveryActionHandlerInput, VoiceIncidentRecoveryActionResult, VoiceIncidentRecoveryOutcome, VoiceIncidentRecoveryOutcomeEntry, VoiceIncidentRecoveryOutcomeOptions, VoiceIncidentRecoveryOutcomeReport, VoiceIncidentTimelineAction, VoiceIncidentTimelineEvent, VoiceIncidentTimelineLinks, VoiceIncidentTimelineOptions, VoiceIncidentTimelineReport, VoiceIncidentTimelineRoutesOptions, VoiceIncidentTimelineSeverity, VoiceIncidentTimelineStatus, VoiceIncidentTimelineValue } from './incidentTimeline';
64
64
  export { compareVoiceEvalBaseline, createVoiceFileEvalBaselineStore, createVoiceFileScenarioFixtureStore, createVoiceEvalRoutes, renderVoiceEvalBaselineHTML, renderVoiceEvalHTML, renderVoiceScenarioEvalHTML, renderVoiceScenarioFixtureEvalHTML, runVoiceScenarioEvals, runVoiceScenarioFixtureEvals, runVoiceSessionEvals } from './evalRoutes';
65
65
  export { assertVoiceSimulationSuiteEvidence, createVoiceSimulationSuiteRoutes, evaluateVoiceSimulationSuiteEvidence, renderVoiceSimulationSuiteHTML, runVoiceSimulationSuite } from './simulationSuite';
66
66
  export { createVoiceWorkflowContract, createVoiceWorkflowContractHandler, createVoiceWorkflowContractPreset, createVoiceWorkflowScenario, recordVoiceWorkflowContractTrace, validateVoiceWorkflowRouteResult } from './workflowContract';
package/dist/index.js CHANGED
@@ -26759,6 +26759,41 @@ var createCallDebuggerArtifact = (report, href) => ({
26759
26759
  sessionId: report.sessionId,
26760
26760
  status: "pass"
26761
26761
  });
26762
+ var createIncidentBundleArtifact = (bundle) => ({
26763
+ generatedAt: bundle.exportedAt,
26764
+ id: `incident-bundle:${bundle.sessionId}`,
26765
+ kind: "incident",
26766
+ label: `Incident bundle ${bundle.sessionId}`,
26767
+ metadata: {
26768
+ errors: bundle.summary.errors,
26769
+ recoveryOutcomes: bundle.recoveryOutcomes ? {
26770
+ failed: bundle.recoveryOutcomes.failed,
26771
+ improved: bundle.recoveryOutcomes.improved,
26772
+ regressed: bundle.recoveryOutcomes.regressed,
26773
+ total: bundle.recoveryOutcomes.total,
26774
+ unchanged: bundle.recoveryOutcomes.unchanged
26775
+ } : undefined,
26776
+ status: bundle.summary.status
26777
+ },
26778
+ required: true,
26779
+ sessionId: bundle.sessionId,
26780
+ status: bundle.summary.status === "failed" ? "fail" : bundle.summary.status === "warning" ? "warn" : "pass"
26781
+ });
26782
+ var createIncidentRecoveryOutcomeArtifact = (report) => ({
26783
+ generatedAt: report.checkedAt,
26784
+ id: `incident-recovery-outcomes:${report.checkedAt}`,
26785
+ kind: "incident-recovery-outcomes",
26786
+ label: "Incident recovery outcomes",
26787
+ metadata: {
26788
+ failed: report.failed,
26789
+ improved: report.improved,
26790
+ regressed: report.regressed,
26791
+ total: report.total,
26792
+ unchanged: report.unchanged
26793
+ },
26794
+ required: true,
26795
+ status: report.failed > 0 || report.regressed > 0 ? "fail" : report.unchanged > 0 ? "warn" : "pass"
26796
+ });
26762
26797
  var unique2 = (values) => [...new Set(values)].sort();
26763
26798
  var stripArtifactPathAnchor = (path) => path.split("#")[0] ?? path;
26764
26799
  var toEpochMs = (value) => {
@@ -27419,6 +27454,7 @@ var collectSessionIds = (input) => unique2([
27419
27454
  ...input.events.map((event) => event.sessionId),
27420
27455
  ...input.auditEvents.map((event) => event.sessionId).filter((sessionId) => Boolean(sessionId)),
27421
27456
  ...input.operationsRecords.map((record) => record.sessionId),
27457
+ ...input.incidentBundles.map((bundle) => bundle.sessionId),
27422
27458
  ...input.sessionSnapshots.map((snapshot) => snapshot.sessionId),
27423
27459
  ...input.callDebuggerReports.map((report) => report.sessionId)
27424
27460
  ]);
@@ -27443,6 +27479,15 @@ var collectIssues = (input) => {
27443
27479
  });
27444
27480
  }
27445
27481
  for (const artifact of input.artifacts) {
27482
+ if (artifact.required && (artifact.status === "fail" || artifact.status === "warn")) {
27483
+ issues.push({
27484
+ code: "voice.observability.artifact_failed",
27485
+ detail: `${artifact.label} reported ${artifact.status}.`,
27486
+ label: "Artifact status",
27487
+ severity: artifact.status,
27488
+ value: artifact.id
27489
+ });
27490
+ }
27446
27491
  if (artifact.path && artifact.status !== "pass" && artifact.required && artifact.bytes === undefined && artifact.freshness?.ageMs === undefined) {
27447
27492
  issues.push({
27448
27493
  code: "voice.observability.artifact_missing",
@@ -27541,14 +27586,22 @@ var buildVoiceObservabilityExport = async (options = {}) => {
27541
27586
  const events = await time("events", async () => options.events ?? await options.store?.list() ?? []);
27542
27587
  const auditEvents = await time("auditEvents", async () => options.audit ? await options.audit.list() : []);
27543
27588
  const baseOperationsRecords = options.operationsRecords ?? [];
27544
- const [sessionSnapshots, callDebuggerReports] = await time("supportArtifacts", () => Promise.all([
27589
+ const [
27590
+ sessionSnapshots,
27591
+ callDebuggerReports,
27592
+ incidentBundles,
27593
+ incidentRecoveryOutcomeReports
27594
+ ] = await time("supportArtifacts", () => Promise.all([
27545
27595
  resolveObservabilityExportList(options.sessionSnapshots),
27546
- resolveObservabilityExportList(options.callDebuggerReports)
27596
+ resolveObservabilityExportList(options.callDebuggerReports),
27597
+ resolveObservabilityExportList(options.incidentBundles),
27598
+ resolveObservabilityExportList(options.incidentRecoveryOutcomeReports)
27547
27599
  ]));
27548
27600
  const sessionIds = await time("sessionIds", () => collectSessionIds({
27549
27601
  auditEvents,
27550
27602
  callDebuggerReports,
27551
27603
  events,
27604
+ incidentBundles,
27552
27605
  operationsRecords: baseOperationsRecords,
27553
27606
  sessionIds: options.sessionIds,
27554
27607
  sessionSnapshots
@@ -27575,10 +27628,14 @@ var buildVoiceObservabilityExport = async (options = {}) => {
27575
27628
  const operationArtifacts = await time("operationArtifacts", () => operationsRecords.map((record) => createOperationArtifact(record, options.links?.operationsRecord?.(record.sessionId))));
27576
27629
  const sessionSnapshotArtifacts = await time("sessionSnapshotArtifacts", () => sessionSnapshots.map((snapshot) => createSessionSnapshotArtifact(snapshot, options.links?.sessionSnapshot?.(snapshot.sessionId))));
27577
27630
  const callDebuggerArtifacts = await time("callDebuggerArtifacts", () => callDebuggerReports.map((report) => createCallDebuggerArtifact(report, options.links?.callDebugger?.(report.sessionId))));
27631
+ const incidentBundleArtifacts = await time("incidentBundleArtifacts", () => incidentBundles.map(createIncidentBundleArtifact));
27632
+ const incidentRecoveryOutcomeArtifacts = await time("incidentRecoveryOutcomeArtifacts", () => incidentRecoveryOutcomeReports.map(createIncidentRecoveryOutcomeArtifact));
27578
27633
  const artifacts = await time("artifacts", async () => addArtifactDownloadHrefs(await verifyArtifacts([
27579
27634
  ...operationArtifacts,
27580
27635
  ...sessionSnapshotArtifacts,
27581
27636
  ...callDebuggerArtifacts,
27637
+ ...incidentBundleArtifacts,
27638
+ ...incidentRecoveryOutcomeArtifacts,
27582
27639
  ...options.artifacts ?? []
27583
27640
  ], options.artifactIntegrity), options.links));
27584
27641
  const operationHrefBySessionId = new Map(sessionIds.map((sessionId) => [
@@ -30183,6 +30240,62 @@ var defaultIncidentRecoveryActions = (events, links) => {
30183
30240
  return actions;
30184
30241
  };
30185
30242
  var worstStatus3 = (statuses) => statuses.includes("fail") ? "fail" : statuses.includes("warn") ? "warn" : "pass";
30243
+ var statusRank6 = (status) => status === "fail" ? 3 : status === "warn" ? 2 : status === "pass" ? 1 : 0;
30244
+ var isRecord4 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
30245
+ var getIncidentRecoveryBody = (event) => {
30246
+ const payload = isRecord4(event.payload) ? event.payload : {};
30247
+ return isRecord4(payload.body) ? payload.body : {};
30248
+ };
30249
+ var getIncidentRecoveryStatus = (value) => value === "fail" || value === "pass" || value === "warn" ? value : undefined;
30250
+ var getIncidentRecoveryDetail = (event) => {
30251
+ const payload = isRecord4(event.payload) ? event.payload : {};
30252
+ const body = getIncidentRecoveryBody(event);
30253
+ const result = isRecord4(body.result) ? body.result : {};
30254
+ const detail = result.detail ?? payload.error;
30255
+ return typeof detail === "string" ? detail : undefined;
30256
+ };
30257
+ var toIncidentRecoveryOutcomeEntry = (event) => {
30258
+ const body = getIncidentRecoveryBody(event);
30259
+ const beforeStatus = getIncidentRecoveryStatus(body.beforeStatus);
30260
+ const afterStatus = getIncidentRecoveryStatus(body.afterStatus);
30261
+ const beforeRank = statusRank6(beforeStatus);
30262
+ const afterRank = statusRank6(afterStatus);
30263
+ const outcome = event.outcome === "error" ? "failed" : beforeRank > 0 && afterRank > 0 && afterRank < beforeRank ? "improved" : beforeRank > 0 && afterRank > beforeRank ? "regressed" : "unchanged";
30264
+ const payload = isRecord4(event.payload) ? event.payload : {};
30265
+ return {
30266
+ actionId: event.action.replace(/^incident\./, ""),
30267
+ afterStatus,
30268
+ at: event.at,
30269
+ beforeStatus,
30270
+ detail: getIncidentRecoveryDetail(event),
30271
+ eventId: event.id,
30272
+ outcome,
30273
+ status: typeof payload.status === "number" ? payload.status : undefined,
30274
+ traceId: event.traceId
30275
+ };
30276
+ };
30277
+ var buildVoiceIncidentRecoveryOutcomeReport = async (options) => {
30278
+ const events = options.audit ? await options.audit.list({
30279
+ limit: options.limit ?? 50,
30280
+ resourceType: "voice.ops.action",
30281
+ type: "operator.action"
30282
+ }) : [];
30283
+ const entries = events.filter((event) => event.action.startsWith("incident.")).map(toIncidentRecoveryOutcomeEntry).sort((left, right) => right.at - left.at);
30284
+ return {
30285
+ checkedAt: Date.now(),
30286
+ entries,
30287
+ failed: entries.filter((entry) => entry.outcome === "failed").length,
30288
+ improved: entries.filter((entry) => entry.outcome === "improved").length,
30289
+ regressed: entries.filter((entry) => entry.outcome === "regressed").length,
30290
+ total: entries.length,
30291
+ unchanged: entries.filter((entry) => entry.outcome === "unchanged").length
30292
+ };
30293
+ };
30294
+ var renderVoiceIncidentRecoveryOutcomeHTML = (report, options = {}) => {
30295
+ const title = options.title ?? "AbsoluteJS Voice Incident Recovery Outcomes";
30296
+ const rows = report.entries.map((entry) => `<article class="${escapeHtml43(entry.outcome)}"><span>${escapeHtml43(entry.outcome.toUpperCase())}</span><h2>${escapeHtml43(entry.actionId)}</h2><p>${escapeHtml43(new Date(entry.at).toLocaleString())}</p><strong>${escapeHtml43(entry.beforeStatus ?? "unknown")} -> ${escapeHtml43(entry.afterStatus ?? "unknown")}</strong>${entry.detail ? `<p>${escapeHtml43(entry.detail)}</p>` : ""}</article>`).join("");
30297
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml43(title)}</title><style>body{background:#10120d;color:#fbf4df;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:980px;padding:32px}.hero,article{background:#181711;border:1px solid #39301d;border-radius:24px;padding:20px}.hero{margin-bottom:16px}h1{font-size:clamp(2rem,6vw,4.5rem);line-height:.95}.summary{display:flex;flex-wrap:wrap;gap:10px}.summary span{border:1px solid #4a3f23;border-radius:999px;padding:8px 12px}section{display:grid;gap:12px}article.improved{border-color:rgba(34,197,94,.65)}article.failed,article.regressed{border-color:rgba(239,68,68,.8)}article.unchanged{border-color:rgba(245,158,11,.7)}article span{color:#fcd34d;font-weight:900;letter-spacing:.08em}article strong{display:block;font-size:1.4rem;margin:.5rem 0}p{color:#cfc5a8}</style></head><body><main><section class="hero"><span>Recovery proof</span><h1>${escapeHtml43(title)}</h1><div class="summary"><span>${String(report.improved)} improved</span><span>${String(report.unchanged)} unchanged</span><span>${String(report.regressed)} regressed</span><span>${String(report.failed)} failed</span><span>${String(report.total)} total</span></div></section><section>${rows || "<p>No incident recovery actions have been recorded.</p>"}</section></main></body></html>`;
30298
+ };
30186
30299
  var pushOperationalStatusEvents = (events, report, links) => {
30187
30300
  if (!report) {
30188
30301
  return;
@@ -30429,6 +30542,8 @@ var createVoiceIncidentTimelineRoutes = (options) => {
30429
30542
  const htmlPath = options.htmlPath === undefined ? "/voice/incident-timeline" : options.htmlPath;
30430
30543
  const markdownPath = options.markdownPath === undefined ? "/voice/incident-timeline.md" : options.markdownPath;
30431
30544
  const actionPath = options.actionPath === undefined ? "/api/voice/incident-timeline/actions" : options.actionPath;
30545
+ const recoveryOutcomePath = options.recoveryOutcomePath === undefined ? "/api/voice/incident-timeline/recovery-outcomes" : options.recoveryOutcomePath;
30546
+ const recoveryOutcomeHtmlPath = options.recoveryOutcomeHtmlPath === undefined ? "/voice/incident-recovery-outcomes" : options.recoveryOutcomeHtmlPath;
30432
30547
  const routes = new Elysia46({
30433
30548
  name: options.name ?? "absolutejs-voice-incident-timeline"
30434
30549
  }).get(path, async () => {
@@ -30552,6 +30667,34 @@ var createVoiceIncidentTimelineRoutes = (options) => {
30552
30667
  });
30553
30668
  });
30554
30669
  }
30670
+ if (recoveryOutcomePath !== false) {
30671
+ routes.get(recoveryOutcomePath, async () => {
30672
+ const report = await buildVoiceIncidentRecoveryOutcomeReport({
30673
+ audit: options.audit
30674
+ });
30675
+ return new Response(JSON.stringify(report), {
30676
+ headers: {
30677
+ "Content-Type": "application/json; charset=utf-8",
30678
+ ...options.headers
30679
+ }
30680
+ });
30681
+ });
30682
+ }
30683
+ if (recoveryOutcomeHtmlPath !== false) {
30684
+ routes.get(recoveryOutcomeHtmlPath, async () => {
30685
+ const report = await buildVoiceIncidentRecoveryOutcomeReport({
30686
+ audit: options.audit
30687
+ });
30688
+ return new Response(renderVoiceIncidentRecoveryOutcomeHTML(report, {
30689
+ title: `${options.title ?? "AbsoluteJS Voice Incident Timeline"} Recovery Outcomes`
30690
+ }), {
30691
+ headers: {
30692
+ "Content-Type": "text/html; charset=utf-8",
30693
+ ...options.headers
30694
+ }
30695
+ });
30696
+ });
30697
+ }
30555
30698
  return routes;
30556
30699
  };
30557
30700
  // src/dataControl.ts
@@ -36062,7 +36205,7 @@ var defaultRequirement = {
36062
36205
  requireFallback: false,
36063
36206
  requireTimeoutBudget: false
36064
36207
  };
36065
- var statusRank6 = {
36208
+ var statusRank7 = {
36066
36209
  pass: 0,
36067
36210
  warn: 1,
36068
36211
  fail: 2
@@ -36078,7 +36221,7 @@ var surfaceProviderNames = (surface) => uniqueSorted8([
36078
36221
  ...isProviderList(surface.fallback) ? surface.fallback : [],
36079
36222
  ...isProviderList(surface.allowProviders) ? surface.allowProviders : []
36080
36223
  ]);
36081
- var surfaceStatus = (issues) => issues.reduce((status, issue) => statusRank6[issue.status] > statusRank6[status] ? issue.status : status, "pass");
36224
+ var surfaceStatus = (issues) => issues.reduce((status, issue) => statusRank7[issue.status] > statusRank7[status] ? issue.status : status, "pass");
36082
36225
  var resolvedRequirement = (surface, options) => ({
36083
36226
  ...defaultRequirement,
36084
36227
  ...options.defaultRequirement ?? {},
@@ -37222,12 +37365,12 @@ var recommendVoiceProviderStack = (input) => {
37222
37365
  };
37223
37366
  };
37224
37367
  var rollupContractStatus = (checks) => checks.some((check) => check.status === "fail") ? "fail" : checks.some((check) => check.status === "warn") ? "warn" : "pass";
37225
- var statusRank7 = {
37368
+ var statusRank8 = {
37226
37369
  pass: 0,
37227
37370
  warn: 1,
37228
37371
  fail: 2
37229
37372
  };
37230
- var statusExceeds2 = (actual, max2) => statusRank7[actual] > statusRank7[max2];
37373
+ var statusExceeds2 = (actual, max2) => statusRank8[actual] > statusRank8[max2];
37231
37374
  var buildVoiceProviderContractMatrix = (input) => {
37232
37375
  const rows = input.contracts.map((contract) => {
37233
37376
  const configured = contract.configured !== false;
@@ -37817,6 +37960,7 @@ var buildSummary = (record) => ({
37817
37960
  turns: record.summary.turnCount
37818
37961
  });
37819
37962
  var renderIncidentMarkdown = (input) => {
37963
+ const recoveryOutcomes = input.recoveryOutcomes;
37820
37964
  const lines = [
37821
37965
  `# ${input.title ?? `Voice Incident ${input.summary.sessionId}`}`,
37822
37966
  "",
@@ -37848,6 +37992,18 @@ var renderIncidentMarkdown = (input) => {
37848
37992
  "",
37849
37993
  ...input.record.tools.length ? input.record.tools.map((tool) => `- ${tool.toolName ?? "tool"} ${tool.status ?? ""} ${tool.elapsedMs === undefined ? "" : `${tool.elapsedMs}ms`} ${tool.error ?? ""}`.trim()) : ["- none"],
37850
37994
  "",
37995
+ "## Recovery Outcomes",
37996
+ "",
37997
+ ...recoveryOutcomes ? [
37998
+ `- Improved: ${recoveryOutcomes.improved}`,
37999
+ `- Unchanged: ${recoveryOutcomes.unchanged}`,
38000
+ `- Regressed: ${recoveryOutcomes.regressed}`,
38001
+ `- Failed: ${recoveryOutcomes.failed}`,
38002
+ `- Total actions: ${recoveryOutcomes.total}`,
38003
+ "",
38004
+ ...recoveryOutcomes.entries.length ? recoveryOutcomes.entries.map((entry) => `- ${entry.outcome}: ${entry.actionId} ${entry.beforeStatus ?? "unknown"} -> ${entry.afterStatus ?? "unknown"}${entry.detail ? ` - ${entry.detail}` : ""}`) : ["- no recovery actions recorded"]
38005
+ ] : ["- no recovery outcome report attached"],
38006
+ "",
37851
38007
  renderVoiceOperationsRecordGuardrailMarkdown(input.record),
37852
38008
  "",
37853
38009
  "## Trace Evidence",
@@ -37897,6 +38053,7 @@ var buildVoiceIncidentBundle = async (options) => {
37897
38053
  traceEvents: redactedTraceEvents
37898
38054
  });
37899
38055
  const summary = buildSummary(redactedRecord);
38056
+ const recoveryOutcomes = options.recoveryOutcomes ? redactRecordValue(options.recoveryOutcomes, redactedTraceEvents, options.redact) : undefined;
37900
38057
  const traceMarkdown = renderVoiceTraceMarkdown(record.traceEvents, {
37901
38058
  evaluation: options.evaluation,
37902
38059
  redact: options.redact,
@@ -37909,6 +38066,7 @@ var buildVoiceIncidentBundle = async (options) => {
37909
38066
  const markdown = renderIncidentMarkdown({
37910
38067
  auditMarkdown,
37911
38068
  record: redactedRecord,
38069
+ recoveryOutcomes,
37912
38070
  summary,
37913
38071
  title: options.title,
37914
38072
  traceMarkdown
@@ -37919,6 +38077,7 @@ var buildVoiceIncidentBundle = async (options) => {
37919
38077
  formatVersion: 1,
37920
38078
  markdown,
37921
38079
  record: redactedRecord,
38080
+ recoveryOutcomes,
37922
38081
  redacted: Boolean(options.redact),
37923
38082
  sessionId: options.sessionId,
37924
38083
  summary,
@@ -41402,6 +41561,7 @@ export {
41402
41561
  renderVoiceLatencySLOMarkdown,
41403
41562
  renderVoiceIncidentTimelineMarkdown,
41404
41563
  renderVoiceIncidentTimelineHTML,
41564
+ renderVoiceIncidentRecoveryOutcomeHTML,
41405
41565
  renderVoiceHandoffHealthHTML,
41406
41566
  renderVoiceGuardrailMarkdown,
41407
41567
  renderVoiceFailureReplayMarkdown,
@@ -41903,6 +42063,7 @@ export {
41903
42063
  buildVoiceLiveOpsControlState,
41904
42064
  buildVoiceLatencySLOGate,
41905
42065
  buildVoiceIncidentTimelineReport,
42066
+ buildVoiceIncidentRecoveryOutcomeReport,
41906
42067
  buildVoiceIncidentBundle,
41907
42068
  buildVoiceIOProviderRouterTraceEvent,
41908
42069
  buildVoiceGuardrailReport,
@@ -4,6 +4,8 @@ import { Database } from 'bun:sqlite';
4
4
  import { type VoiceAuditSinkDeliveryQueueSummary, type VoiceAuditSinkDeliveryRecord, type VoiceAuditSinkDeliveryStore } from './auditSinks';
5
5
  import type { VoiceAuditEventStore, VoiceAuditEventType } from './audit';
6
6
  import type { VoiceCallDebuggerReport } from './callDebugger';
7
+ import type { VoiceIncidentBundle } from './incidentBundle';
8
+ import type { VoiceIncidentRecoveryOutcomeReport } from './incidentTimeline';
7
9
  import { type VoiceOperationsRecord } from './operationsRecord';
8
10
  import type { VoiceSessionSnapshot } from './sessionSnapshot';
9
11
  import { type VoiceTraceSinkDeliveryQueueSummary } from './queue';
@@ -43,7 +45,7 @@ export type VoiceObservabilityExportRecordValidationOptions = {
43
45
  };
44
46
  export declare const validateVoiceObservabilityExportRecord: (input: unknown, options?: VoiceObservabilityExportRecordValidationOptions) => VoiceObservabilityExportValidationResult;
45
47
  export declare const assertVoiceObservabilityExportRecord: (input: unknown, options?: VoiceObservabilityExportRecordValidationOptions) => VoiceObservabilityExportValidationResult;
46
- export type VoiceObservabilityExportArtifactKind = 'call-debugger' | 'incident' | 'markdown' | 'operations-record' | 'proof-pack' | 'readiness' | 'screenshot' | 'session-snapshot' | 'slo' | 'trace' | 'audit' | 'custom';
48
+ export type VoiceObservabilityExportArtifactKind = 'call-debugger' | 'incident' | 'incident-recovery-outcomes' | 'markdown' | 'operations-record' | 'proof-pack' | 'readiness' | 'screenshot' | 'session-snapshot' | 'slo' | 'trace' | 'audit' | 'custom';
47
49
  export type VoiceObservabilityExportArtifactChecksum = {
48
50
  algorithm: 'sha256';
49
51
  value: string;
@@ -86,7 +88,7 @@ export type VoiceObservabilityExportEnvelope = {
86
88
  severity: VoiceObservabilityExportStatus;
87
89
  traceId?: string;
88
90
  };
89
- export type VoiceObservabilityExportIssueCode = 'voice.observability.no_evidence' | 'voice.observability.operation_failed' | 'voice.observability.artifact_missing' | 'voice.observability.artifact_stale' | 'voice.observability.audit_delivery_failed' | 'voice.observability.audit_delivery_pending' | 'voice.observability.trace_delivery_failed' | 'voice.observability.trace_delivery_pending';
91
+ export type VoiceObservabilityExportIssueCode = 'voice.observability.no_evidence' | 'voice.observability.operation_failed' | 'voice.observability.artifact_failed' | 'voice.observability.artifact_missing' | 'voice.observability.artifact_stale' | 'voice.observability.audit_delivery_failed' | 'voice.observability.audit_delivery_pending' | 'voice.observability.trace_delivery_failed' | 'voice.observability.trace_delivery_pending';
90
92
  export type VoiceObservabilityExportIssue = {
91
93
  code: VoiceObservabilityExportIssueCode;
92
94
  detail?: string;
@@ -389,6 +391,8 @@ export type VoiceObservabilityExportOptions = {
389
391
  sessionSnapshot?: (sessionId: string) => string;
390
392
  };
391
393
  callDebuggerReports?: VoiceCallDebuggerReport[] | (() => VoiceCallDebuggerReport[] | Promise<VoiceCallDebuggerReport[]>);
394
+ incidentBundles?: VoiceIncidentBundle[] | (() => VoiceIncidentBundle[] | Promise<VoiceIncidentBundle[]>);
395
+ incidentRecoveryOutcomeReports?: VoiceIncidentRecoveryOutcomeReport[] | (() => VoiceIncidentRecoveryOutcomeReport[] | Promise<VoiceIncidentRecoveryOutcomeReport[]>);
392
396
  operationsRecords?: VoiceOperationsRecord[];
393
397
  onTiming?: (timing: VoiceObservabilityExportTiming) => void;
394
398
  redact?: VoiceTraceRedactionConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.435",
3
+ "version": "0.0.22-beta.437",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",