#!/usr/bin/env node
import { b as getEnvBool, o as logger_default } from "./logger-BHK-IvoX.js";
import { g as DEFAULT_QUERY_LIMIT } from "./fetch-DosdWgS6.js";
import { t as invariant } from "./invariant-CzGQ4hYY.js";
import { o as getUserEmail } from "./accounts-Cd72TCBa.js";
import { t as getCurrentTimestamp } from "./time-oiqzx7WV.js";
import { H as riskCategorySeverityMap, at as PLUGIN_CATEGORIES, s as ResultFailureReason } from "./types-DEGWIbpc.js";
import { t as convertTestResultsToTableRow } from "./exportToFile-B1s42t-e.js";
import { t as hashPrompt } from "./utils-CmRYp5lJ.js";
import { o as randomSequence, s as sha256 } from "./extractor-BMhT-GPW.js";
import { a as createEmptyTokenUsage, r as accumulateTokenUsage } from "./tokenUsageUtils-cygnvkaF.js";
import { a as evalResultsTable, c as evalsToPromptsTable, d as promptsTable, g as getDb, i as datasetsTable, l as evalsToTagsTable, o as evalsTable, p as tagsTable, s as evalsToDatasetsTable } from "./tables-Bgd8sdal.js";
import { n as getTraceStore } from "./store-D_9cs2jy.js";
import { r as updateSignalFile } from "./signal-CBgKJYME.js";
import { t as EvalResult } from "./evalResult-BGsLZeKb.js";
import { and, desc, eq, sql } from "drizzle-orm";

//#region src/redteam/metrics.ts
/**
* The Attack Success Rate (ASR) is the number of tests which failed grading divided by the
* total number of tests.
* @param testCount - The total number of tests.
* @param failCount - The number of failed tests.
* @returns The attack success rate.
*/
function calculateAttackSuccessRate(testCount, failCount) {
	return testCount > 0 ? failCount / testCount * 100 : 0;
}

//#endregion
//#region src/redteam/sharedFrontend.ts
function getRiskCategorySeverityMap(plugins) {
	const overrides = plugins?.reduce((acc, plugin) => {
		if (plugin.severity) acc[plugin.id] = plugin.severity;
		return acc;
	}, {}) || {};
	return {
		...riskCategorySeverityMap,
		...overrides
	};
}

//#endregion
//#region src/util/calculateFilteredMetrics.ts
/**
* Calculate metrics for filtered evaluation results.
*
* This module implements optimized SQL aggregation to calculate metrics for
* filtered evaluation datasets. It uses a single GROUP BY query to aggregate
* ALL prompts at once, achieving significant performance improvements over
* the naive approach of querying each prompt separately.
*
* SECURITY: This module uses Drizzle's sql template strings for parameterized queries
* to prevent SQL injection. The whereSql parameter is a SQL fragment, not a string,
* ensuring all user-provided values are properly escaped.
*
* Performance targets:
* - Simple eval (2 prompts, 100 results): <50ms
* - Complex eval (10 prompts, 1000 results): <150ms
* - Large eval (10 prompts, 10000 results): <500ms
*
* Critical design decisions:
* 1. Single GROUP BY query for all basic metrics + token usage
* 2. SQL JSON aggregation for named scores (avoids memory issues)
* 3. SQL JSON aggregation for assertions (complex nested JSON)
* 4. OOM protection with MAX_RESULTS_FOR_METRICS limit
*/
/**
* Maximum number of results to process for metrics calculation.
* Protects against OOM on extremely large filtered datasets.
*/
const MAX_RESULTS_FOR_METRICS = 5e4;
/**
* Calculates metrics for filtered results using optimized SQL aggregation.
* Uses a SINGLE GROUP BY query to aggregate all prompts at once.
*
* SECURITY: Uses parameterized SQL queries via Drizzle's sql template strings.
* The whereSql parameter is a SQL fragment, not a raw string, ensuring all
* user-provided values are properly escaped.
*
* This is the core performance optimization - instead of making 2-3 queries
* per prompt (which would be 30 queries for 10 prompts), we make 3-4 total queries:
* 1. Count check (OOM protection)
* 2. Basic metrics + token usage (GROUP BY prompt_idx)
* 3. Named scores (GROUP BY prompt_idx, metric_name)
* 4. Assertions (GROUP BY prompt_idx)
*
* @param opts - Options including WHERE clause SQL fragment
* @returns Array of PromptMetrics, one per prompt
*/
async function calculateFilteredMetrics(opts) {
	const { numPrompts, whereSql } = opts;
	try {
		const countResult = await getResultCount(whereSql);
		if (countResult > MAX_RESULTS_FOR_METRICS) {
			logger_default.warn(`Filtered result count ${countResult} exceeds limit ${MAX_RESULTS_FOR_METRICS}`, { evalId: opts.evalId });
			throw new Error(`Result count ${countResult} exceeds maximum ${MAX_RESULTS_FOR_METRICS}`);
		}
		return await calculateWithOptimizedQuery(opts);
	} catch (error) {
		logger_default.error("Failed to calculate filtered metrics with optimized query", { error });
		return createEmptyMetricsArray(numPrompts);
	}
}
/**
* Get count of filtered results (for OOM protection)
*
* SECURITY: Uses parameterized SQL query via Drizzle's sql template strings.
*/
async function getResultCount(whereSql) {
	const db = getDb();
	const query = sql`
    SELECT COUNT(*) as count
    FROM eval_results
    WHERE ${whereSql}
  `;
	return (await db.get(query))?.count || 0;
}
/**
* OPTIMIZED: Single GROUP BY query aggregating ALL prompts at once.
* This is the key performance improvement from the audit.
*
* SECURITY: Uses parameterized SQL queries via Drizzle's sql template strings.
*/
async function calculateWithOptimizedQuery(opts) {
	const { numPrompts, whereSql } = opts;
	const db = getDb();
	const metrics = createEmptyMetricsArray(numPrompts);
	const basicMetricsQuery = sql`
    SELECT
      prompt_idx,
      COUNT(DISTINCT test_idx) as total_count,
      SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as pass_count,
      SUM(CASE WHEN success = 0 AND failure_reason != ${ResultFailureReason.ERROR} THEN 1 ELSE 0 END) as fail_count,
      SUM(CASE WHEN failure_reason = ${ResultFailureReason.ERROR} THEN 1 ELSE 0 END) as error_count,
      SUM(score) as total_score,
      SUM(latency_ms) as total_latency,
      SUM(cost) as total_cost,
      -- Token usage aggregation (token usage is inside response JSON)
      SUM(CAST(json_extract(response, '$.tokenUsage.total') AS INTEGER)) as total_tokens,
      SUM(CAST(json_extract(response, '$.tokenUsage.prompt') AS INTEGER)) as prompt_tokens,
      SUM(CAST(json_extract(response, '$.tokenUsage.completion') AS INTEGER)) as completion_tokens,
      SUM(CAST(json_extract(response, '$.tokenUsage.cached') AS INTEGER)) as cached_tokens,
      COUNT(CASE WHEN json_extract(response, '$.tokenUsage') IS NOT NULL THEN 1 END) as num_requests_with_tokens
    FROM eval_results
    WHERE ${whereSql}
    GROUP BY prompt_idx
    ORDER BY prompt_idx
  `;
	const basicResults = await db.all(basicMetricsQuery);
	for (const row of basicResults) {
		const idx = row.prompt_idx;
		if (idx < 0 || idx >= numPrompts) {
			logger_default.warn(`Invalid prompt_idx ${idx}, expected 0-${numPrompts - 1}`);
			continue;
		}
		metrics[idx] = {
			score: row.total_score || 0,
			testPassCount: row.pass_count || 0,
			testFailCount: row.fail_count || 0,
			testErrorCount: row.error_count || 0,
			totalLatencyMs: row.total_latency || 0,
			cost: row.total_cost || 0,
			tokenUsage: {
				total: row.total_tokens || 0,
				prompt: row.prompt_tokens || 0,
				completion: row.completion_tokens || 0,
				cached: row.cached_tokens || 0,
				numRequests: row.num_requests_with_tokens || 0
			},
			namedScores: {},
			namedScoresCount: {},
			assertPassCount: 0,
			assertFailCount: 0
		};
	}
	await aggregateNamedScores(metrics, whereSql);
	await aggregateAssertions(metrics, whereSql);
	logger_default.debug("Filtered metrics calculated", {
		numPrompts,
		metricsCount: basicResults.length
	});
	return metrics;
}
/**
* Aggregate named scores using SQL json_each().
* This is MUCH more efficient than fetching all results and parsing in JavaScript.
*
* SECURITY: Uses parameterized SQL query via Drizzle's sql template strings.
*
* Uses SQLite's json_each() to parse JSON in the database, avoiding the need
* to fetch potentially thousands of rows into memory.
*/
async function aggregateNamedScores(metrics, whereSql) {
	const db = getDb();
	const query = sql`
    SELECT
      prompt_idx,
      json_each.key as metric_name,
      SUM(CAST(json_each.value AS REAL)) as metric_sum,
      COUNT(*) as metric_count
    FROM eval_results,
      json_each(eval_results.named_scores)
    WHERE ${whereSql}
      AND named_scores IS NOT NULL
      AND json_valid(named_scores)
    GROUP BY prompt_idx, json_each.key
  `;
	const results = await db.all(query);
	for (const row of results) {
		const idx = row.prompt_idx;
		if (idx >= 0 && idx < metrics.length && metrics[idx]) {
			metrics[idx].namedScores[row.metric_name] = row.metric_sum;
			metrics[idx].namedScoresCount[row.metric_name] = row.metric_count;
		}
	}
}
/**
* Aggregate assertion counts using SQL json_each().
* This requires nested JSON extraction for componentResults.
*
* SECURITY: Uses parameterized SQL query via Drizzle's sql template strings.
*
* The grading_result structure is:
* {
*   "componentResults": [
*     {"pass": true, "assertion": {...}},
*     {"pass": false, "assertion": {...}}
*   ]
* }
*
* We need to count pass=true vs pass=false across all results.
*/
async function aggregateAssertions(metrics, whereSql) {
	const db = getDb();
	const query = sql`
    SELECT
      prompt_idx,
      SUM(
        CASE
          WHEN json_valid(grading_result) AND json_type(json_extract(grading_result, '$.componentResults')) = 'array' THEN
            (
              SELECT COUNT(*)
              FROM json_each(json_extract(grading_result, '$.componentResults'))
              WHERE CAST(json_extract(json_each.value, '$.pass') AS INTEGER) = 1
            )
          ELSE 0
        END
      ) as assert_pass_count,
      SUM(
        CASE
          WHEN json_valid(grading_result) AND json_type(json_extract(grading_result, '$.componentResults')) = 'array' THEN
            (
              SELECT COUNT(*)
              FROM json_each(json_extract(grading_result, '$.componentResults'))
              WHERE CAST(json_extract(json_each.value, '$.pass') AS INTEGER) = 0
            )
          ELSE 0
        END
      ) as assert_fail_count
    FROM eval_results
    WHERE ${whereSql}
      AND grading_result IS NOT NULL
    GROUP BY prompt_idx
  `;
	const results = await db.all(query);
	for (const row of results) {
		const idx = row.prompt_idx;
		if (idx >= 0 && idx < metrics.length && metrics[idx]) {
			metrics[idx].assertPassCount = row.assert_pass_count || 0;
			metrics[idx].assertFailCount = row.assert_fail_count || 0;
		}
	}
}
/**
* Create empty metrics array initialized with zeros.
* Used as fallback when calculation fails or no results found.
*/
function createEmptyMetricsArray(numPrompts) {
	return Array.from({ length: numPrompts }, () => ({
		score: 0,
		testPassCount: 0,
		testFailCount: 0,
		testErrorCount: 0,
		assertPassCount: 0,
		assertFailCount: 0,
		totalLatencyMs: 0,
		tokenUsage: {
			total: 0,
			prompt: 0,
			completion: 0,
			cached: 0,
			numRequests: 0
		},
		namedScores: {},
		namedScoresCount: {},
		cost: 0
	}));
}

