Files
AIclinicalresearch/backend/scripts/regression_hardrule_guards_by_project.ts
HaHafeng a666649fd4 feat(iit): harden QC pipeline consistency and release artifacts
Implement IIT quality workflow hardening across eQuery deduplication, guard metadata validation, timeline/readability improvements, and chat evidence fallbacks, then synchronize release and development documentation for deployment handoff.

Includes migration/scripts for open eQuery dedupe guards, orchestration/status semantics, report/tool readability fixes, and updated module status plus deployment checklist.

Made-with: Cursor
2026-03-08 21:54:35 +08:00

120 lines
4.3 KiB
TypeScript

import assert from 'node:assert';
import { PrismaClient } from '@prisma/client';
import { HardRuleEngine, type QCRule } from '../src/modules/iit-manager/engines/HardRuleEngine.js';
const prisma = new PrismaClient();
type Status = 'PASS' | 'FAIL' | 'WARNING';
function fieldsOf(rule: QCRule): string[] {
return Array.isArray(rule.field) ? rule.field : [rule.field];
}
function findRule(rules: QCRule[], patterns: RegExp[]): QCRule | null {
return rules.find((r) => patterns.some((p) => p.test(r.name || ''))) || null;
}
function findRuleByGuardType(rules: QCRule[], guardType: string): QCRule | null {
return rules.find((r) => String((r.metadata as any)?.guardType || '') === guardType) || null;
}
function runCase(name: string, rule: QCRule, data: Record<string, any>, expectedStatus: Status) {
const engine = new HardRuleEngine('regression-project');
const result = engine.executeWithRules('R1', data, [rule]);
assert.strictEqual(result.overallStatus, expectedStatus, `${name} 期望 ${expectedStatus},实际 ${result.overallStatus}`);
console.log(`${name}: ${result.overallStatus}`);
}
async function main() {
const projectId = process.argv[2];
if (!projectId) {
throw new Error('Usage: npx tsx scripts/regression_hardrule_guards_by_project.ts <projectId>');
}
const skill = await prisma.iitSkill.findFirst({
where: { projectId, skillType: 'qc_process', isActive: true },
select: { config: true },
});
const rules = (((skill?.config as any)?.rules || []) as QCRule[]);
if (rules.length === 0) {
throw new Error(`项目 ${projectId} 未找到可用 qc_process 规则`);
}
const dateRule = findRuleByGuardType(rules, 'date_not_before_or_equal');
const assessRule = findRuleByGuardType(rules, 'skip_if_any_missing');
const inclusionRule = findRuleByGuardType(rules, 'pass_if_all_ones');
const exclusionRule = findRuleByGuardType(rules, 'pass_if_exclusion_all_zero');
const skipped: string[] = [];
if (dateRule) {
const f = fieldsOf(dateRule);
if (f.length >= 2) {
runCase('同日访视不应误判早于', dateRule, { [f[0]]: '2024-03-27', [f[1]]: '2024-03-27' }, 'PASS');
} else {
skipped.push('dateRule(字段数不足2)');
}
} else {
const legacy = findRule(rules, [/访视日期.*知情同意/i, /早于知情同意/i]);
skipped.push(legacy ? 'dateRule(规则存在但未配置 guardType=date_not_before_or_equal)' : 'dateRule(未匹配)');
}
if (assessRule) {
const f = fieldsOf(assessRule);
if (f.length >= 2) {
runCase('评估日期缺失不应判不一致', assessRule, { [f[0]]: '', [f[1]]: '2024-03-27' }, 'PASS');
} else {
skipped.push('assessRule(字段数不足2)');
}
} else {
const legacy = findRule(rules, [/SF-?MPQ.*CMSS.*不一致/i, /评估日期.*访视日期.*不一致/i]);
skipped.push(legacy ? 'assessRule(规则存在但未配置 guardType=skip_if_any_missing)' : 'assessRule(未匹配)');
}
if (inclusionRule) {
const f = fieldsOf(inclusionRule);
if (f.length >= 2) {
const payload: Record<string, any> = {};
for (const field of f) payload[field] = 1;
runCase('纳入标准全1应通过', inclusionRule, payload, 'PASS');
} else {
skipped.push('inclusionRule(字段数不足2)');
}
} else {
const legacy = findRule(rules, [/所有纳入标准.*检查/i, /纳入标准.*满足/i]);
skipped.push(legacy ? 'inclusionRule(规则存在但未配置 guardType=pass_if_all_ones)' : 'inclusionRule(未匹配)');
}
if (exclusionRule) {
const f = fieldsOf(exclusionRule);
if (f.length >= 2) {
const payload: Record<string, any> = {};
payload[f[0]] = 1;
for (const field of f.slice(1)) payload[field] = 0;
runCase('排除标准全0应通过', exclusionRule, payload, 'PASS');
} else {
skipped.push('exclusionRule(字段数不足2)');
}
} else {
const legacy = findRule(rules, [/入组状态.*排除标准.*冲突/i]);
skipped.push(legacy ? 'exclusionRule(规则存在但未配置 guardType=pass_if_exclusion_all_zero)' : 'exclusionRule(未匹配)');
}
console.log(JSON.stringify({
projectId,
totalRules: rules.length,
skipped,
}, null, 2));
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});