/** * HardRuleEngine - 硬规则质控引擎 * * 功能: * - 基于 JSON Logic 执行质控规则 * - 支持纳入标准、排除标准、变量范围检查 * - 返回结构化的质控结果 * * 设计原则: * - 零容忍:规则判断是确定性的,不依赖 AI 猜测 * - 可追溯:每条规则执行结果都有详细记录 * - 高性能:纯逻辑计算,无 LLM 调用 */ import jsonLogic from 'json-logic-js'; import { PrismaClient } from '@prisma/client'; import { logger } from '../../../common/logging/index.js'; const prisma = new PrismaClient(); // ============================================================ // HardRuleEngine 实现 // ============================================================ export class HardRuleEngine { constructor(projectId) { this.rules = []; this.fieldMappings = new Map(); this.projectId = projectId; } /** * 初始化引擎(加载规则和字段映射) * * @param formName 可选,按表单名过滤规则(用于单表实时质控) */ async initialize(formName) { // 1. 加载质控规则 const skill = await prisma.iitSkill.findFirst({ where: { projectId: this.projectId, skillType: 'qc_process', isActive: true } }); if (!skill) { throw new Error(`No active QC rules found for project: ${this.projectId}`); } const config = skill.config; let allRules = config.rules || []; // ⭐ 如果指定了 formName,则只加载该表单相关的规则 // 规则通过 formName 或 field 字段来判断所属表单 if (formName) { allRules = allRules.filter((rule) => { // 优先使用规则中的 formName 字段 if (rule.formName) { return rule.formName === formName; } // 如果规则没有 formName,则默认包含(兼容旧规则) // TODO: 后续可以通过 field_metadata 表来判断字段所属表单 return true; }); } this.rules = allRules; logger.info('[HardRuleEngine] Rules loaded', { projectId: this.projectId, ruleCount: this.rules.length }); // 2. 加载字段映射 const mappings = await prisma.iitFieldMapping.findMany({ where: { projectId: this.projectId } }); for (const m of mappings) { this.fieldMappings.set(m.aliasName, m.actualName); } logger.info('[HardRuleEngine] Field mappings loaded', { mappingCount: this.fieldMappings.size }); } /** * 执行质控检查 * * @param recordId 记录ID * @param data 记录数据(REDCap 格式) * @returns 质控结果 */ execute(recordId, data) { const startTime = Date.now(); const results = []; const errors = []; const warnings = []; // 1. 数据预处理:应用字段映射 const normalizedData = this.normalizeData(data); // 2. 执行每条规则 for (const rule of this.rules) { const result = this.executeRule(rule, normalizedData); results.push(result); if (!result.passed) { if (result.severity === 'error') { errors.push(result); } else if (result.severity === 'warning') { warnings.push(result); } } } // 3. 计算整体状态 let overallStatus = 'PASS'; if (errors.length > 0) { overallStatus = 'FAIL'; } else if (warnings.length > 0) { overallStatus = 'WARNING'; } const duration = Date.now() - startTime; logger.info('[HardRuleEngine] QC completed', { recordId, overallStatus, totalRules: this.rules.length, errors: errors.length, warnings: warnings.length, duration: `${duration}ms` }); return { recordId, projectId: this.projectId, timestamp: new Date().toISOString(), overallStatus, summary: { totalRules: this.rules.length, passed: results.filter(r => r.passed).length, failed: errors.length, warnings: warnings.length }, results, errors, warnings }; } /** * 批量质控检查 * * @param records 记录数组 * @returns 质控结果数组 */ executeBatch(records) { return records.map(r => this.execute(r.recordId, r.data)); } /** * 执行单条规则 * * V2.1 优化:生成自包含的 LLM 友好消息 */ executeRule(rule, data) { try { // 获取字段值 const fieldValue = this.getFieldValue(rule.field, data); // 执行 JSON Logic const passed = jsonLogic.apply(rule.logic, data); // V2.1: 解析期望值(从 JSON Logic 中提取) const expectedValue = this.extractExpectedValue(rule.logic); const expectedCondition = this.describeLogic(rule.logic); // V2.1: 构建自包含的 LLM 友好消息 const llmMessage = passed ? '通过' : this.buildLlmMessage(rule, fieldValue, expectedValue); // V2.1: 构建结构化证据 const evidence = { value: fieldValue, threshold: expectedValue, unit: rule.metadata?.unit, }; return { ruleId: rule.id, ruleName: rule.name, field: rule.field, passed, message: passed ? '通过' : rule.message, llmMessage, severity: rule.severity, category: rule.category, actualValue: fieldValue, expectedValue, expectedCondition, evidence, }; } catch (error) { logger.error('[HardRuleEngine] Rule execution error', { ruleId: rule.id, error: error.message }); return { ruleId: rule.id, ruleName: rule.name, field: rule.field, passed: false, message: `规则执行出错: ${error.message}`, llmMessage: `规则执行出错: ${error.message}`, severity: 'error', category: rule.category }; } } /** * V2.1: 从 JSON Logic 中提取期望值 */ extractExpectedValue(logic) { const operator = Object.keys(logic)[0]; const args = logic[operator]; switch (operator) { case '>=': case '<=': case '>': case '<': case '==': case '!=': return String(args[1]); case 'and': // 对于 and 逻辑,尝试提取范围 const values = args.map((a) => this.extractExpectedValue(a)).filter(Boolean); if (values.length === 2) { return `${values[0]}-${values[1]}`; } return values.join(', '); case '!!': return '非空/必填'; default: return ''; } } /** * V2.1: 构建 LLM 友好的自包含消息 * * 格式:当前 **{actualValue}** (标准: {expectedValue}) */ buildLlmMessage(rule, actualValue, expectedValue) { const fieldName = Array.isArray(rule.field) ? rule.field.join(', ') : rule.field; const displayValue = actualValue !== undefined && actualValue !== null && actualValue !== '' ? `**${actualValue}**` : '**空**'; // 根据规则类别生成不同的消息格式 switch (rule.category) { case 'inclusion': return `**${rule.name}**: 当前值 ${displayValue} (入排标准: ${expectedValue || rule.message})`; case 'exclusion': return `**${rule.name}**: 当前值 ${displayValue} 触发排除条件`; case 'lab_values': return `**${rule.name}**: 当前值 ${displayValue} (正常范围: ${expectedValue})`; case 'logic_check': return `**${rule.name}**: \`${fieldName}\` = ${displayValue} (要求: ${expectedValue || '非空'})`; default: return `**${rule.name}**: 当前值 ${displayValue} (标准: ${expectedValue || rule.message})`; } } /** * 获取字段值(支持映射) */ getFieldValue(field, data) { if (Array.isArray(field)) { return field.map(f => data[f]); } // 先尝试直接获取 if (data[field] !== undefined) { return data[field]; } // 再尝试通过映射获取 const mappedField = this.fieldMappings.get(field); if (mappedField && data[mappedField] !== undefined) { return data[mappedField]; } return undefined; } /** * 数据标准化(应用字段映射,转换类型) */ normalizeData(data) { const normalized = { ...data }; // 1. 应用字段映射(反向映射:actualName -> aliasName) for (const [alias, actual] of this.fieldMappings.entries()) { if (data[actual] !== undefined && normalized[alias] === undefined) { normalized[alias] = data[actual]; } } // 2. 类型转换(字符串数字转数字) for (const [key, value] of Object.entries(normalized)) { if (typeof value === 'string' && value !== '' && !isNaN(Number(value))) { normalized[key] = Number(value); } } return normalized; } /** * 描述 JSON Logic 表达式(用于报告) */ describeLogic(logic) { const operator = Object.keys(logic)[0]; const args = logic[operator]; switch (operator) { case '>=': return `>= ${args[1]}`; case '<=': return `<= ${args[1]}`; case '>': return `> ${args[1]}`; case '<': return `< ${args[1]}`; case '==': return `= ${args[1]}`; case '!=': return `≠ ${args[1]}`; case 'and': return args.map((a) => this.describeLogic(a)).join(' 且 '); case 'or': return args.map((a) => this.describeLogic(a)).join(' 或 '); case '!!': return '非空'; default: return JSON.stringify(logic); } } /** * 获取规则列表 */ getRules() { return this.rules; } /** * 获取规则统计 */ getRuleStats() { const byCategory = {}; const bySeverity = {}; for (const rule of this.rules) { byCategory[rule.category] = (byCategory[rule.category] || 0) + 1; bySeverity[rule.severity] = (bySeverity[rule.severity] || 0) + 1; } return { total: this.rules.length, byCategory, bySeverity }; } } // ============================================================ // 工厂函数 // ============================================================ /** * 创建并初始化 HardRuleEngine * * @param projectId 项目ID * @param formName 可选,按表单名过滤规则(用于单表实时质控) * 如果不传,则加载所有规则(用于全案批量质控) */ export async function createHardRuleEngine(projectId, formName) { const engine = new HardRuleEngine(projectId); await engine.initialize(formName); return engine; }