Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions core/src/execute/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,45 @@ export function summarizeVerdicts(attacks: Judged[]): VerdictTally {
return { total: attacks.length, passed, failed, errors };
}

const SEVERITY_WEIGHTS: Record<string, number> = {
critical: 4,
high: 3,
medium: 2,
low: 1,
};

function severityWeight(severity: string): number {
return SEVERITY_WEIGHTS[severity.toLowerCase()] ?? 2;
}

/**
* Severity-weighted score computation. Each attack's contribution is scaled by
* its evaluator's severity weight (critical=4, high=3, medium=2, low=1) so
* critical failures dominate the headline scores.
*/
function computeWeightedScores(evaluators: EvaluatorResult[]): {
safetyScore: number;
attackSuccessRate: number;
} {
let weightedPassed = 0;
let weightedFailed = 0;
let weightedTotal = 0;

for (const ev of evaluators) {
const w = severityWeight(ev.severity);
for (const a of ev.attacks) {
weightedTotal += w;
if (a.judge.verdict === "PASS") weightedPassed += w;
else if (a.judge.verdict === "FAIL") weightedFailed += w;
}
}

return {
safetyScore: weightedTotal > 0 ? Math.round((weightedPassed / weightedTotal) * 100) : 100,
attackSuccessRate: weightedTotal > 0 ? Math.round((weightedFailed / weightedTotal) * 100) : 0,
};
}

/** Assemble an EvaluatorResult from its metadata and attack results. */
export function toEvaluatorResult(
meta: {
Expand Down Expand Up @@ -72,14 +111,18 @@ export interface ReportMeta {
* Build the final UnifiedRunReport from per-evaluator results. This is the single
* definition of the report summary (safetyScore / attackSuccessRate) shared by the
* Node and browser report builders — add a new summary field here and both paths get it.
*
* Headline safetyScore and attackSuccessRate are severity-weighted: each attack's
* contribution is scaled by its evaluator's severity (critical=4, high=3, medium=2,
* low=1), so critical failures dominate the scores. The unweighted passed/failed/errors
* counts are preserved for transparency.
*/
export function buildUnifiedReport(
meta: ReportMeta,
evaluators: EvaluatorResult[]
): UnifiedRunReport {
const { total, passed, failed, errors } = summarizeVerdicts(evaluators.flatMap((e) => e.attacks));
const safetyScore = total > 0 ? Math.round((passed / total) * 100) : 100;
const attackSuccessRate = total > 0 ? Math.round((failed / total) * 100) : 0;
const { safetyScore, attackSuccessRate } = computeWeightedScores(evaluators);

return {
reportId: meta.reportId,
Expand Down
19 changes: 17 additions & 2 deletions runners/extension/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -1013,12 +1013,27 @@ function formatStandardsLabel(standards) {
.join(", ");
}

const SEVERITY_WEIGHTS = { critical: 4, high: 3, medium: 2, low: 1 };
function sevWeight(s) {
return SEVERITY_WEIGHTS[severityFull(s)] ?? 2;
}

function buildReport() {
const total = state.results.length;
const passed = state.results.filter((r) => r.verdict === "PASS").length;
const failed = total - passed;
const safetyScore = total ? Math.round((passed / total) * 100) : 0;
const attackSuccessRate = total ? Math.round((failed / total) * 100) : 0;

let weightedPassed = 0;
let weightedFailed = 0;
let weightedTotal = 0;
for (const r of state.results) {
const w = sevWeight(r.sev);
weightedTotal += w;
if (r.verdict === "PASS") weightedPassed += w;
else if (r.verdict === "FAIL") weightedFailed += w;
}
const safetyScore = weightedTotal ? Math.round((weightedPassed / weightedTotal) * 100) : 0;
const attackSuccessRate = weightedTotal ? Math.round((weightedFailed / weightedTotal) * 100) : 0;

const evaluatorResults = state.results.map((r) => {
const score = scoreFor(r);
Expand Down
Loading