//#endregion
//#region src/util/convertEvalResultsToTable.ts
/**
* Converts evaluation results from a ResultsFile into a table format for display.
* Processes test results, formats variables (including pretty-printing objects/arrays as JSON),
* handles redteam prompts, and structures data for console table and HTML output.
*
* @param eval_ - The results file containing evaluation data (requires version >= 4)
* @returns An EvaluateTable with formatted headers and body rows for display
*/
function convertResultsToTable(eval_) {
	invariant(eval_.prompts, `Prompts are required in this version of the results file, this needs to be results file version >= 4, version: ${eval_.version}`);
	const results = eval_.results;
	const varsForHeader = /* @__PURE__ */ new Set();
	const varValuesForRow = /* @__PURE__ */ new Map();
	const rowMap = {};
	for (const result of results.results) {
		for (const varName of Object.keys(result.vars || {})) varsForHeader.add(varName);
		const row = rowMap[result.testIdx] || {
			description: result.description || void 0,
			outputs: [],
			vars: result.vars ? Object.values(varsForHeader).map((varName) => {
				const varValue = result.vars?.[varName] || "";
				if (typeof varValue === "string") return varValue;
				return JSON.stringify(varValue, null, 2);
			}).flat() : [],
			test: result.testCase
		};
		if (result.vars && result.metadata?.redteamFinalPrompt) {
			const varKeys = Object.keys(result.vars);
			if (varKeys.length === 1 && varKeys[0] !== "harmCategory") result.vars[varKeys[0]] = result.metadata.redteamFinalPrompt;
			else if (varKeys.length > 1) {
				const keyToUpdate = [
					"prompt",
					"query",
					"question"
				].find((key) => result.vars[key]);
				if (keyToUpdate) result.vars[keyToUpdate] = result.metadata.redteamFinalPrompt;
			}
		}
		varValuesForRow.set(result.testIdx, result.vars);
		rowMap[result.testIdx] = row;
		let resultText;
		const outputTextDisplay = typeof result.response?.output === "object" ? JSON.stringify(result.response.output) : result.response?.output || result.error || "";
		if (result.testCase.assert) if (result.success) resultText = `${outputTextDisplay || result.error || ""}`;
		else resultText = `${outputTextDisplay}`;
		else if (result.error) resultText = `${result.error}`;
		else resultText = outputTextDisplay;
		row.outputs[result.promptIdx] = {
			id: result.id || `${result.testIdx}-${result.promptIdx}`,
			...result,
			text: resultText || "",
			prompt: result.prompt.raw,
			provider: result.provider?.label || result.provider?.id || "unknown provider",
			pass: result.success,
			failureReason: result.failureReason,
			cost: result.cost || 0,
			tokenUsage: result.tokenUsage,
			audio: result.response?.audio ? {
				id: result.response.audio.id,
				expiresAt: result.response.audio.expiresAt,
				data: result.response.audio.data,
				blobRef: result.response.audio.blobRef,
				transcript: result.response.audio.transcript,
				format: result.response.audio.format,
				sampleRate: result.response.audio.sampleRate,
				channels: result.response.audio.channels,
				duration: result.response.audio.duration
			} : void 0,
			video: result.response?.video ? {
				id: result.response.video.id,
				blobRef: result.response.video.blobRef,
				storageRef: result.response.video.storageRef,
				url: result.response.video.url,
				format: result.response.video.format,
				size: result.response.video.size,
				duration: result.response.video.duration,
				thumbnail: result.response.video.thumbnail,
				spritesheet: result.response.video.spritesheet,
				model: result.response.video.model,
				aspectRatio: result.response.video.aspectRatio,
				resolution: result.response.video.resolution
			} : void 0
		};
		invariant(result.promptId, "Prompt ID is required");
		row.testIdx = result.testIdx;
	}
	const rows = Object.values(rowMap);
	const sortedVars = [...varsForHeader].sort();
	for (const row of rows) row.vars = sortedVars.map((varName) => {
		const varValue = varValuesForRow.get(row.testIdx)?.[varName] || "";
		if (typeof varValue === "string") return varValue;
		return JSON.stringify(varValue, null, 2);
	});
	return {
		head: {
			prompts: eval_.prompts,
			vars: [...varsForHeader].sort()
		},
		body: rows
	};
}

