{"version":3,"file":"verdict.mjs","names":[],"sources":["../../src/ci/verdict.ts"],"sourcesContent":["import { z } from \"zod\";\n\n/**\n * The versioned, shared verdict contract (#3497) — the cohesion mechanism for\n * the CI⇄MCP verdict loop (epic #3493). One condition's outcome on a CI run,\n * structured so an agent can *route* it (fix / add a test / escalate) without\n * scraping a build log. Every future condition — coverage gaps, regressions,\n * schema drift, nudges — emits this same shape, so no condition grows its own\n * red/green or its own remediation UX.\n *\n * The same payload is meant to render to the PR comment and to surface in the\n * MCP run object; the surfacing itself is owned by #3106/#3105, so this module\n * defines and validates the contract they consume, plus a reference renderer.\n *\n * Copy rule (ADR-0003): every string a verdict carries — `reason`, the action\n * `detail`s — renders verbatim in the PR comment. Hold it to\n * the `writing` skill (`.claude/skills/writing/SKILL.md`): one point per field, no hedging, no\n * self-narration, no restating the condition. The terse-voice test in\n * `verdict.test.ts` is the reference copy the analyzer conforms to.\n */\n\n/**\n * Bump only on a *breaking* change to the shape below, so a consumer can refuse\n * a payload it doesn't understand. Additive, backward-compatible fields do not\n * require a bump.\n */\nexport const VERDICT_CONTRACT_VERSION = 1;\n\n/**\n * How honest the run can be about a condition. `pass` is the only clean\n * outcome; every other class is a non-pass the loop must route.\n * `uncertain-conservative-flag` is the deliberate \"we could not check this, so\n * we're flagging to be safe\" state — kept distinct so the tool's blindness is\n * never dressed up as either a clean pass or a hard failure.\n */\nexport const VerdictClass = z.enum([\n  \"pass\",\n  \"coverage-gap\",\n  \"finding\",\n  \"infra-error\",\n  \"setup-broken\",\n  \"uncertain-conservative-flag\",\n]);\nexport type VerdictClass = z.infer<typeof VerdictClass>;\n\n/**\n * The kind of action a verdict routes to. The ceiling is `auto-fix`; the floor\n * is `notify-human` — escalate-to-human-with-context, a designed success state\n * rather than a gap. `none` is only for a clean pass.\n */\nexport const VerdictActionKind = z.enum([\n  \"auto-fix\",\n  \"add-test\",\n  \"triage\",\n  \"notify-human\",\n  \"none\",\n]);\nexport type VerdictActionKind = z.infer<typeof VerdictActionKind>;\n\n/**\n * A routable next action. `detail` always carries the human- and\n * machine-readable specifics: the fix instruction, the test to add, or — for\n * `notify-human` — the context handed to the person. Modeling \"notify human\n * (with context)\" as a first-class value, not an absent field, is the point:\n * the escalate-to-human floor is always well-formed, never an absence.\n */\nexport const VerdictAction = z.object({\n  kind: VerdictActionKind,\n  detail: z.string(),\n});\nexport type VerdictAction = z.infer<typeof VerdictAction>;\n\n/** A next step / triage action that escalates to a human, carrying context. */\nexport function notifyHuman(context: string): VerdictAction {\n  return { kind: \"notify-human\", detail: context };\n}\n\n/** The \"nothing to do\" action, for a clean pass. */\nexport const NO_ACTION: VerdictAction = { kind: \"none\", detail: \"\" };\n\nexport const CiVerdict = z.object({\n  version: z.literal(VERDICT_CONTRACT_VERSION),\n  /** The condition evaluated, e.g. \"untested-data-access\", \"regression-beyond-threshold\". */\n  condition: z.string().min(1),\n  verdictClass: VerdictClass,\n  /** Plain-language why, honest about the tool's epistemic state. */\n  reason: z.string().min(1),\n  /** The best-available next step; `notify-human` is a valid, well-formed value. */\n  nextStep: VerdictAction,\n  /** The non-fix exit — triage/acknowledge, or escalate to a human. */\n  triageAction: VerdictAction,\n  /** Deep link to the relevant guide, when one applies. */\n  docsUrl: z.string().url().optional(),\n  /** MCP tools an agent should reach for to act on this verdict. */\n  suggestedMcpTools: z.array(z.string()).default([]),\n});\nexport type CiVerdict = z.infer<typeof CiVerdict>;\n\nconst ACTION_LABEL: Record<VerdictActionKind, string> = {\n  \"auto-fix\": \"Fix\",\n  \"add-test\": \"Add a test\",\n  triage: \"Triage\",\n  \"notify-human\": \"Notify a human\",\n  none: \"No action\",\n};\n\n/**\n * Render a verdict to Markdown for a PR comment. Intentionally presentation\n * only: it shows the fields, it does not decide pass/fail (that mapping is the\n * failure taxonomy, #3498). Kept alongside the contract so \"the comment renders\n * from the contract\" is demonstrably true and ready for the analyzer to import.\n */\nexport function renderVerdictMarkdown(verdict: CiVerdict): string {\n  const parts: string[] = [`**${verdict.condition}** — ${verdict.reason}`];\n  const hasNextStep = verdict.nextStep.kind !== \"none\";\n  const hasTriage = verdict.triageAction.kind !== \"none\";\n  // The fix path and the triage exit are alternatives, not a sequence — when\n  // both exist, render them as one choice.\n  if (hasNextStep && hasTriage) {\n    parts.push(\n      [\n        \"**Next step — do one of:**\",\n        `- **${ACTION_LABEL[verdict.nextStep.kind]}:** ${verdict.nextStep.detail}`,\n        `- **${ACTION_LABEL[verdict.triageAction.kind]}:** ${verdict.triageAction.detail}`,\n      ].join(\"\\n\"),\n    );\n  } else if (hasNextStep) {\n    parts.push(\n      `**Next step (${ACTION_LABEL[verdict.nextStep.kind]}):** ${verdict.nextStep.detail}`,\n    );\n  } else if (hasTriage) {\n    parts.push(\n      `**Triage (${ACTION_LABEL[verdict.triageAction.kind]}):** ${verdict.triageAction.detail}`,\n    );\n  }\n  if (verdict.suggestedMcpTools.length > 0) {\n    const tools = verdict.suggestedMcpTools.map((t) => `\\`${t}\\``).join(\", \");\n    parts.push(`**MCP tools:** ${tools}`);\n  }\n  if (verdict.docsUrl) {\n    parts.push(`[Learn more](${verdict.docsUrl})`);\n  }\n  return parts.join(\"\\n\\n\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,MAAa,2BAA2B;;;;;;;;AASxC,MAAa,eAAe,EAAE,KAAK;CACjC;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;AAQF,MAAa,oBAAoB,EAAE,KAAK;CACtC;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;AAUF,MAAa,gBAAgB,EAAE,OAAO;CACpC,MAAM;CACN,QAAQ,EAAE,QAAQ;CACnB,CAAC;;AAIF,SAAgB,YAAY,SAAgC;AAC1D,QAAO;EAAE,MAAM;EAAgB,QAAQ;EAAS;;;AAIlD,MAAa,YAA2B;CAAE,MAAM;CAAQ,QAAQ;CAAI;AAEpE,MAAa,YAAY,EAAE,OAAO;CAChC,SAAS,EAAE,QAAA,EAAiC;;CAE5C,WAAW,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC5B,cAAc;;CAEd,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE;;CAEzB,UAAU;;CAEV,cAAc;;CAEd,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU;;CAEpC,mBAAmB,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CACnD,CAAC;AAGF,MAAM,eAAkD;CACtD,YAAY;CACZ,YAAY;CACZ,QAAQ;CACR,gBAAgB;CAChB,MAAM;CACP;;;;;;;AAQD,SAAgB,sBAAsB,SAA4B;CAChE,MAAM,QAAkB,CAAC,KAAK,QAAQ,UAAU,OAAO,QAAQ,SAAS;CACxE,MAAM,cAAc,QAAQ,SAAS,SAAS;CAC9C,MAAM,YAAY,QAAQ,aAAa,SAAS;AAGhD,KAAI,eAAe,UACjB,OAAM,KACJ;EACE;EACA,OAAO,aAAa,QAAQ,SAAS,MAAM,MAAM,QAAQ,SAAS;EAClE,OAAO,aAAa,QAAQ,aAAa,MAAM,MAAM,QAAQ,aAAa;EAC3E,CAAC,KAAK,KAAK,CACb;UACQ,YACT,OAAM,KACJ,gBAAgB,aAAa,QAAQ,SAAS,MAAM,OAAO,QAAQ,SAAS,SAC7E;UACQ,UACT,OAAM,KACJ,aAAa,aAAa,QAAQ,aAAa,MAAM,OAAO,QAAQ,aAAa,SAClF;AAEH,KAAI,QAAQ,kBAAkB,SAAS,GAAG;EACxC,MAAM,QAAQ,QAAQ,kBAAkB,KAAK,MAAM,KAAK,EAAE,IAAI,CAAC,KAAK,KAAK;AACzE,QAAM,KAAK,kBAAkB,QAAQ;;AAEvC,KAAI,QAAQ,QACV,OAAM,KAAK,gBAAgB,QAAQ,QAAQ,GAAG;AAEhD,QAAO,MAAM,KAAK,OAAO"}