import { PolicyV2 } from '@nihal1983/core'; import { JiraClient } from '@nihal1983/context-gatherer'; import { Checker, PRContext, Violation } from '../types'; /** * TicketChecker validates ticket requirements from CODE-POLICY.md */ export class TicketChecker implements Checker { name = 'TicketChecker'; constructor( private policy: PolicyV2, private jiraClient?: JiraClient ) {} async check(context: PRContext): Promise { const violations: Violation[] = []; const ticketReq = this.policy.ticket_requirements; if (!ticketReq) { return violations; // No ticket requirements defined } // Check 1: Ticket required if (ticketReq.required && !context.ticket) { violations.push({ ruleId: 'ticket-required', severity: 'block', message: `PR must be linked to a ticket matching pattern: ${ticketReq.pattern}`, location: 'PR description', suggestion: `Add ticket ID to PR description (e.g., "Fixes PB-123")`, reference: 'CODE-POLICY.md: ticket_requirements.required' }); return violations; // Can't check other requirements without ticket } if (!context.ticket) { return violations; // No ticket but not required } // Check 2: Conditional requirements - Performance tickets if (ticketReq.performance) { const shouldCheck = this.shouldCheckConditional( context, ticketReq.performance.triggers ); if (shouldCheck) { const perfViolations = this.checkRequiredFields( context, ticketReq.performance.required_fields, ticketReq.performance.enforcement, ticketReq.performance.message || 'Performance ticket missing required fields' ); violations.push(...perfViolations); } } // Check 3: Conditional requirements - Security tickets if (ticketReq.security) { const shouldCheck = this.shouldCheckConditional( context, ticketReq.security.triggers ); if (shouldCheck) { const secViolations = this.checkRequiredFields( context, ticketReq.security.required_fields, ticketReq.security.enforcement, ticketReq.security.message || 'Security ticket missing required fields' ); violations.push(...secViolations); } } return violations; } /** * Check if conditional requirement applies */ private shouldCheckConditional( context: PRContext, triggers: Array<{ ticket_type?: string; branch_pattern?: string; label?: string; file_pattern?: string }> ): boolean { const ticket = context.ticket!; return triggers.some(trigger => { // Check ticket type if (trigger.ticket_type && ticket.type === trigger.ticket_type) { return true; } // Check branch pattern if (trigger.branch_pattern) { const regex = new RegExp(trigger.branch_pattern); if (regex.test(context.pr.branch)) { return true; } } // Check label (if available in context) if (trigger.label) { // TODO: Implement label checking when we add label support to PRContext } // Check file pattern if (trigger.file_pattern) { const regex = new RegExp(trigger.file_pattern); if (context.files.some(f => regex.test(f.path))) { return true; } } return false; }); } /** * Check if ticket has required fields */ private checkRequiredFields( context: PRContext, requiredFields: string[], enforcement: string, message: string ): Violation[] { const violations: Violation[] = []; const ticket = context.ticket!; if (!this.jiraClient) { // Can't check fields without Jira client return violations; } const fieldCheck = this.jiraClient.hasRequiredFields(ticket, requiredFields); if (!fieldCheck.hasAll) { violations.push({ ruleId: 'ticket-missing-fields', severity: enforcement.toLowerCase() as 'block' | 'warn', message: `${message}: Missing fields - ${fieldCheck.missing.join(', ')}`, location: `Ticket ${ticket.key}`, suggestion: `Add missing fields to ticket: ${fieldCheck.missing.join(', ')}`, reference: 'CODE-POLICY.md: ticket_requirements' }); } return violations; } }