//#endregion
//#region src/models/evalPerformance.ts
const distinctCountCache = /* @__PURE__ */ new Map();
const totalRowCountCache = /* @__PURE__ */ new Map();
const CACHE_TTL = 300 * 1e3;
/**
* Get the count of distinct test indices for an eval.
* This represents the number of unique test cases (rows in the UI table).
*
* Use getTotalResultRowCount() if you need the total number of result rows
* (which may be higher when there are multiple prompts/providers per test case).
*/
async function getCachedResultsCount(evalId) {
	const cacheKey = `distinct:${evalId}`;
	const cached = distinctCountCache.get(cacheKey);
	if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
		logger_default.debug(`Using cached distinct count for eval ${evalId}: ${cached.count}`);
		return cached.count;
	}
	const db = getDb();
	const start = Date.now();
	const result = db.select({ count: sql`COUNT(DISTINCT test_idx)` }).from(evalResultsTable).where(sql`eval_id = ${evalId}`).all();
	const count$1 = Number(result[0]?.count ?? 0);
	const duration = Date.now() - start;
	logger_default.debug(`Distinct count query for eval ${evalId}: ${count$1} in ${duration}ms`);
	distinctCountCache.set(cacheKey, {
		count: count$1,
		timestamp: Date.now()
	});
	return count$1;
}
/**
* Get the total count of all result rows for an eval.
* This counts every result row in the database, including multiple results
* per test case (e.g., when using multiple prompts or providers).
*
* Use this for progress tracking when iterating over all results (e.g., sharing).
*/
async function getTotalResultRowCount(evalId) {
	const cacheKey = `total:${evalId}`;
	const cached = totalRowCountCache.get(cacheKey);
	if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
		logger_default.debug(`Using cached total row count for eval ${evalId}: ${cached.count}`);
		return cached.count;
	}
	const db = getDb();
	const start = Date.now();
	const result = db.select({ count: sql`COUNT(*)` }).from(evalResultsTable).where(sql`eval_id = ${evalId}`).all();
	const count$1 = Number(result[0]?.count ?? 0);
	const duration = Date.now() - start;
	logger_default.debug(`Total row count query for eval ${evalId}: ${count$1} in ${duration}ms`);
	totalRowCountCache.set(cacheKey, {
		count: count$1,
		timestamp: Date.now()
	});
	return count$1;
}
async function queryTestIndicesOptimized(evalId, opts) {
	const db = getDb();
	const offset = opts.offset ?? 0;
	const limit = opts.limit ?? 50;
	const mode = opts.filterMode ?? "all";
	let baseQuery = sql`eval_id = ${evalId}`;
	if (mode === "errors") baseQuery = sql`${baseQuery} AND failure_reason = ${2}`;
	else if (mode === "failures") baseQuery = sql`${baseQuery} AND success = 0 AND failure_reason != ${2}`;
	else if (mode === "passes") baseQuery = sql`${baseQuery} AND success = 1`;
	else if (mode === "highlights") baseQuery = sql`${baseQuery} AND json_extract(grading_result, '$.comment') LIKE '!highlight%'`;
	let searchCondition = sql`1=1`;
	if (opts.searchQuery && opts.searchQuery.trim() !== "" && !opts.filters?.length) searchCondition = sql`response LIKE ${"%" + opts.searchQuery.replace(/'/g, "''") + "%"}`;
	const whereClause = sql`${baseQuery} AND ${searchCondition}`;
	const countStart = Date.now();
	const countQuery = sql`
    SELECT COUNT(DISTINCT test_idx) as count 
    FROM ${evalResultsTable} 
    WHERE ${whereClause}
  `;
	const countResult = db.all(countQuery);
	const filteredCount = Number(countResult[0]?.count ?? 0);
	logger_default.debug(`Optimized count query took ${Date.now() - countStart}ms`);
	const idxStart = Date.now();
	const idxQuery = sql`
    SELECT DISTINCT test_idx 
    FROM ${evalResultsTable} 
    WHERE ${whereClause}
    ORDER BY test_idx 
    LIMIT ${limit} 
    OFFSET ${offset}
  `;
	const testIndices = db.all(idxQuery).map((row) => row.test_idx);
	logger_default.debug(`Optimized index query took ${Date.now() - idxStart}ms`);
	return {
		testIndices,
		filteredCount
	};
}

