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
120 lines
4.3 KiB
TypeScript
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();
|
|
});
|
|
|