{"version":3,"file":"regression.mjs","names":[],"sources":["../../../../../../../ai/src/eval/regression.ts"],"sourcesContent":["import type { EvalRegression, EvalReport } from \"../contracts/agent/eval.type\";\n\n/**\n * Diff a fresh {@link EvalReport} against a `baseline`, joining cases by\n * name, to produce an {@link EvalRegression} verdict.\n *\n * A case **regresses** when its new aggregate `score` is more than\n * `tolerance` below its baseline score (`before - after > tolerance`).\n * Cases that improved, held steady, or moved within `tolerance` are not\n * flagged. Cases present in only one of the two reports are surfaced\n * under `added` / `removed` rather than treated as regressions, so adding\n * or dropping a case never fails the gate by itself.\n *\n * Pure — depends only on the two reports and the tolerance; attaches no\n * state and mutates neither input.\n *\n * @param report - The newly produced report.\n * @param baseline - A prior report to compare against.\n * @param tolerance - Max allowed score drop before a case counts as a\n *   regression. Defaults to `0` (any drop regresses).\n *\n * @example\n * const regression = diff(report, baseline, 0.05);\n * expect(regression.passed).toBe(true);\n */\nexport function diff<TOutput = unknown>(\n  report: EvalReport<TOutput>,\n  baseline: EvalReport<TOutput>,\n  tolerance = 0,\n): EvalRegression {\n  const baselineScores = new Map<string, number>();\n\n  for (const entry of baseline.cases) {\n    baselineScores.set(entry.case.name, entry.score);\n  }\n\n  const currentNames = new Set<string>();\n  const regressed: EvalRegression[\"regressed\"] = [];\n\n  for (const entry of report.cases) {\n    const name = entry.case.name;\n    currentNames.add(name);\n\n    const before = baselineScores.get(name);\n\n    if (before === undefined) {\n      continue;\n    }\n\n    if (before - entry.score > tolerance) {\n      regressed.push({ name, before, after: entry.score });\n    }\n  }\n\n  const removed = baseline.cases\n    .map((entry) => entry.case.name)\n    .filter((name) => !currentNames.has(name));\n\n  const added = report.cases\n    .map((entry) => entry.case.name)\n    .filter((name) => !baselineScores.has(name));\n\n  return {\n    regressed,\n    removed,\n    added,\n    passed: regressed.length === 0,\n  };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,KACd,QACA,UACA,YAAY,GACI;CAChB,MAAM,iCAAiB,IAAI,IAAoB;CAE/C,KAAK,MAAM,SAAS,SAAS,OAC3B,eAAe,IAAI,MAAM,KAAK,MAAM,MAAM,KAAK;CAGjD,MAAM,+BAAe,IAAI,IAAY;CACrC,MAAM,YAAyC,CAAC;CAEhD,KAAK,MAAM,SAAS,OAAO,OAAO;EAChC,MAAM,OAAO,MAAM,KAAK;EACxB,aAAa,IAAI,IAAI;EAErB,MAAM,SAAS,eAAe,IAAI,IAAI;EAEtC,IAAI,WAAW,QACb;EAGF,IAAI,SAAS,MAAM,QAAQ,WACzB,UAAU,KAAK;GAAE;GAAM;GAAQ,OAAO,MAAM;EAAM,CAAC;CAEvD;CAUA,OAAO;EACL;EACA,SAVc,SAAS,MACtB,KAAK,UAAU,MAAM,KAAK,IAAI,CAAC,CAC/B,QAAQ,SAAS,CAAC,aAAa,IAAI,IAAI,CAQlC;EACN,OAPY,OAAO,MAClB,KAAK,UAAU,MAAM,KAAK,IAAI,CAAC,CAC/B,QAAQ,SAAS,CAAC,eAAe,IAAI,IAAI,CAKtC;EACJ,QAAQ,UAAU,WAAW;CAC/B;AACF"}