(events)
| 59 | } |
| 60 | |
| 61 | function aggregate(events) { |
| 62 | const byType = new Map(); |
| 63 | for (const ev of events) { |
| 64 | if (!ev || ev.kind !== 'recall_verify') continue; |
| 65 | const type = (ev.asset && ev.asset.type) || 'Unknown'; |
| 66 | if (!byType.has(type)) { |
| 67 | byType.set(type, { |
| 68 | type, |
| 69 | total: 0, |
| 70 | ok: 0, |
| 71 | missing: 0, |
| 72 | mismatch: 0, |
| 73 | skipped: 0, |
| 74 | latencies: [], |
| 75 | ages: [], |
| 76 | }); |
| 77 | } |
| 78 | const bucket = byType.get(type); |
| 79 | bucket.total += 1; |
| 80 | const v = ev.verification || {}; |
| 81 | if (v.outcome === 'roundtrip_ok') bucket.ok += 1; |
| 82 | else if (v.outcome === 'roundtrip_missing') bucket.missing += 1; |
| 83 | else if (v.outcome === 'roundtrip_mismatch') bucket.mismatch += 1; |
| 84 | else bucket.skipped += 1; |
| 85 | if (Number.isFinite(v.latency_ms)) bucket.latencies.push(v.latency_ms); |
| 86 | if (Number.isFinite(v.age_at_verify_ms)) bucket.ages.push(v.age_at_verify_ms); |
| 87 | } |
| 88 | const rows = []; |
| 89 | for (const bucket of byType.values()) { |
| 90 | const denom = bucket.ok + bucket.missing + bucket.mismatch; |
| 91 | bucket.success_rate = denom > 0 ? bucket.ok / denom : 0; |
| 92 | bucket.latencies.sort(function (a, b) { return a - b; }); |
| 93 | bucket.ages.sort(function (a, b) { return a - b; }); |
| 94 | bucket.p50_latency_ms = percentile(bucket.latencies, 0.5); |
| 95 | bucket.p95_latency_ms = percentile(bucket.latencies, 0.95); |
| 96 | bucket.p99_latency_ms = percentile(bucket.latencies, 0.99); |
| 97 | bucket.p50_age_ms = percentile(bucket.ages, 0.5); |
| 98 | bucket.p95_age_ms = percentile(bucket.ages, 0.95); |
| 99 | bucket.p99_age_ms = percentile(bucket.ages, 0.99); |
| 100 | delete bucket.latencies; |
| 101 | delete bucket.ages; |
| 102 | rows.push(bucket); |
| 103 | } |
| 104 | rows.sort(function (a, b) { return a.type.localeCompare(b.type); }); |
| 105 | |
| 106 | const totals = { type: 'TOTAL', total: 0, ok: 0, missing: 0, mismatch: 0, skipped: 0 }; |
| 107 | for (const r of rows) { |
| 108 | totals.total += r.total; |
| 109 | totals.ok += r.ok; |
| 110 | totals.missing += r.missing; |
| 111 | totals.mismatch += r.mismatch; |
| 112 | totals.skipped += r.skipped; |
| 113 | } |
| 114 | const totalsDenom = totals.ok + totals.missing + totals.mismatch; |
| 115 | totals.success_rate = totalsDenom > 0 ? totals.ok / totalsDenom : 0; |
| 116 | |
| 117 | // Gate severity is monotonic: once a row triggers a worse state, later |
| 118 | // rows cannot downgrade it. Without this, AntiPattern@0% (RED) followed |
no test coverage detected