@query-doctor/core 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,12 @@ let zod = require("zod");
13
13
  * The same payload is meant to render to the PR comment and to surface in the
14
14
  * MCP run object; the surfacing itself is owned by #3106/#3105, so this module
15
15
  * defines and validates the contract they consume, plus a reference renderer.
16
+ *
17
+ * Copy rule (ADR-0003): every string a verdict carries — `reason`, the action
18
+ * `detail`s — renders verbatim in the PR comment. Hold it to
19
+ * `.claude/rules/writing.md`: one point per field, no hedging, no
20
+ * self-narration, no restating the condition. The terse-voice test in
21
+ * `verdict.test.ts` is the reference copy the analyzer conforms to.
16
22
  */
17
23
  /**
18
24
  * Bump only on a *breaking* change to the shape below, so a consumer can refuse
@@ -95,8 +101,15 @@ const ACTION_LABEL = {
95
101
  */
96
102
  function renderVerdictMarkdown(verdict) {
97
103
  const parts = [`**${verdict.condition}** — ${verdict.reason}`];
98
- if (verdict.nextStep.kind !== "none") parts.push(`**Next step (${ACTION_LABEL[verdict.nextStep.kind]}):** ${verdict.nextStep.detail}`);
99
- if (verdict.triageAction.kind !== "none") parts.push(`**Triage (${ACTION_LABEL[verdict.triageAction.kind]}):** ${verdict.triageAction.detail}`);
104
+ const hasNextStep = verdict.nextStep.kind !== "none";
105
+ const hasTriage = verdict.triageAction.kind !== "none";
106
+ if (hasNextStep && hasTriage) parts.push([
107
+ "**Next step — do one of:**",
108
+ `- **${ACTION_LABEL[verdict.nextStep.kind]}:** ${verdict.nextStep.detail}`,
109
+ `- **${ACTION_LABEL[verdict.triageAction.kind]}:** ${verdict.triageAction.detail}`
110
+ ].join("\n"));
111
+ else if (hasNextStep) parts.push(`**Next step (${ACTION_LABEL[verdict.nextStep.kind]}):** ${verdict.nextStep.detail}`);
112
+ else if (hasTriage) parts.push(`**Triage (${ACTION_LABEL[verdict.triageAction.kind]}):** ${verdict.triageAction.detail}`);
100
113
  if (verdict.suggestedMcpTools.length > 0) {
101
114
  const tools = verdict.suggestedMcpTools.map((t) => `\`${t}\``).join(", ");
102
115
  parts.push(`**MCP tools:** ${tools}`);
@@ -1 +1 @@
1
- {"version":3,"file":"verdict.cjs","names":["z"],"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\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 if (verdict.nextStep.kind !== \"none\") {\n parts.push(\n `**Next step (${ACTION_LABEL[verdict.nextStep.kind]}):** ${verdict.nextStep.detail}`,\n );\n }\n if (verdict.triageAction.kind !== \"none\") {\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":";;;;;;;;;;;;;;;;;;;;;AAoBA,MAAa,2BAA2B;;;;;;;;AASxC,MAAa,eAAeA,IAAAA,EAAE,KAAK;CACjC;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;AAQF,MAAa,oBAAoBA,IAAAA,EAAE,KAAK;CACtC;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;AAUF,MAAa,gBAAgBA,IAAAA,EAAE,OAAO;CACpC,MAAM;CACN,QAAQA,IAAAA,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,YAAYA,IAAAA,EAAE,OAAO;CAChC,SAASA,IAAAA,EAAE,QAAA,EAAiC;CAE5C,WAAWA,IAAAA,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC5B,cAAc;CAEd,QAAQA,IAAAA,EAAE,QAAQ,CAAC,IAAI,EAAE;CAEzB,UAAU;CAEV,cAAc;CAEd,SAASA,IAAAA,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU;CAEpC,mBAAmBA,IAAAA,EAAE,MAAMA,IAAAA,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;AACxE,KAAI,QAAQ,SAAS,SAAS,OAC5B,OAAM,KACJ,gBAAgB,aAAa,QAAQ,SAAS,MAAM,OAAO,QAAQ,SAAS,SAC7E;AAEH,KAAI,QAAQ,aAAa,SAAS,OAChC,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"}
1
+ {"version":3,"file":"verdict.cjs","names":["z"],"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 * `.claude/rules/writing.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,eAAeA,IAAAA,EAAE,KAAK;CACjC;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;AAQF,MAAa,oBAAoBA,IAAAA,EAAE,KAAK;CACtC;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;AAUF,MAAa,gBAAgBA,IAAAA,EAAE,OAAO;CACpC,MAAM;CACN,QAAQA,IAAAA,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,YAAYA,IAAAA,EAAE,OAAO;CAChC,SAASA,IAAAA,EAAE,QAAA,EAAiC;CAE5C,WAAWA,IAAAA,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC5B,cAAc;CAEd,QAAQA,IAAAA,EAAE,QAAQ,CAAC,IAAI,EAAE;CAEzB,UAAU;CAEV,cAAc;CAEd,SAASA,IAAAA,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU;CAEpC,mBAAmBA,IAAAA,EAAE,MAAMA,IAAAA,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"}
@@ -14,6 +14,12 @@ import { z } from "zod";
14
14
  * The same payload is meant to render to the PR comment and to surface in the
15
15
  * MCP run object; the surfacing itself is owned by #3106/#3105, so this module
16
16
  * defines and validates the contract they consume, plus a reference renderer.
17
+ *
18
+ * Copy rule (ADR-0003): every string a verdict carries — `reason`, the action
19
+ * `detail`s — renders verbatim in the PR comment. Hold it to
20
+ * `.claude/rules/writing.md`: one point per field, no hedging, no
21
+ * self-narration, no restating the condition. The terse-voice test in
22
+ * `verdict.test.ts` is the reference copy the analyzer conforms to.
17
23
  */
18
24
  /**
19
25
  * Bump only on a *breaking* change to the shape below, so a consumer can refuse
@@ -1 +1 @@
1
- {"version":3,"file":"verdict.d.cts","names":[],"sources":["../../src/ci/verdict.ts"],"mappings":";;;;;;;;AAoBA;;;;;AASA;;;;;;;;;cATa,wBAAA;;;;;AAiBb;;;cARa,YAAA,EAAY,CAAA,CAAA,OAAA;;;;;;;;KAQb,YAAA,GAAe,CAAA,CAAE,KAAA,QAAa,YAAA;;;;;;cAO7B,iBAAA,EAAiB,CAAA,CAAA,OAAA;;;;;;;KAOlB,iBAAA,GAAoB,CAAA,CAAE,KAAA,QAAa,iBAAA;;;;;;;AAS/C;cAAa,aAAA,EAAa,CAAA,CAAA,SAAA;;;;;;;;;;KAId,aAAA,GAAgB,CAAA,CAAE,KAAA,QAAa,aAAA;;iBAG3B,WAAA,CAAY,OAAA,WAAkB,aAAA;;cAKjC,SAAA,EAAW,aAAA;AAAA,cAEX,SAAA,EAAS,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAgBV,SAAA,GAAY,CAAA,CAAE,KAAA,QAAa,SAAA;;;;;;;iBAgBvB,qBAAA,CAAsB,OAAA,EAAS,SAAA"}
1
+ {"version":3,"file":"verdict.d.cts","names":[],"sources":["../../src/ci/verdict.ts"],"mappings":";;;;;;;;AA0BA;;;;;AASA;;;;;;;;;;;;;;AAQA;cAjBa,wBAAA;;;;;;;;cASA,YAAA,EAAY,CAAA,CAAA,OAAA;;;;;;;;KAQb,YAAA,GAAe,CAAA,CAAE,KAAA,QAAa,YAAA;;;;;AAc1C;cAPa,iBAAA,EAAiB,CAAA,CAAA,OAAA;;;;;;;KAOlB,iBAAA,GAAoB,CAAA,CAAE,KAAA,QAAa,iBAAA;AAS/C;;;;;;;AAAA,cAAa,aAAA,EAAa,CAAA,CAAA,SAAA;;;;;;;;;;KAId,aAAA,GAAgB,CAAA,CAAE,KAAA,QAAa,aAAA;;iBAG3B,WAAA,CAAY,OAAA,WAAkB,aAAA;;cAKjC,SAAA,EAAW,aAAA;AAAA,cAEX,SAAA,EAAS,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAgBV,SAAA,GAAY,CAAA,CAAE,KAAA,QAAa,SAAA;;;;;;;iBAgBvB,qBAAA,CAAsB,OAAA,EAAS,SAAA"}
@@ -14,6 +14,12 @@ import { z } from "zod";
14
14
  * The same payload is meant to render to the PR comment and to surface in the
15
15
  * MCP run object; the surfacing itself is owned by #3106/#3105, so this module
16
16
  * defines and validates the contract they consume, plus a reference renderer.
17
+ *
18
+ * Copy rule (ADR-0003): every string a verdict carries — `reason`, the action
19
+ * `detail`s — renders verbatim in the PR comment. Hold it to
20
+ * `.claude/rules/writing.md`: one point per field, no hedging, no
21
+ * self-narration, no restating the condition. The terse-voice test in
22
+ * `verdict.test.ts` is the reference copy the analyzer conforms to.
17
23
  */
18
24
  /**
19
25
  * Bump only on a *breaking* change to the shape below, so a consumer can refuse
@@ -1 +1 @@
1
- {"version":3,"file":"verdict.d.mts","names":[],"sources":["../../src/ci/verdict.ts"],"mappings":";;;;;;;;AAoBA;;;;;AASA;;;;;;;;;cATa,wBAAA;;;;;AAiBb;;;cARa,YAAA,EAAY,CAAA,CAAA,OAAA;;;;;;;;KAQb,YAAA,GAAe,CAAA,CAAE,KAAA,QAAa,YAAA;;;;;;cAO7B,iBAAA,EAAiB,CAAA,CAAA,OAAA;;;;;;;KAOlB,iBAAA,GAAoB,CAAA,CAAE,KAAA,QAAa,iBAAA;;;;;;;AAS/C;cAAa,aAAA,EAAa,CAAA,CAAA,SAAA;;;;;;;;;;KAId,aAAA,GAAgB,CAAA,CAAE,KAAA,QAAa,aAAA;;iBAG3B,WAAA,CAAY,OAAA,WAAkB,aAAA;;cAKjC,SAAA,EAAW,aAAA;AAAA,cAEX,SAAA,EAAS,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAgBV,SAAA,GAAY,CAAA,CAAE,KAAA,QAAa,SAAA;;;;;;;iBAgBvB,qBAAA,CAAsB,OAAA,EAAS,SAAA"}
1
+ {"version":3,"file":"verdict.d.mts","names":[],"sources":["../../src/ci/verdict.ts"],"mappings":";;;;;;;;AA0BA;;;;;AASA;;;;;;;;;;;;;;AAQA;cAjBa,wBAAA;;;;;;;;cASA,YAAA,EAAY,CAAA,CAAA,OAAA;;;;;;;;KAQb,YAAA,GAAe,CAAA,CAAE,KAAA,QAAa,YAAA;;;;;AAc1C;cAPa,iBAAA,EAAiB,CAAA,CAAA,OAAA;;;;;;;KAOlB,iBAAA,GAAoB,CAAA,CAAE,KAAA,QAAa,iBAAA;AAS/C;;;;;;;AAAA,cAAa,aAAA,EAAa,CAAA,CAAA,SAAA;;;;;;;;;;KAId,aAAA,GAAgB,CAAA,CAAE,KAAA,QAAa,aAAA;;iBAG3B,WAAA,CAAY,OAAA,WAAkB,aAAA;;cAKjC,SAAA,EAAW,aAAA;AAAA,cAEX,SAAA,EAAS,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAgBV,SAAA,GAAY,CAAA,CAAE,KAAA,QAAa,SAAA;;;;;;;iBAgBvB,qBAAA,CAAsB,OAAA,EAAS,SAAA"}
@@ -12,6 +12,12 @@ import { z } from "zod";
12
12
  * The same payload is meant to render to the PR comment and to surface in the
13
13
  * MCP run object; the surfacing itself is owned by #3106/#3105, so this module
14
14
  * defines and validates the contract they consume, plus a reference renderer.
15
+ *
16
+ * Copy rule (ADR-0003): every string a verdict carries — `reason`, the action
17
+ * `detail`s — renders verbatim in the PR comment. Hold it to
18
+ * `.claude/rules/writing.md`: one point per field, no hedging, no
19
+ * self-narration, no restating the condition. The terse-voice test in
20
+ * `verdict.test.ts` is the reference copy the analyzer conforms to.
15
21
  */
16
22
  /**
17
23
  * Bump only on a *breaking* change to the shape below, so a consumer can refuse
@@ -94,8 +100,15 @@ const ACTION_LABEL = {
94
100
  */
95
101
  function renderVerdictMarkdown(verdict) {
96
102
  const parts = [`**${verdict.condition}** — ${verdict.reason}`];
97
- if (verdict.nextStep.kind !== "none") parts.push(`**Next step (${ACTION_LABEL[verdict.nextStep.kind]}):** ${verdict.nextStep.detail}`);
98
- if (verdict.triageAction.kind !== "none") parts.push(`**Triage (${ACTION_LABEL[verdict.triageAction.kind]}):** ${verdict.triageAction.detail}`);
103
+ const hasNextStep = verdict.nextStep.kind !== "none";
104
+ const hasTriage = verdict.triageAction.kind !== "none";
105
+ if (hasNextStep && hasTriage) parts.push([
106
+ "**Next step — do one of:**",
107
+ `- **${ACTION_LABEL[verdict.nextStep.kind]}:** ${verdict.nextStep.detail}`,
108
+ `- **${ACTION_LABEL[verdict.triageAction.kind]}:** ${verdict.triageAction.detail}`
109
+ ].join("\n"));
110
+ else if (hasNextStep) parts.push(`**Next step (${ACTION_LABEL[verdict.nextStep.kind]}):** ${verdict.nextStep.detail}`);
111
+ else if (hasTriage) parts.push(`**Triage (${ACTION_LABEL[verdict.triageAction.kind]}):** ${verdict.triageAction.detail}`);
99
112
  if (verdict.suggestedMcpTools.length > 0) {
100
113
  const tools = verdict.suggestedMcpTools.map((t) => `\`${t}\``).join(", ");
101
114
  parts.push(`**MCP tools:** ${tools}`);
@@ -1 +1 @@
1
- {"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\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 if (verdict.nextStep.kind !== \"none\") {\n parts.push(\n `**Next step (${ACTION_LABEL[verdict.nextStep.kind]}):** ${verdict.nextStep.detail}`,\n );\n }\n if (verdict.triageAction.kind !== \"none\") {\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":";;;;;;;;;;;;;;;;;;;;AAoBA,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;AACxE,KAAI,QAAQ,SAAS,SAAS,OAC5B,OAAM,KACJ,gBAAgB,aAAa,QAAQ,SAAS,MAAM,OAAO,QAAQ,SAAS,SAC7E;AAEH,KAAI,QAAQ,aAAa,SAAS,OAChC,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"}
1
+ {"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 * `.claude/rules/writing.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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@query-doctor/core",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "private": false,
5
5
  "description": "Core logic for Query Doctor",
6
6
  "license": "",