wulf-pulse/lib/services/triage-validator.ts

151 lines
4.9 KiB
TypeScript

/**
* 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<number>;
subIssueTypes: Map<number, number>; // value → parent_value
priorities: Set<number>;
queues: Set<number>;
ticketCategories: Set<number>;
loadedAt: number;
}
export class TriageValidator {
private cache: PicklistCache | null = null;
/**
* Load picklist data from DB into cache.
*/
private async loadPicklists(forceRefresh = false): Promise<PicklistCache> {
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<ValidationResult> {
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<void> {
await this.loadPicklists(true);
}
}
// Export singleton instance
export const triageValidator = new TriageValidator();