/** * Triage Validator * Validates classification output against DB picklists. * Ensures issueType/subIssueType parent-child relationships are correct, * priority exists, queue exists, etc. */ import { postgresClient } from './postgres-client'; import { ClassificationResult, ValidationResult, ValidationError, } from '../types/workflow'; // Cache TTL for picklist data (10 minutes) const PICKLIST_CACHE_TTL_MS = 10 * 60 * 1000; interface PicklistCache { issueTypes: Set; subIssueTypes: Map; // value → parent_value priorities: Set; queues: Set; ticketCategories: Set; loadedAt: number; } export class TriageValidator { private cache: PicklistCache | null = null; /** * Load picklist data from DB into cache. */ private async loadPicklists(forceRefresh = false): Promise { const now = Date.now(); if (!forceRefresh && this.cache && now - this.cache.loadedAt < PICKLIST_CACHE_TTL_MS) { return this.cache; } const [issueTypes, subIssueTypes, priorities, queues, ticketCategories] = await Promise.all([ postgresClient.query(`SELECT value FROM issue_types WHERE is_deleted = false`), postgresClient.query(`SELECT value, parent_value FROM sub_issue_types WHERE is_deleted = false`), postgresClient.query(`SELECT value FROM priorities WHERE is_active = true`), postgresClient.query(`SELECT value FROM queues WHERE is_active = true`), postgresClient.query(`SELECT value FROM ticket_categories WHERE is_active = true`), ]); this.cache = { issueTypes: new Set(issueTypes.rows.map((r: any) => Number(r.value))), subIssueTypes: new Map( subIssueTypes.rows.map((r: any) => [Number(r.value), Number(r.parent_value)]) ), priorities: new Set(priorities.rows.map((r: any) => Number(r.value))), queues: new Set(queues.rows.map((r: any) => Number(r.value))), ticketCategories: new Set(ticketCategories.rows.map((r: any) => Number(r.value))), loadedAt: now, }; console.log( `[VALIDATOR] Loaded picklists: ${this.cache.issueTypes.size} issueTypes, ` + `${this.cache.subIssueTypes.size} subIssueTypes, ${this.cache.priorities.size} priorities, ` + `${this.cache.queues.size} queues, ${this.cache.ticketCategories.size} ticketCategories` ); return this.cache; } /** * Validate a classification result against DB picklists. */ async validate(classification: ClassificationResult): Promise { const picklists = await this.loadPicklists(); const errors: ValidationError[] = []; // Validate issue type if (classification.issue_classification) { const issueTypeValue = Number(classification.issue_classification.value); if (!picklists.issueTypes.has(issueTypeValue)) { errors.push({ field: 'issue_type', message: `Issue type ${issueTypeValue} not found in picklist`, value: issueTypeValue, }); } // Validate sub-issue type if (classification.issue_classification.value_2 != null) { const subIssueTypeValue = Number(classification.issue_classification.value_2); if (!picklists.subIssueTypes.has(subIssueTypeValue)) { errors.push({ field: 'sub_issue_type', message: `Sub-issue type ${subIssueTypeValue} not found in picklist`, value: subIssueTypeValue, }); } else { // Validate parent-child relationship const parentValue = picklists.subIssueTypes.get(subIssueTypeValue); if (parentValue !== issueTypeValue) { errors.push({ field: 'sub_issue_type', message: `Sub-issue type ${subIssueTypeValue} has parent ${parentValue}, but issue type is ${issueTypeValue}`, value: subIssueTypeValue, }); } } } } // Validate priority if (classification.priority) { const priorityValue = Number(classification.priority.value); if (!picklists.priorities.has(priorityValue)) { errors.push({ field: 'priority', message: `Priority ${priorityValue} not found in picklist`, value: priorityValue, }); } } // Validate queue if (classification.queue) { const queueValue = Number(classification.queue.value); if (!picklists.queues.has(queueValue)) { errors.push({ field: 'queue_id', message: `Queue ${queueValue} not found in picklist`, value: queueValue, }); } } if (errors.length > 0) { console.warn(`[VALIDATOR] Validation failed with ${errors.length} error(s):`, errors); } return { is_valid: errors.length === 0, errors, }; } /** * Force refresh the picklist cache. */ async refreshCache(): Promise { await this.loadPicklists(true); } } // Export singleton instance export const triageValidator = new TriageValidator();