import type { AnalysisResult, EngineBackend, QualityBackendConfig, WebhookEvent, } from '../types.js'; // Conventional commit prefixes const CONVENTIONAL_COMMIT = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?:\s/i; const WIP_PATTERN = /\b(wip|work.in.progress|do.not.merge|don't.merge|draft)\b/i; const BREAKING_PATTERN = /\b(breaking.change|BREAKING)\b/i; const TODO_PATTERN = /\b(TODO|FIXME|HACK|XXX|TEMP)\b/; export class QualityBackend implements EngineBackend { name = 'quality'; constructor(private config: QualityBackendConfig) {} async analyze(event: WebhookEvent): Promise { const body = event.body.trim(); const title = event.title.trim(); const signals: { name: string; positive: boolean; weight: number }[] = []; // --- Title analysis --- if (title.length < 10) { signals.push({ name: 'very short title', positive: false, weight: 1.2 }); } else if (title.length > 200) { signals.push({ name: 'excessively long title', positive: false, weight: 0.5, }); } if (CONVENTIONAL_COMMIT.test(title)) { signals.push({ name: 'conventional commit format', positive: true, weight: 1, }); } if (WIP_PATTERN.test(title) || WIP_PATTERN.test(body)) { signals.push({ name: 'marked as work-in-progress', positive: false, weight: 1.5, }); } // --- Body analysis --- if (body.length === 0) { signals.push({ name: 'empty description', positive: false, weight: 2 }); } else if (body.length < this.config.minBodyLength) { signals.push({ name: `short description (${body.length} chars)`, positive: false, weight: 1.2, }); } else if (body.length >= this.config.minBodyLength) { signals.push({ name: 'adequate description', positive: true, weight: 1 }); if (body.length > 300) { signals.push({ name: 'thorough description', positive: true, weight: 0.5, }); } } if (/```[\s\S]*?```/.test(body)) { signals.push({ name: 'has code blocks', positive: true, weight: 0.7 }); } if (/^#{1,6}\s/m.test(body)) { signals.push({ name: 'has section headers', positive: true, weight: 0.8, }); } // Checklists const checklistItems = body.match(/^[\s]*-\s*\[[ x]\]/gm); if (checklistItems) { const checked = checklistItems.filter((i) => /\[x\]/i.test(i)).length; const total = checklistItems.length; if (total > 0 && checked === total) { signals.push({ name: `checklist complete (${total}/${total})`, positive: true, weight: 1, }); } else if (total > 0) { signals.push({ name: `checklist incomplete (${checked}/${total})`, positive: false, weight: 0.8, }); } } // Breaking changes if (BREAKING_PATTERN.test(title) || BREAKING_PATTERN.test(body)) { // Not inherently positive or negative, but we flag it for visibility. // If there's a description of the breaking change, it's better. if (body.length > 100 && BREAKING_PATTERN.test(body)) { signals.push({ name: 'breaking change documented', positive: true, weight: 0.8, }); } else { signals.push({ name: 'breaking change mentioned but not detailed', positive: false, weight: 0.8, }); } } // TODOs/FIXMEs in description suggest unfinished work const todoMatches = body.match(TODO_PATTERN); if (todoMatches) { signals.push({ name: `unfinished markers in description (${todoMatches.length})`, positive: false, weight: 0.6, }); } // --- Type-specific signals --- if (event.type === 'issue') { if (/\b(steps?\s+to\s+reproduce|reproduction|repro\s+steps?)\b/i.test(body)) { signals.push({ name: 'has reproduction steps', positive: true, weight: 1.3, }); } if (/\b(expected|actual)\s+(behavior|behaviour|result|output)\b/i.test(body)) { signals.push({ name: 'has expected/actual behavior', positive: true, weight: 1.2, }); } if ( /\b(version|environment|os|platform|browser|node|python|java|rust|go)\s*[:\d]/i.test(body) ) { signals.push({ name: 'has environment details', positive: true, weight: 1, }); } if (/\b(stack\s*trace|traceback|error|exception|panic)\b/i.test(body)) { signals.push({ name: 'includes error output', positive: true, weight: 0.8, }); } // Template usage detection (common issue template markers) if (/\b(describe the bug|feature request|is your feature request related to)\b/i.test(body)) { signals.push({ name: 'uses issue template', positive: true, weight: 0.6, }); } } if (event.type === 'pull_request') { if (/\b(fix(es)?|clos(es|ing)|resolv(es|ing))\s+#\d+/i.test(body)) { signals.push({ name: 'links to issue', positive: true, weight: 1.3 }); } if (/\b(test\s*(plan|strategy|coverage)|how\s+to\s+test|testing|tested\s+by)\b/i.test(body)) { signals.push({ name: 'has test plan', positive: true, weight: 1.2 }); } // Migration or upgrade guide if (/\b(migration|upgrade|breaking).*(guide|instruction|step)/i.test(body)) { signals.push({ name: 'has migration guide', positive: true, weight: 1, }); } // Before/after comparison if (/\b(before|after)\b/i.test(body) && /\b(before|after)\b/gi.test(body)) { const beforeAfter = body.match(/\b(before|after)\b/gi); if (beforeAfter && beforeAfter.length >= 2) { signals.push({ name: 'has before/after comparison', positive: true, weight: 0.7, }); } } } // Shared: references to other issues/PRs const refs = body.match(/#\d+/g); if (refs && refs.length > 0) { signals.push({ name: `references ${refs.length} issue(s)/PR(s)`, positive: true, weight: 0.6, }); } // Screenshots or images if (/!\[.*\]\(.*\)/.test(body) || / s.positive).reduce((s, x) => s + x.weight, 0); const negativeWeight = signals.filter((s) => !s.positive).reduce((s, x) => s + x.weight, 0); let impact: AnalysisResult['impact']; if (positiveWeight > negativeWeight * 1.2) { impact = 'positive'; } else if (negativeWeight > positiveWeight * 1.2) { impact = 'negative'; } else { impact = 'neutral'; } const totalWeight = positiveWeight + negativeWeight; const confidence = Math.min( 1, (Math.abs(positiveWeight - negativeWeight) / Math.max(totalWeight, 1)) * 0.5 + 0.2 ); const reasoning = `Quality: ${signals.map((s) => `${s.positive ? '+' : '-'} ${s.name}`).join(', ')}.`; return { impact, confidence, reasoning }; } }