//#endregion
//#region src/models/eval.ts
/**
* Sanitizes runtime options to ensure only JSON-serializable data is persisted.
* Removes non-serializable fields like AbortSignal, functions, and symbols.
*/
function sanitizeRuntimeOptions(options) {
	if (!options) return;
	const sanitized = { ...options };
	delete sanitized.abortSignal;
	for (const key in sanitized) {
		const value = sanitized[key];
		if (typeof value === "function" || typeof value === "symbol") delete sanitized[key];
	}
	return sanitized;
}
function createEvalId(createdAt = /* @__PURE__ */ new Date()) {
	return `eval-${randomSequence(3)}-${createdAt.toISOString().slice(0, 19)}`;
}
/**
* Escapes a key for use in a JSON path expression.
* Handles backslashes and double quotes which have special meaning in JSON paths.
*/
function escapeJsonPathKey(key) {
	return key.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
}
/**
* Builds a safe JSON path for use in SQLite json_extract() queries.
*
* SECURITY NOTE: This function uses sql.raw() which is normally unsafe, but is REQUIRED here
* because SQLite's json_extract() function only accepts JSON paths as string literals,
* not as parameterized values.
*
* Safety is ensured through double escaping:
* 1. JSON path characters are escaped (backslashes and double quotes)
* 2. SQL single quotes are escaped using standard SQL escaping ('' for ')
*
* @param field - The JSON field name from user input
* @returns A sql.raw() fragment containing the safely escaped JSON path
*/
function buildSafeJsonPath(field) {
	const sqlSafeJsonPath = `$."${escapeJsonPathKey(field)}"`.replace(/'/g, "''");
	return sql.raw(`'${sqlSafeJsonPath}'`);
}
/**
* Combines multiple filter conditions using their associated logic operators (AND/OR).
*
* @param filterConditions - Array of conditions with their logic operators
* @returns A single SQL fragment combining all conditions, or null if empty
*/
function combineFilterConditions(filterConditions) {
	if (filterConditions.length === 0) return null;
	if (filterConditions.length === 1) return filterConditions[0].condition;
	return filterConditions.reduce((acc, { condition: cond, logicOperator }, idx) => {
		if (idx === 0) return cond;
		return logicOperator === "OR" ? sql`${acc} OR ${cond}` : sql`${acc} AND ${cond}`;
	}, filterConditions[0].condition);
}
var EvalQueries = class {
	static async getVarsFromEvals(evals) {
		const db = getDb();
		if (evals.length === 0) return {};
		const evalIds = evals.map((e) => e.id);
		const query = sql`
      SELECT DISTINCT j.key, eval_id
      FROM (
        SELECT eval_id, json_extract(eval_results.test_case, '$.vars') as vars
        FROM eval_results
        WHERE eval_id IN (${sql.join(evalIds, sql`, `)})
      ) t, json_each(t.vars) j
    `;
		return (await db.all(query)).reduce((acc, r) => {
			acc[r.eval_id] = acc[r.eval_id] || [];
			acc[r.eval_id].push(r.key);
			return acc;
		}, {});
	}
	static async getVarsFromEval(evalId) {
		const db = getDb();
		const query = sql`
      SELECT DISTINCT j.key
      FROM (
        SELECT json_extract(eval_results.test_case, '$.vars') as vars
        FROM eval_results
        WHERE eval_results.eval_id = ${evalId}
      ) t, json_each(t.vars) j
    `;
		return (await db.all(query)).map((r) => r.key);
	}
	static async setVars(evalId, vars) {
		const db = getDb();
		try {
			db.update(evalsTable).set({ vars }).where(eq(evalsTable.id, evalId)).run();
		} catch (e) {
			logger_default.error(`Error setting vars: ${vars} for eval ${evalId}: ${e}`);
		}
	}
	static async getMetadataKeysFromEval(evalId, comparisonEvalIds = []) {
		const db = getDb();
		try {
			const allEvalIds = [evalId, ...comparisonEvalIds];
			const query = sql`
        SELECT DISTINCT j.key FROM (
          SELECT metadata FROM eval_results
          WHERE eval_id IN (${sql.join(allEvalIds, sql`, `)})
            AND metadata IS NOT NULL
            AND metadata != '{}'
            AND json_valid(metadata)
          LIMIT 10000
        ) t, json_each(t.metadata) j
        ORDER BY j.key
        LIMIT 1000
      `;
			return (await db.all(query)).map((r) => r.key);
		} catch (error) {
			logger_default.error(`Error fetching metadata keys for eval ${evalId} and comparisons [${comparisonEvalIds.join(", ")}]: ${error}`);
			return [];
		}
	}
	/**
	* Queries all unique metadata values for a given metadata key.
	* @param evalId - The ID of the eval to get the metadata values from.
	* @param key - The key of the metadata to get the values from.
	* @returns An array of unique metadata values.
	*/
	static getMetadataValuesFromEval(evalId, key) {
		const db = getDb();
		const trimmedKey = key.trim();
		if (!trimmedKey) return [];
		try {
			const jsonPath = `$."${escapeJsonPathKey(trimmedKey)}"`;
			const query = sql`
        SELECT DISTINCT
          json_extract(${evalResultsTable.metadata}, ${jsonPath}) AS value
        FROM ${evalResultsTable}
        WHERE ${evalResultsTable.evalId} = ${evalId}
          AND ${evalResultsTable.metadata} IS NOT NULL
          AND ${evalResultsTable.metadata} != '{}'
          AND json_valid(${evalResultsTable.metadata})
          AND json_extract(${evalResultsTable.metadata}, ${jsonPath}) IS NOT NULL
        ORDER BY value
        LIMIT 1000
      `;
			const values = db.all(query).map(({ value }) => String(value).trim()).filter((value) => value.length > 0);
			return Array.from(new Set(values));
		} catch (error) {
			logger_default.error(`Error fetching metadata values for eval ${evalId} and key ${trimmedKey}: ${error instanceof Error ? error.message : String(error)}`);
			return [];
		}
	}
};
var Eval = class Eval {
	id;
	createdAt;
	author;
	description;
	config;
	results;
	datasetId;
	prompts;
	oldResults;
	persisted;
	vars;
	_resultsLoaded = false;
	runtimeOptions;
	_shared = false;
	durationMs;
	/**
	* The shareable URL for this evaluation, if it has been shared.
	* Set by the evaluate() function when sharing is enabled.
	*/
	shareableUrl;
	static async latest() {
		const db_results = await getDb().select({ id: evalsTable.id }).from(evalsTable).orderBy(desc(evalsTable.createdAt)).limit(1);
		if (db_results.length === 0) return;
		return await Eval.findById(db_results[0].id);
	}
	static async findById(id) {
		const db = getDb();
		const evalData = db.select().from(evalsTable).where(eq(evalsTable.id, id)).all();
		if (evalData.length === 0) return;
		const datasetResults = db.select({ datasetId: evalsToDatasetsTable.datasetId }).from(evalsToDatasetsTable).where(eq(evalsToDatasetsTable.evalId, id)).limit(1).all();
		const eval_ = evalData[0];
		const datasetId = datasetResults[0]?.datasetId;
		const resultsObj = eval_.results;
		const rawDurationMs = resultsObj && "durationMs" in resultsObj ? resultsObj.durationMs : void 0;
		const durationMs = typeof rawDurationMs === "number" && Number.isFinite(rawDurationMs) && rawDurationMs >= 0 ? rawDurationMs : void 0;
		const evalInstance = new Eval(eval_.config, {
			id: eval_.id,
			createdAt: new Date(eval_.createdAt),
			author: eval_.author || void 0,
			description: eval_.description || void 0,
			prompts: eval_.prompts || [],
			datasetId,
			persisted: true,
			vars: eval_.vars || [],
			runtimeOptions: eval_.runtimeOptions ?? void 0,
			durationMs
		});
		if (eval_.results && "table" in eval_.results) evalInstance.oldResults = eval_.results;
		if (!eval_.vars || eval_.vars.length === 0) {
			const vars = await EvalQueries.getVarsFromEval(id);
			evalInstance.setVars(vars);
			await EvalQueries.setVars(id, vars);
		}
		return evalInstance;
	}
	static async getMany(limit = DEFAULT_QUERY_LIMIT) {
		return (await getDb().select().from(evalsTable).limit(limit).orderBy(desc(evalsTable.createdAt)).all()).map((e) => new Eval(e.config, {
			id: e.id,
			createdAt: new Date(e.createdAt),
			author: e.author || void 0,
			description: e.description || void 0,
			prompts: e.prompts || [],
			persisted: true
		}));
	}
	/**
	* Get paginated evals with offset support for efficient infinite scroll.
	* @param offset - Number of evals to skip
	* @param limit - Maximum number of evals to return
	*/
	static async getPaginated(offset = 0, limit = DEFAULT_QUERY_LIMIT) {
		return (await getDb().select().from(evalsTable).orderBy(desc(evalsTable.createdAt)).limit(limit).offset(offset).all()).map((e) => new Eval(e.config, {
			id: e.id,
			createdAt: new Date(e.createdAt),
			author: e.author || void 0,
			description: e.description || void 0,
			prompts: e.prompts || [],
			persisted: true
		}));
	}
	/**
	* Get total count of evals for pagination.
	*/
	static async getCount() {
		return (await getDb().select({ count: sql`count(*)` }).from(evalsTable).get())?.count ?? 0;
	}
	static async create(config, renderedPrompts, opts) {
		const createdAt = opts?.createdAt || /* @__PURE__ */ new Date();
		const evalId = opts?.id || createEvalId(createdAt);
		const author = opts?.author || getUserEmail();
		const db = getDb();
		const datasetId = sha256(JSON.stringify(config.tests || []));
		db.transaction(() => {
			db.insert(evalsTable).values({
				id: evalId,
				createdAt: createdAt.getTime(),
				author,
				description: config.description,
				config,
				results: {},
				vars: opts?.vars || [],
				runtimeOptions: sanitizeRuntimeOptions(opts?.runtimeOptions),
				prompts: opts?.completedPrompts || []
			}).run();
			for (const prompt of renderedPrompts) {
				const label = prompt.label || prompt.display || prompt.raw;
				const promptId = hashPrompt(prompt);
				db.insert(promptsTable).values({
					id: promptId,
					prompt: label
				}).onConflictDoNothing().run();
				db.insert(evalsToPromptsTable).values({
					evalId,
					promptId
				}).onConflictDoNothing().run();
				logger_default.debug(`Inserting prompt ${promptId}`);
			}
			if (opts?.results && opts.results.length > 0) {
				const res = db.insert(evalResultsTable).values(opts.results?.map((r) => ({
					...r,
					evalId,
					id: crypto.randomUUID()
				}))).run();
				logger_default.debug(`Inserted ${res.changes} eval results`);
			}
			db.insert(datasetsTable).values({
				id: datasetId,
				tests: config.tests
			}).onConflictDoNothing().run();
			db.insert(evalsToDatasetsTable).values({
				evalId,
				datasetId
			}).onConflictDoNothing().run();
			logger_default.debug(`Inserting dataset ${datasetId}`);
			if (config.tags) for (const [tagKey, tagValue] of Object.entries(config.tags)) {
				const tagId = sha256(`${tagKey}:${tagValue}`);
				db.insert(tagsTable).values({
					id: tagId,
					name: tagKey,
					value: tagValue
				}).onConflictDoNothing().run();
				db.insert(evalsToTagsTable).values({
					evalId,
					tagId
				}).onConflictDoNothing().run();
				logger_default.debug(`Inserting tag ${tagId}`);
			}
		});
		return new Eval(config, {
			id: evalId,
			author: opts?.author,
			createdAt,
			persisted: true,
			runtimeOptions: sanitizeRuntimeOptions(opts?.runtimeOptions)
		});
	}
	constructor(config, opts) {
		const createdAt = opts?.createdAt || /* @__PURE__ */ new Date();
		this.createdAt = createdAt.getTime();
		this.id = opts?.id || createEvalId(createdAt);
		this.author = opts?.author;
		this.config = config;
		this.results = [];
		this.prompts = opts?.prompts || [];
		this.datasetId = opts?.datasetId;
		this.persisted = opts?.persisted || false;
		this._resultsLoaded = false;
		this.vars = opts?.vars || [];
		this.runtimeOptions = opts?.runtimeOptions;
		this.durationMs = opts?.durationMs;
	}
	version() {
		/**
		* Version 3 is the denormalized version of where the table and results are stored on the eval object.
		* Version 4 is the normalized version where the results are stored in another databse table and the table for vizualization is generated by the app.
		*/
		return this.oldResults && "table" in this.oldResults ? 3 : 4;
	}
	useOldResults() {
		return this.version() < 4;
	}
	setTable(table) {
		invariant(this.version() < 4, "Eval is not version 3");
		invariant(this.oldResults, "Old results not found");
		this.oldResults.table = table;
	}
	async save() {
		const db = getDb();
		const updateObj = {
			config: this.config,
			prompts: this.prompts,
			description: this.config.description,
			author: this.author,
			updatedAt: getCurrentTimestamp(),
			vars: Array.from(this.vars),
			runtimeOptions: sanitizeRuntimeOptions(this.runtimeOptions)
		};
		if (this.useOldResults()) {
			invariant(this.oldResults, "Old results not found");
			updateObj.results = this.oldResults;
		} else if (this.durationMs !== void 0) updateObj.results = { durationMs: this.durationMs };
		db.update(evalsTable).set(updateObj).where(eq(evalsTable.id, this.id)).run();
		this.persisted = true;
	}
	setVars(vars) {
		this.vars = vars;
	}
	addVar(varName) {
		this.vars.push(varName);
	}
	setDurationMs(durationMs) {
		this.durationMs = durationMs;
	}
	getPrompts() {
		if (this.useOldResults()) {
			invariant(this.oldResults, "Old results not found");
			return this.oldResults.table?.head.prompts || [];
		}
		return this.prompts;
	}
	async getTable() {
		if (this.useOldResults()) return this.oldResults?.table || {
			head: {
				prompts: [],
				vars: []
			},
			body: []
		};
		return convertResultsToTable(await this.toResultsFile());
	}
	async addResult(result) {
		const newResult = await EvalResult.createFromEvaluateResult(this.id, result, { persist: this.persisted });
		if (!this.persisted) this.results.push(newResult);
		if (this.persisted) updateSignalFile(this.id);
	}
	async *fetchResultsBatched(batchSize = 100) {
		for await (const batch of EvalResult.findManyByEvalIdBatched(this.id, { batchSize })) yield batch;
	}
	async getResultsCount() {
		return getCachedResultsCount(this.id);
	}
	/**
	* Get the total count of all result rows for this eval.
	* Use this when iterating over all results (e.g., for sharing progress).
	* This may be higher than getResultsCount() when there are multiple prompts/providers.
	*/
	async getTotalResultRowCount() {
		return getTotalResultRowCount(this.id);
	}
	async fetchResultsByTestIdx(testIdx) {
		return await EvalResult.findManyByEvalId(this.id, { testIdx });
	}
	/**
	* CRITICAL: Builds the WHERE SQL clause for filtering results.
	* This is the single source of truth for all filtering logic.
	* Used by both queryTestIndices() (pagination) and getFilteredMetrics().
	*
	* SECURITY: This method uses Drizzle's sql template strings for parameterized queries
	* to prevent SQL injection. All user-provided values are passed as parameters,
	* not interpolated into the SQL string.
	*
	* Any changes to filter logic MUST be made here to ensure consistency
	* between displayed rows and calculated metrics.
	*
	* @returns SQL fragment (without "WHERE" keyword) that can be used in queries
	*/
	buildFilterWhereSql(opts) {
		const mode = opts.filterMode ?? "all";
		const conditions = [sql`eval_id = ${this.id}`];
		if (mode === "errors") conditions.push(sql`failure_reason = ${ResultFailureReason.ERROR}`);
		else if (mode === "failures") conditions.push(sql`success = 0 AND failure_reason != ${ResultFailureReason.ERROR}`);
		else if (mode === "passes") conditions.push(sql`success = 1`);
		else if (mode === "highlights") conditions.push(sql`json_extract(grading_result, '$.comment') LIKE ${"!highlight%"}`);
		if (opts.filters && opts.filters.length > 0) {
			const filterConditions = [];
			opts.filters.forEach((filter) => {
				const { logicOperator, type, operator, value, field } = JSON.parse(filter);
				let condition = null;
				if (type === "metric") {
					const metricKey = field || value;
					if (!metricKey) {
						logger_default.warn("Invalid metric filter: missing field and value", { filter });
						return;
					}
					const jsonPath = buildSafeJsonPath(metricKey);
					const numericValue = typeof value === "number" ? value : Number.parseFloat(value);
					if (operator === "is_defined" || operator === "equals" && !field) condition = sql`json_extract(named_scores, ${jsonPath}) IS NOT NULL`;
					else if (Number.isFinite(numericValue)) {
						if (operator === "eq") condition = sql`CAST(json_extract(named_scores, ${jsonPath}) AS REAL) = ${numericValue}`;
						else if (operator === "neq") condition = sql`(json_extract(named_scores, ${jsonPath}) IS NOT NULL AND CAST(json_extract(named_scores, ${jsonPath}) AS REAL) != ${numericValue})`;
						else if (operator === "gt") condition = sql`CAST(json_extract(named_scores, ${jsonPath}) AS REAL) > ${numericValue}`;
						else if (operator === "gte") condition = sql`CAST(json_extract(named_scores, ${jsonPath}) AS REAL) >= ${numericValue}`;
						else if (operator === "lt") condition = sql`CAST(json_extract(named_scores, ${jsonPath}) AS REAL) < ${numericValue}`;
						else if (operator === "lte") condition = sql`CAST(json_extract(named_scores, ${jsonPath}) AS REAL) <= ${numericValue}`;
					} else {
						logger_default.warn("Invalid numeric value in metric filter", {
							metricKey,
							value,
							numericValue,
							operator
						});
						return;
					}
				} else if (type === "metadata" && field) {
					const jsonPath = buildSafeJsonPath(field);
					if (operator === "equals") condition = sql`json_extract(metadata, ${jsonPath}) = ${value}`;
					else if (operator === "contains") condition = sql`json_extract(metadata, ${jsonPath}) LIKE ${`%${value}%`}`;
					else if (operator === "not_contains") condition = sql`(json_extract(metadata, ${jsonPath}) IS NULL OR json_extract(metadata, ${jsonPath}) NOT LIKE ${`%${value}%`})`;
					else if (operator === "exists") condition = sql`LENGTH(TRIM(COALESCE(json_extract(metadata, ${jsonPath}), ''))) > 0`;
				} else if (type === "plugin") {
					const isCategory = Object.keys(PLUGIN_CATEGORIES).includes(value);
					if (operator === "equals") if (isCategory) condition = sql`json_extract(metadata, '$.pluginId') LIKE ${`${value}:%`}`;
					else condition = sql`json_extract(metadata, '$.pluginId') = ${value}`;
					else if (operator === "not_equals") if (isCategory) condition = sql`(json_extract(metadata, '$.pluginId') IS NULL OR (json_extract(metadata, '$.pluginId') != ${value} AND json_extract(metadata, '$.pluginId') NOT LIKE ${`${value}:%`}))`;
					else condition = sql`(json_extract(metadata, '$.pluginId') IS NULL OR json_extract(metadata, '$.pluginId') != ${value})`;
				} else if (type === "strategy" && operator === "equals") if (value === "basic") condition = sql`(json_extract(metadata, '$.strategyId') IS NULL OR json_extract(metadata, '$.strategyId') = '')`;
				else condition = sql`json_extract(metadata, '$.strategyId') = ${value}`;
				else if (type === "severity" && operator === "equals") {
					const explicit = sql`json_extract(metadata, '$.severity') = ${value}`;
					const severityMap = getRiskCategorySeverityMap(this.config?.redteam?.plugins);
					const pluginConditions = Object.entries(severityMap).filter(([, severity]) => severity === value).map(([pluginId]) => pluginId).map((pluginId) => {
						return pluginId.includes(":") ? sql`json_extract(metadata, '$.pluginId') = ${pluginId}` : sql`json_extract(metadata, '$.pluginId') LIKE ${`${pluginId}:%`}`;
					});
					if (pluginConditions.length > 0) condition = sql`(${explicit} OR ((${sql.join(pluginConditions, sql` OR `)}) AND ${sql`(json_extract(metadata, '$.severity') IS NULL OR json_extract(metadata, '$.severity') = ${value})`}))`;
					else condition = sql`(${explicit})`;
				} else if (type === "policy" && operator === "equals") condition = sql`(named_scores LIKE '%PolicyViolation:%' AND named_scores LIKE ${`%${value}%`})`;
				if (condition) filterConditions.push({
					condition,
					logicOperator: logicOperator || "AND"
				});
			});
			const filterClause = combineFilterConditions(filterConditions);
			if (filterClause) conditions.push(sql`(${filterClause})`);
		}
		if (opts.searchQuery && opts.searchQuery.trim() !== "") {
			const searchPattern = `%${opts.searchQuery}%`;
			const searchConditions = [
				sql`response LIKE ${searchPattern}`,
				sql`json_extract(grading_result, '$.reason') LIKE ${searchPattern}`,
				sql`json_extract(grading_result, '$.comment') LIKE ${searchPattern}`,
				sql`json_extract(named_scores, '$') LIKE ${searchPattern}`,
				sql`json_extract(metadata, '$') LIKE ${searchPattern}`,
				sql`json_extract(test_case, '$.vars') LIKE ${searchPattern}`,
				sql`json_extract(test_case, '$.metadata') LIKE ${searchPattern}`
			];
			const searchClause = sql.join(searchConditions, sql` OR `);
			conditions.push(sql`(${searchClause})`);
		}
		return sql.join(conditions, sql` AND `);
	}
	/**
	* Private helper method to build filter conditions and query for test indices.
	*
	* SECURITY: Uses parameterized queries via Drizzle's sql template strings
	* to prevent SQL injection attacks.
	*/
	async queryTestIndices(opts) {
		const db = getDb();
		const offset = opts.offset ?? 0;
		const limit = opts.limit ?? 50;
		const whereSql = this.buildFilterWhereSql({
			filterMode: opts.filterMode,
			searchQuery: opts.searchQuery,
			filters: opts.filters
		});
		const filteredCountQuery = sql`
      SELECT COUNT(DISTINCT test_idx) as count
      FROM eval_results
      WHERE ${whereSql}
    `;
		const countStart = Date.now();
		const countResult = await db.get(filteredCountQuery);
		const countEnd = Date.now();
		logger_default.debug(`Count query took ${countEnd - countStart}ms`);
		const filteredCount = countResult?.count || 0;
		const idxQuery = sql`
      SELECT DISTINCT test_idx
      FROM eval_results
      WHERE ${whereSql}
      ORDER BY test_idx
      LIMIT ${limit}
      OFFSET ${offset}
    `;
		const idxStart = Date.now();
		const rows = await db.all(idxQuery);
		const idxEnd = Date.now();
		logger_default.debug(`Index query took ${idxEnd - idxStart}ms`);
		return {
			testIndices: rows.map((row) => row.test_idx),
			filteredCount
		};
	}
	/**
	* CRITICAL: Calculates metrics for filtered results.
	* Uses the SAME WHERE clause as queryTestIndices() to ensure consistency.
	*
	* SECURITY: Uses parameterized SQL queries to prevent SQL injection.
	*
	* This method is called from the API route when filters are active to provide
	* metrics that accurately reflect the filtered dataset.
	*
	* @returns Array of PromptMetrics, one per prompt
	*/
	async getFilteredMetrics(opts) {
		const whereSql = this.buildFilterWhereSql(opts);
		return calculateFilteredMetrics({
			evalId: this.id,
			numPrompts: this.prompts.length,
			whereSql
		});
	}
	async getTablePage(opts) {
		const totalCount = await this.getResultsCount();
		let testIndices;
		let filteredCount;
		if (opts.testIndices && opts.testIndices.length > 0) {
			testIndices = opts.testIndices;
			filteredCount = testIndices.length;
		} else {
			const hasComplexFilters = opts.filters && opts.filters.length > 0;
			let queryResult;
			if (hasComplexFilters) {
				logger_default.debug("Using original query for complex filters");
				queryResult = await this.queryTestIndices({
					offset: opts.offset,
					limit: opts.limit,
					filterMode: opts.filterMode,
					searchQuery: opts.searchQuery,
					filters: opts.filters
				});
			} else {
				logger_default.debug("Using optimized query for table page");
				queryResult = await queryTestIndicesOptimized(this.id, {
					offset: opts.offset,
					limit: opts.limit,
					filterMode: opts.filterMode,
					searchQuery: opts.searchQuery,
					filters: opts.filters
				});
			}
			testIndices = queryResult.testIndices;
			filteredCount = queryResult.filteredCount;
		}
		const varsStart = Date.now();
		const vars = Array.from(this.vars);
		const varsEnd = Date.now();
		logger_default.debug(`Vars query took ${varsEnd - varsStart}ms`);
		const body = [];
		const bodyStart = Date.now();
		if (testIndices.length === 0) {
			const bodyEnd$1 = Date.now();
			logger_default.debug(`Body query took ${bodyEnd$1 - bodyStart}ms`);
			return {
				head: {
					prompts: this.prompts,
					vars
				},
				body,
				totalCount,
				filteredCount,
				id: this.id
			};
		}
		const allResults = await EvalResult.findManyByEvalIdAndTestIndices(this.id, testIndices);
		const resultsByTestIdx = /* @__PURE__ */ new Map();
		for (const result of allResults) {
			if (!resultsByTestIdx.has(result.testIdx)) resultsByTestIdx.set(result.testIdx, []);
			resultsByTestIdx.get(result.testIdx).push(result);
		}
		for (const testIdx of testIndices) {
			const results = resultsByTestIdx.get(testIdx) || [];
			if (results.length > 0) body.push(convertTestResultsToTableRow(results, vars));
		}
		const bodyEnd = Date.now();
		logger_default.debug(`Body query took ${bodyEnd - bodyStart}ms`);
		return {
			head: {
				prompts: this.prompts,
				vars
			},
			body,
			totalCount,
			filteredCount,
			id: this.id
		};
	}
	async addPrompts(prompts) {
		this.prompts = prompts;
		if (this.persisted) getDb().update(evalsTable).set({ prompts }).where(eq(evalsTable.id, this.id)).run();
	}
	async setResults(results) {
		this.results = results;
		if (this.persisted) await getDb().insert(evalResultsTable).values(results.map((r) => ({
			...r,
			evalId: this.id
		})));
		this._resultsLoaded = true;
	}
	async loadResults() {
		this.results = await EvalResult.findManyByEvalId(this.id);
		this._resultsLoaded = true;
	}
	async getResults() {
		if (this.useOldResults()) {
			invariant(this.oldResults, "Old results not found");
			return this.oldResults.results;
		}
		await this.loadResults();
		this._resultsLoaded = true;
		return this.results;
	}
	clearResults() {
		this.results = [];
		this._resultsLoaded = false;
	}
	getStats() {
		const stats = {
			successes: 0,
			failures: 0,
			errors: 0,
			tokenUsage: createEmptyTokenUsage(),
			durationMs: this.durationMs
		};
		for (const prompt of this.prompts) {
			stats.successes += prompt.metrics?.testPassCount ?? 0;
			stats.failures += prompt.metrics?.testFailCount ?? 0;
			stats.errors += prompt.metrics?.testErrorCount ?? 0;
			accumulateTokenUsage(stats.tokenUsage, prompt.metrics?.tokenUsage);
		}
		return stats;
	}
	async toEvaluateSummary() {
		if (this.useOldResults()) {
			invariant(this.oldResults, "Old results not found");
			return {
				version: 2,
				timestamp: new Date(this.createdAt).toISOString(),
				results: this.oldResults.results,
				table: this.oldResults.table,
				stats: this.oldResults.stats
			};
		}
		if (this.results.length === 0) await this.loadResults();
		const stats = await this.getStats();
		const prompts = getEnvBool("PROMPTFOO_STRIP_PROMPT_TEXT", false) ? this.prompts.map((p) => ({
			...p,
			raw: "[prompt stripped]"
		})) : this.prompts;
		return {
			version: 3,
			timestamp: new Date(this.createdAt).toISOString(),
			prompts,
			results: this.results.map((r) => r.toEvaluateResult()),
			stats
		};
	}
	async getTraces() {
		try {
			return (await getTraceStore().getTracesByEvaluation(this.id)).map((trace) => ({
				traceId: trace.traceId,
				evaluationId: trace.evaluationId,
				testCaseId: trace.testCaseId,
				metadata: trace.metadata,
				spans: (trace.spans || []).map((span) => {
					const durationMs = span.endTime && span.startTime ? (span.endTime - span.startTime) / 1e6 : void 0;
					const statusCode = span.statusCode === 1 ? "ok" : span.statusCode === 2 ? "error" : "unset";
					return {
						spanId: span.spanId,
						parentSpanId: span.parentSpanId,
						name: span.name,
						kind: span.kind || "unspecified",
						startTime: span.startTime,
						endTime: span.endTime,
						durationMs,
						attributes: span.attributes || {},
						status: {
							code: statusCode,
							message: span.statusMessage
						},
						depth: 0,
						events: span.events || []
					};
				})
			}));
		} catch (error) {
			logger_default.debug(`Failed to fetch traces for eval ${this.id}: ${error}`);
			return [];
		}
	}
	async toResultsFile() {
		const traces = await this.getTraces();
		return {
			version: this.version(),
			createdAt: new Date(this.createdAt).toISOString(),
			results: await this.toEvaluateSummary(),
			config: this.config,
			author: this.author || null,
			prompts: this.getPrompts(),
			datasetId: this.datasetId || null,
			...traces.length > 0 && { traces }
		};
	}
	async delete() {
		const db = getDb();
		db.transaction(() => {
			db.delete(evalsToDatasetsTable).where(eq(evalsToDatasetsTable.evalId, this.id)).run();
			db.delete(evalsToPromptsTable).where(eq(evalsToPromptsTable.evalId, this.id)).run();
			db.delete(evalsToTagsTable).where(eq(evalsToTagsTable.evalId, this.id)).run();
			db.delete(evalResultsTable).where(eq(evalResultsTable.evalId, this.id)).run();
			db.delete(evalsTable).where(eq(evalsTable.id, this.id)).run();
		});
	}
	/**
	* Creates a deep copy of this eval including all results.
	* Uses batching to avoid memory exhaustion on large evals.
	* @param description - Optional description for the new eval
	* @param distinctTestCount - Optional pre-computed test count to avoid duplicate query
	*/
	async copy(description, distinctTestCount) {
		const newEvalId = createEvalId(/* @__PURE__ */ new Date());
		const copyDescription = description || `${this.description || "Evaluation"} (Copy)`;
		const testCount = distinctTestCount ?? await this.getResultsCount();
		logger_default.info("Starting eval copy", {
			sourceEvalId: this.id,
			targetEvalId: newEvalId,
			distinctTestCount: testCount
		});
		const newConfig = structuredClone(this.config);
		newConfig.description = copyDescription;
		const newPrompts = structuredClone(this.prompts);
		const newVars = this.vars ? structuredClone(this.vars) : [];
		const author = getUserEmail();
		const db = getDb();
		let copiedCount = 0;
		db.transaction(() => {
			db.insert(evalsTable).values({
				id: newEvalId,
				createdAt: Date.now(),
				author,
				description: copyDescription,
				config: newConfig,
				results: {},
				prompts: newPrompts,
				vars: newVars,
				runtimeOptions: sanitizeRuntimeOptions(this.runtimeOptions),
				isRedteam: Boolean(newConfig.redteam)
			}).run();
			const promptRels = db.select().from(evalsToPromptsTable).where(eq(evalsToPromptsTable.evalId, this.id)).all();
			if (promptRels.length > 0) db.insert(evalsToPromptsTable).values(promptRels.map((rel) => ({
				evalId: newEvalId,
				promptId: rel.promptId
			}))).onConflictDoNothing().run();
			if (this.config.tags) for (const [tagKey, tagValue] of Object.entries(this.config.tags)) {
				const tagId = sha256(`${tagKey}:${tagValue}`);
				db.insert(tagsTable).values({
					id: tagId,
					name: tagKey,
					value: tagValue
				}).onConflictDoNothing().run();
				db.insert(evalsToTagsTable).values({
					evalId: newEvalId,
					tagId
				}).onConflictDoNothing().run();
			}
			const datasetRel = db.select().from(evalsToDatasetsTable).where(eq(evalsToDatasetsTable.evalId, this.id)).limit(1).all();
			if (datasetRel.length > 0) db.insert(evalsToDatasetsTable).values({
				evalId: newEvalId,
				datasetId: datasetRel[0].datasetId
			}).onConflictDoNothing().run();
			const BATCH_SIZE = 1e3;
			let offset = 0;
			while (true) {
				const batch = db.select().from(evalResultsTable).where(eq(evalResultsTable.evalId, this.id)).orderBy(evalResultsTable.id).limit(BATCH_SIZE).offset(offset).all();
				if (batch.length === 0) break;
				const now = Date.now();
				const copiedResults = batch.map((result) => ({
					...result,
					id: crypto.randomUUID(),
					evalId: newEvalId,
					createdAt: now,
					updatedAt: now
				}));
				db.insert(evalResultsTable).values(copiedResults).run();
				copiedCount += batch.length;
				offset += BATCH_SIZE;
				logger_default.debug("Copied batch of eval results", {
					sourceEvalId: this.id,
					targetEvalId: newEvalId,
					batchSize: batch.length,
					rowsCopied: copiedCount,
					distinctTestCount: testCount
				});
			}
		});
		logger_default.info("Eval copy completed successfully", {
			sourceEvalId: this.id,
			targetEvalId: newEvalId,
			rowsCopied: copiedCount,
			distinctTestCount: testCount
		});
		return await Eval.findById(newEvalId);
	}
	get shared() {
		return this._shared;
	}
	set shared(shared) {
		this._shared = shared;
	}
};
/**
* Queries summaries of all evals, optionally for a given dataset.
*
* @param datasetId - An optional dataset ID to filter by.
* @param type - An optional eval type to filter by.
* @param includeProviders - An optional flag to include providers in the summary.
* @returns A list of eval summaries.
*/
async function getEvalSummaries(datasetId, type, includeProviders = false) {
	const db = getDb();
	const whereClauses = [];
	if (datasetId) whereClauses.push(eq(evalsToDatasetsTable.datasetId, datasetId));
	if (type) if (type === "redteam") whereClauses.push(sql`json_type(${evalsTable.config}, '$.redteam') IS NOT NULL`);
	else whereClauses.push(sql`json_type(${evalsTable.config}, '$.redteam') IS NULL`);
	/**
	* Deserialize the evals. A few things to note:
	*
	* - Test statistics are derived from the prompt metrics as this is the only reliable source of truth
	* that's written to the evals table.
	*/
	return db.select({
		evalId: evalsTable.id,
		createdAt: evalsTable.createdAt,
		description: evalsTable.description,
		datasetId: evalsToDatasetsTable.datasetId,
		isRedteam: sql`json_type(${evalsTable.config}, '$.redteam') IS NOT NULL`,
		prompts: evalsTable.prompts,
		config: evalsTable.config
	}).from(evalsTable).leftJoin(evalsToDatasetsTable, eq(evalsTable.id, evalsToDatasetsTable.evalId)).where(and(...whereClauses)).orderBy(desc(evalsTable.createdAt)).all().map((result) => {
		const passCount = result.prompts?.reduce((memo, prompt) => {
			return memo + (prompt.metrics?.testPassCount ?? 0);
		}, 0) ?? 0;
		const failCount = result.prompts?.reduce((memo, prompt) => {
			return memo + (prompt.metrics?.testFailCount ?? 0);
		}, 0) ?? 0;
		const testCounts = result.prompts?.map((p) => {
			return (p.metrics?.testPassCount ?? 0) + (p.metrics?.testFailCount ?? 0) + (p.metrics?.testErrorCount ?? 0);
		}) ?? [0];
		const testCount = testCounts.length > 0 ? testCounts[0] : 0;
		const testRunCount = testCount * (result.prompts?.length ?? 0);
		const deserializedProviders = [];
		const providers = result.config.providers;
		if (includeProviders) {
			if (typeof providers === "string") deserializedProviders.push({
				id: providers,
				label: null
			});
			else if (Array.isArray(providers)) providers.forEach((p) => {
				if (typeof p === "string") deserializedProviders.push({
					id: p,
					label: null
				});
				else if (typeof p === "object" && p) {
					const keys = Object.keys(p);
					if (keys.length === 1 && !("id" in p)) {
						const providerId = keys[0];
						const providerConfig = p[providerId];
						deserializedProviders.push({
							id: providerId,
							label: providerConfig.label ?? null
						});
					} else deserializedProviders.push({
						id: p.id ?? "unknown",
						label: p.label ?? null
					});
				}
			});
		}
		return {
			evalId: result.evalId,
			createdAt: result.createdAt,
			description: result.description,
			numTests: testCount,
			datasetId: result.datasetId,
			isRedteam: Boolean(result.isRedteam),
			passRate: testRunCount > 0 ? passCount / testRunCount * 100 : 0,
			label: result.description ? `${result.description} (${result.evalId})` : result.evalId,
			providers: deserializedProviders,
			attackSuccessRate: type === "redteam" ? calculateAttackSuccessRate(testRunCount, failCount) : void 0
		};
	});
}

//#endregion
export { createEvalId as a, combineFilterConditions as i, EvalQueries as n, escapeJsonPathKey as o, buildSafeJsonPath as r, getEvalSummaries as s, Eval as t };
//# sourceMappingURL=eval-BysnTFMB.js.map