/**
 * Compliance drift alerting (Tier 3 of the agentic eval framework).
 *
 * When any (model × harness) cell exceeds the configured threshold for a
 * sustained window with at least N turns, this module emits a
 * `product_feedback` entity with `kind: "compliance_drift"` so the alarm
 * surfaces alongside other product-level signals in the existing
 * agent-feedback pipeline.
 *
 * Configuration (env):
 *   - `NEOTOMA_COMPLIANCE_BACKFILL_ALERT_THRESHOLD` (default `0.30`)
 *   - `NEOTOMA_COMPLIANCE_BACKFILL_ALERT_WINDOW`    (default `24h`)
 *   - `NEOTOMA_COMPLIANCE_BACKFILL_ALERT_MIN_TURNS` (default `100`)
 *
 * The check is intentionally idempotent: each (model × harness × window
 * end) tuple keys an idempotency value, so re-running this module under
 * cron will not flood the feedback queue with duplicates. The deterministic
 * key also lets operators see drift recurrences as additional observations
 * rather than separate entities.
 *
 * Triggering happens lazily — callers (cron, CLI, tests) call
 * `runComplianceAlertCheck()`; nothing runs automatically inside the
 * server hot path.
 */
import { logger } from "../../utils/logger.js";
import { getComplianceScorecard, resolveWindowBoundary } from "./scorecard.js";
const DEFAULT_THRESHOLD = 0.3;
const DEFAULT_WINDOW = "24h";
const DEFAULT_MIN_TURNS = 100;
const THRESHOLD_ENV = "NEOTOMA_COMPLIANCE_BACKFILL_ALERT_THRESHOLD";
const WINDOW_ENV = "NEOTOMA_COMPLIANCE_BACKFILL_ALERT_WINDOW";
const MIN_TURNS_ENV = "NEOTOMA_COMPLIANCE_BACKFILL_ALERT_MIN_TURNS";
function readNumberEnv(name, fallback) {
    const raw = process.env[name];
    if (!raw)
        return fallback;
    const n = Number(raw);
    return Number.isFinite(n) ? n : fallback;
}
function readStringEnv(name, fallback) {
    const raw = process.env[name];
    return raw && raw.trim().length > 0 ? raw.trim() : fallback;
}
async function defaultStore(userId) {
    const { createOperations } = await import("../../core/operations.js");
    const ops = createOperations({ userId });
    return {
        async storeStructured(input) {
            return (await ops.storeStructured({
                entities: input.entities,
                idempotency_key: input.idempotency_key,
            }));
        },
    };
}
/**
 * Compute alerts for the current window without emitting anything. Used
 * by the CLI dry-run and by tests.
 */
export function evaluateAlerts(options) {
    const threshold = Math.max(0, Math.min(1, options.threshold ?? readNumberEnv(THRESHOLD_ENV, DEFAULT_THRESHOLD)));
    const window = options.window ?? readStringEnv(WINDOW_ENV, DEFAULT_WINDOW);
    const minTurns = Math.max(1, options.min_turns ?? readNumberEnv(MIN_TURNS_ENV, DEFAULT_MIN_TURNS));
    const now = options.now ?? new Date();
    const since = resolveWindowBoundary(window, now);
    const until = now.toISOString();
    const card = getComplianceScorecard({
        user_id: options.user_id,
        since,
        until,
        group_by: "model+harness",
        min_turns: minTurns,
    });
    const alerts = [];
    for (const cell of card.cells) {
        if (cell.total_turns < minTurns)
            continue;
        if (cell.backfill_rate < threshold)
            continue;
        alerts.push({
            entity_type: "product_feedback",
            kind: "compliance_drift",
            title: `Compliance backfill rate exceeded for ${cell.model} on ${cell.harness}`,
            body: [
                `Backfill rate ${(cell.backfill_rate * 100).toFixed(2)}% over the last ${window}`,
                `Threshold: ${(threshold * 100).toFixed(2)}%`,
                `Window: ${since} → ${until}`,
                `Turns: ${cell.total_turns} (${cell.backfilled_turns} backfilled)`,
                cell.top_missed_steps.length > 0
                    ? `Top missed steps: ${cell.top_missed_steps
                        .slice(0, 5)
                        .map((s) => `${s.step} (${s.count})`)
                        .join(", ")}`
                    : null,
            ]
                .filter(Boolean)
                .join("\n"),
            metadata: {
                model: cell.model,
                harness: cell.harness,
                rate: cell.backfill_rate,
                threshold,
                min_turns: minTurns,
                window,
                cell_total_turns: cell.total_turns,
                cell_backfilled_turns: cell.backfilled_turns,
                top_missed_steps: cell.top_missed_steps,
            },
            identity: { model: cell.model, harness: cell.harness },
            source: "compliance-scorecard",
        });
    }
    return { threshold, window, min_turns: minTurns, alerts };
}
/**
 * Run an alert check end-to-end: read the scorecard, evaluate cells
 * against the threshold, and write `product_feedback` entities for every
 * cell that breached.
 */
export async function runComplianceAlertCheck(options) {
    const evaluation = evaluateAlerts({
        user_id: options.user_id,
        threshold: options.threshold,
        window: options.window,
        min_turns: options.min_turns,
        now: options.now,
    });
    if (options.dry_run === true) {
        return {
            threshold: evaluation.threshold,
            window: evaluation.window,
            min_turns: evaluation.min_turns,
            evaluated_cells: evaluation.alerts.length,
            emitted: evaluation.alerts,
            errors: [],
            dry_run: true,
        };
    }
    const store = options.store ?? (await defaultStore(options.user_id));
    const errors = [];
    const emitted = [];
    const now = options.now ?? new Date();
    const dayBucket = now.toISOString().slice(0, 10);
    for (const alert of evaluation.alerts) {
        const idempotency_key = `compliance-drift-${alert.identity.model}-${alert.identity.harness}-${dayBucket}`;
        try {
            await store.storeStructured({
                entities: [alert],
                idempotency_key,
            });
            emitted.push(alert);
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            errors.push(`${alert.identity.model}/${alert.identity.harness}: ${message}`);
            logger.warn(`[compliance/alerting] failed to emit drift alert ${idempotency_key}: ${message}`);
        }
    }
    return {
        threshold: evaluation.threshold,
        window: evaluation.window,
        min_turns: evaluation.min_turns,
        evaluated_cells: evaluation.alerts.length,
        emitted,
        errors,
        dry_run: false,
    };
}
//# sourceMappingURL=alerting.js.map