{"version":3,"file":"policy.mjs","names":[],"sources":["../../src/ci/policy.ts"],"sourcesContent":["import { conclusionForVerdict, type CiConclusion } from \"./taxonomy.ts\";\nimport type { CiVerdict } from \"./verdict.ts\";\n\n// The policy engine (#3500). Turns the single hard-coded gate into a\n// configurable condition set: a per-repo config gives each condition\n// `fail | warn | off`, and this pure function folds (config, condition results)\n// into one run conclusion plus the per-condition verdicts that produced it.\n//\n// It sits one layer above the taxonomy (#3498): the taxonomy says what a\n// verdict *would* conclude; the policy decides whether this repo lets that\n// condition fail, soften to a warning, or be suppressed entirely.\n\n/** What a repo lets a condition do: fail the run, warn-only, or ignore it. */\nexport type ConditionPolicy = \"fail\" | \"warn\" | \"off\";\n\n/** One condition's evaluated outcome, before policy is applied. */\nexport interface ConditionResult {\n  /** Stable condition key, matched against the repo config (e.g. \"untested-data-access\"). */\n  condition: string;\n  verdict: CiVerdict;\n}\n\n/** A condition after policy resolution — its effective conclusion and the policy that shaped it. */\nexport interface ResolvedCondition {\n  condition: string;\n  policy: ConditionPolicy;\n  verdict: CiVerdict;\n  conclusion: CiConclusion;\n}\n\nexport interface RunEvaluation {\n  /** The run's overall conclusion — the worst surfaced per-condition conclusion. */\n  conclusion: CiConclusion;\n  /** Per-condition results that were surfaced (an `off` condition is dropped). */\n  conditions: ResolvedCondition[];\n}\n\n/** Per-repo policy: condition key → its policy. Absent keys fall back to defaults. */\nexport type RepoPolicyConfig = Record<string, ConditionPolicy>;\n\n/**\n * Opinionated safe defaults (epic #3493). A captured new query already ran in a\n * test (tested by definition), so plain `new-query` is informational; but a new\n * query that ships with a beyond-threshold index recommendation is actionable\n * while it's still a one-line fix, so `new-query-index` blocks. Everything that\n * marks a genuine, diff-introduced problem fails by default. The set is\n * deliberately narrow so the gate under-fires.\n */\nexport const DEFAULT_CONDITION_POLICIES: RepoPolicyConfig = {\n  \"untested-data-access\": \"fail\",\n  \"schema-drift\": \"fail\", // #3296 owns the signal; this just gates on it\n  \"new-query\": \"warn\", // captured ⇒ tested — informational, surfaced, non-blocking\n  \"new-query-index\": \"fail\", // a new query carrying a beyond-threshold index rec — actionable, blocks\n  \"regression-beyond-threshold\": \"fail\", // #3104\n  \"high-value-nudge\": \"fail\", // narrow set only\n};\n\n/**\n * Fallback for a condition with neither a repo setting nor a documented default:\n * surface it, don't block. Under-firing on an unrecognised condition is the safe\n * side of the line.\n */\nexport const FALLBACK_POLICY: ConditionPolicy = \"warn\";\n\n/** Resolve the effective policy for a condition: repo config, else default, else fallback. */\nexport function policyFor(\n  condition: string,\n  config: RepoPolicyConfig = {},\n): ConditionPolicy {\n  return (\n    config[condition] ??\n    DEFAULT_CONDITION_POLICIES[condition] ??\n    FALLBACK_POLICY\n  );\n}\n\n/**\n * Fold a policy over a condition's base (taxonomy) conclusion. `fail` respects\n * the taxonomy; `warn` caps a failure at `neutral` (still surfaced, never\n * blocking); `off` is handled by dropping the condition before this runs.\n */\nfunction applyPolicy(\n  base: CiConclusion,\n  policy: ConditionPolicy,\n): CiConclusion {\n  if (policy === \"warn\") return base === \"failure\" ? \"neutral\" : base;\n  return base;\n}\n\nconst SEVERITY: Record<CiConclusion, number> = {\n  success: 0,\n  neutral: 1,\n  failure: 2,\n};\n\n/**\n * Evaluate a run: apply each condition's policy, then take the run conclusion as\n * the worst surfaced conclusion. Pure — the same (results, config) always yields\n * the same evaluation.\n *\n * Only diff-introduced conditions should reach here: detectors (e.g. the crude\n * gate, #3496) evaluate the PR diff, so pre-existing untouched code never\n * produces a result and can't trip a condition. With no results, the run passes.\n */\nexport function evaluateRun(\n  results: ConditionResult[],\n  config: RepoPolicyConfig = {},\n): RunEvaluation {\n  const conditions: ResolvedCondition[] = [];\n  for (const result of results) {\n    const policy = policyFor(result.condition, config);\n    if (policy === \"off\") continue; // suppressed entirely — not surfaced\n    conditions.push({\n      condition: result.condition,\n      policy,\n      verdict: result.verdict,\n      conclusion: applyPolicy(conclusionForVerdict(result.verdict), policy),\n    });\n  }\n  const conclusion = conditions.reduce<CiConclusion>(\n    (worst, c) =>\n      SEVERITY[c.conclusion] > SEVERITY[worst] ? c.conclusion : worst,\n    \"success\",\n  );\n  return { conclusion, conditions };\n}\n"],"mappings":";;;;;;;;;;;AAgDA,MAAa,6BAA+C;CAC1D,wBAAwB;CACxB,gBAAgB;CAChB,aAAa;CACb,mBAAmB;CACnB,+BAA+B;CAC/B,oBAAoB;CACrB;;;;;;AAOD,MAAa,kBAAmC;;AAGhD,SAAgB,UACd,WACA,SAA2B,EAAE,EACZ;AACjB,QACE,OAAO,cACP,2BAA2B,cAAA;;;;;;;AAU/B,SAAS,YACP,MACA,QACc;AACd,KAAI,WAAW,OAAQ,QAAO,SAAS,YAAY,YAAY;AAC/D,QAAO;;AAGT,MAAM,WAAyC;CAC7C,SAAS;CACT,SAAS;CACT,SAAS;CACV;;;;;;;;;;AAWD,SAAgB,YACd,SACA,SAA2B,EAAE,EACd;CACf,MAAM,aAAkC,EAAE;AAC1C,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,SAAS,UAAU,OAAO,WAAW,OAAO;AAClD,MAAI,WAAW,MAAO;AACtB,aAAW,KAAK;GACd,WAAW,OAAO;GAClB;GACA,SAAS,OAAO;GAChB,YAAY,YAAY,qBAAqB,OAAO,QAAQ,EAAE,OAAO;GACtE,CAAC;;AAOJ,QAAO;EAAE,YALU,WAAW,QAC3B,OAAO,MACN,SAAS,EAAE,cAAc,SAAS,SAAS,EAAE,aAAa,OAC5D,UAEiB;EAAE;EAAY"}