QC System Deep Fix: - HardRuleEngine: add null tolerance + field availability pre-check (skipped status) - SkillRunner: baseline data merge for follow-up events + field availability check - QcReportService: record-level pass rate calculation + accurate LLM XML report - iitBatchController: legacy log cleanup (eventId=null) + upsert RecordSummary - seed-iit-qc-rules: null/empty string tolerance + applicableEvents config V3.1 Architecture Design (docs only, no code changes): - QC engine V3.1 plan: 5-level data structure (CDISC ODM) + D1-D7 dimensions - Three-batch implementation strategy (A: foundation, B: bubbling, C: new engines) - Architecture team review: 4 whitepapers reviewed + feedback doc + 4 critical suggestions - CRA Agent strategy roadmap + CRA 4-tool explanation doc for clinical experts Project Member Management: - Cross-tenant member search and assignment (remove tenant restriction) - IIT project detail page enhancement with tabbed layout (KB + members) - IitProjectContext for business-side project selection - System-KB route access control adjustment for project operators Frontend: - AdminLayout sidebar menu restructure - IitLayout with project context provider - IitMemberManagePage new component - Business-side pages adapt to project context Prisma: - 2 new migrations (user-project RBAC + is_demo flag) - Schema updates for project member management Made-with: Cursor
921 lines
41 KiB
Markdown
921 lines
41 KiB
Markdown
# 质控引擎 V3.1 架构升级 — 五级数据结构与多维报告开发计划
|
||
|
||
> **版本**:V1.1(合并架构团队二次评审 4 条关键建议)
|
||
> **日期**:2026-03-01
|
||
> **定位**:基于架构团队评审意见,在 V3.0 三级数据结构基础上升级为 CDISC ODM 五级结构,分三批次落地
|
||
> **前置文档**:
|
||
> - [五层数据架构方案评审反馈](../../09-技术评审报告/五层数据架构方案评审反馈.md)(采纳/暂缓清单)
|
||
> - [CRA Agent 质控体系全景技术路径(策略评审稿)](../../00-系统设计/CRA%20Agent%20质控体系全景技术路径(策略评审稿).md)
|
||
> - [V3.0 三级数据结构技术设计](./质控引擎架构升级-三级数据结构与多模式触发技术设计.md)(历史版本,供参考)
|
||
> - 架构团队:核心数据架构与业务落地白皮书 / 核心转换机制白皮书 / Skill 化配置架构技术设计 / CRA 质控报告自动化生成与 LLM 友好型设计规范
|
||
>
|
||
> **V1.1 变更记录**:
|
||
> - 新增 3.3 节:状态优先级与 SKIPPED 处理
|
||
> - 新增 3.8 节:Record-Level Context(跨表单质控上下文全拉取)
|
||
> - 新增 3.9 节:eQuery 自动闭环(State Transition Hook)
|
||
> - 修正 6.3 节:D2 缺失率增加 Event-Aware 时序过滤
|
||
> - 修正 3.4 节:聚合防抖粒度从项目级细化为受试者级
|
||
|
||
---
|
||
|
||
## 1. V3.0 → V3.1 升级概要
|
||
|
||
### 1.1 为什么从三级升到五级
|
||
|
||
V3.0 设计的三级结构是 Record → Event → Field,足以覆盖常规表单。但架构团队指出了一个关键盲区:**REDCap 重复表单(Repeating Instruments)**。
|
||
|
||
一个患者可以在同一个访视下填写多条 AE(不良事件)、多次合并用药。没有 Form 层和 Instance 层,就无法精确定位"3 号受试者 → 随访 2 → AE 表 → 第 2 条 AE → 事件名称字段"。
|
||
|
||
### 1.2 五级坐标体系
|
||
|
||
```
|
||
Record(受试者)
|
||
└── Event(访视/事件)
|
||
└── Form(表单) ← 新增
|
||
└── Instance(实例) ← 新增(核心突破)
|
||
└── Field(变量/字段)
|
||
```
|
||
|
||
每一个质控状态、每一条 eQuery,都必须绑定在这个五维坐标上,坐标不完整不落盘。
|
||
|
||
### 1.3 与 V3.0 的主要差异
|
||
|
||
| 维度 | V3.0 | V3.1 |
|
||
|------|------|------|
|
||
| 数据层级 | 3 级(Record → Event → Field) | 5 级(+ Form + Instance) |
|
||
| `qc_field_status` 唯一键 | project × record × event × field | project × record × event × form × instance × field |
|
||
| 规则分类 | `inclusion`/`exclusion`/`lab_values`/`logic_check` | D1-D7 七大维度 |
|
||
| 字段语义化 | 无 | `IitFieldMapping.semanticLabel`(反向映射) |
|
||
| 冒泡机制 | 应用层逐级 UPDATE | 异步防抖聚合(避免并发死锁) |
|
||
| 报告结构 | 扁平单章 | 按 D1-D7 分章节 |
|
||
| REDCap InstanceID | 未处理 | `RedcapAdapter` 层强制标准化 |
|
||
|
||
---
|
||
|
||
## 2. 数据库设计
|
||
|
||
### 2.1 新增表:`qc_field_status`(变量级质控状态 — 五级坐标)
|
||
|
||
每个 **project × record × event × form × instance × field** 唯一一行,反映最新质控状态。
|
||
|
||
```sql
|
||
CREATE TABLE iit_schema.qc_field_status (
|
||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
project_id TEXT NOT NULL,
|
||
record_id TEXT NOT NULL,
|
||
event_id TEXT NOT NULL,
|
||
form_name TEXT NOT NULL, -- 表单名(如 ae_log, conmed_log)
|
||
instance_id INT NOT NULL DEFAULT 1, -- 实例编号(非重复表单 = 1)
|
||
field_name TEXT NOT NULL,
|
||
|
||
-- 质控结果
|
||
status TEXT NOT NULL, -- 'PASS' | 'FAIL' | 'WARNING'
|
||
rule_id TEXT,
|
||
rule_name TEXT,
|
||
rule_category TEXT, -- 'D1' | 'D2' | 'D3' | 'D5' | 'D6' | 'D7'
|
||
severity TEXT, -- 'critical' | 'warning' | 'info'
|
||
message TEXT,
|
||
actual_value TEXT,
|
||
expected_value TEXT,
|
||
|
||
-- 溯源
|
||
source_qc_log_id TEXT,
|
||
triggered_by TEXT NOT NULL, -- 'webhook' | 'cron' | 'manual'
|
||
last_qc_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
||
-- 唯一约束:五级坐标
|
||
CONSTRAINT uq_field_status
|
||
UNIQUE (project_id, record_id, event_id, form_name, instance_id, field_name),
|
||
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
-- 高频查询索引
|
||
CREATE INDEX idx_fs_record ON iit_schema.qc_field_status (project_id, record_id);
|
||
CREATE INDEX idx_fs_event ON iit_schema.qc_field_status (project_id, record_id, event_id);
|
||
CREATE INDEX idx_fs_fail ON iit_schema.qc_field_status (project_id, status) WHERE status IN ('FAIL', 'WARNING');
|
||
CREATE INDEX idx_fs_cat ON iit_schema.qc_field_status (project_id, rule_category);
|
||
```
|
||
|
||
### 2.2 新增表:`qc_event_status`(事件级质控状态)
|
||
|
||
每个 **project × record × event** 唯一一行,由 `qc_field_status` 聚合。
|
||
|
||
```sql
|
||
CREATE TABLE iit_schema.qc_event_status (
|
||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
project_id TEXT NOT NULL,
|
||
record_id TEXT NOT NULL,
|
||
event_id TEXT NOT NULL,
|
||
event_label TEXT,
|
||
|
||
-- 聚合状态
|
||
status TEXT NOT NULL, -- 最严重的子级状态
|
||
fields_total INT NOT NULL DEFAULT 0,
|
||
fields_passed INT NOT NULL DEFAULT 0,
|
||
fields_failed INT NOT NULL DEFAULT 0,
|
||
fields_warning INT NOT NULL DEFAULT 0,
|
||
|
||
-- 维度计数(方便多维报告直接读取)
|
||
d1_issues INT NOT NULL DEFAULT 0,
|
||
d2_issues INT NOT NULL DEFAULT 0,
|
||
d3_issues INT NOT NULL DEFAULT 0,
|
||
d5_issues INT NOT NULL DEFAULT 0,
|
||
d6_issues INT NOT NULL DEFAULT 0,
|
||
d7_issues INT NOT NULL DEFAULT 0,
|
||
|
||
-- 表单级摘要
|
||
forms_checked TEXT[] DEFAULT '{}',
|
||
top_issues JSONB DEFAULT '[]',
|
||
|
||
triggered_by TEXT NOT NULL,
|
||
last_qc_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
||
CONSTRAINT uq_event_status
|
||
UNIQUE (project_id, record_id, event_id),
|
||
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_es_record ON iit_schema.qc_event_status (project_id, record_id);
|
||
CREATE INDEX idx_es_fail ON iit_schema.qc_event_status (project_id, status) WHERE status IN ('FAIL', 'WARNING');
|
||
```
|
||
|
||
### 2.3 改造表:`record_summary`
|
||
|
||
在现有 `IitRecordSummary` 上新增聚合字段:
|
||
|
||
```sql
|
||
ALTER TABLE iit_schema.record_summary
|
||
ADD COLUMN IF NOT EXISTS events_total INT DEFAULT 0,
|
||
ADD COLUMN IF NOT EXISTS events_passed INT DEFAULT 0,
|
||
ADD COLUMN IF NOT EXISTS events_failed INT DEFAULT 0,
|
||
ADD COLUMN IF NOT EXISTS events_warning INT DEFAULT 0,
|
||
ADD COLUMN IF NOT EXISTS fields_total INT DEFAULT 0,
|
||
ADD COLUMN IF NOT EXISTS fields_passed INT DEFAULT 0,
|
||
ADD COLUMN IF NOT EXISTS fields_failed INT DEFAULT 0,
|
||
ADD COLUMN IF NOT EXISTS d1_issues INT DEFAULT 0,
|
||
ADD COLUMN IF NOT EXISTS d2_issues INT DEFAULT 0,
|
||
ADD COLUMN IF NOT EXISTS d3_issues INT DEFAULT 0,
|
||
ADD COLUMN IF NOT EXISTS d5_issues INT DEFAULT 0,
|
||
ADD COLUMN IF NOT EXISTS d6_issues INT DEFAULT 0,
|
||
ADD COLUMN IF NOT EXISTS d7_issues INT DEFAULT 0,
|
||
ADD COLUMN IF NOT EXISTS top_issues JSONB DEFAULT '[]';
|
||
```
|
||
|
||
### 2.4 改造表:`IitQcLog` + `IitEquery` 增加 Instance 层
|
||
|
||
```sql
|
||
ALTER TABLE iit_schema.qc_logs
|
||
ADD COLUMN IF NOT EXISTS form_name TEXT,
|
||
ADD COLUMN IF NOT EXISTS instance_id INT DEFAULT 1;
|
||
|
||
ALTER TABLE iit_schema.iit_equeries
|
||
ADD COLUMN IF NOT EXISTS instance_id INT DEFAULT 1;
|
||
```
|
||
|
||
### 2.5 改造表:`IitFieldMapping` 增加反向语义标签
|
||
|
||
```sql
|
||
ALTER TABLE iit_schema.field_mapping
|
||
ADD COLUMN IF NOT EXISTS semantic_label TEXT, -- 中文语义标签(如"谷丙转氨酶(ALT)")
|
||
ADD COLUMN IF NOT EXISTS form_name TEXT, -- 所属表单
|
||
ADD COLUMN IF NOT EXISTS rule_category TEXT; -- 所属维度 D1-D7
|
||
```
|
||
|
||
### 2.6 保留不变:`qc_logs`(审计日志)
|
||
|
||
继续作为追加型审计日志,每次质控执行新增一行,永不删改。新增 `form_name` 和 `instance_id` 字段后,日志也具备完整的五级坐标。
|
||
|
||
### 2.7 完整数据模型
|
||
|
||
```
|
||
REDCap 原始数据(5 层结构)
|
||
│
|
||
│ 质控引擎执行
|
||
▼
|
||
┌─────────────┐ ┌──────────────────┐
|
||
│ qc_logs │ │ qc_field_status │
|
||
│ (审计日志) │←ref─│ (变量级, 5层坐标) │
|
||
│ 追加型 │ │ + rule_category │
|
||
└─────────────┘ └────────┬─────────┘
|
||
│ 异步防抖聚合
|
||
┌────────▼─────────┐
|
||
│ qc_event_status │
|
||
│ (事件级) │
|
||
│ + d1..d7_issues │
|
||
└────────┬─────────┘
|
||
│ 异步防抖聚合
|
||
┌────────▼─────────┐
|
||
│ record_summary │
|
||
│ (记录级) │
|
||
│ + d1..d7_issues │
|
||
└────────┬─────────┘
|
||
│ 聚合
|
||
┌────────▼─────────┐ ┌──────────────┐
|
||
│ qc_project_stats │────►│ qc_reports │
|
||
│ (项目级) │ 生成 │ (LLM 报告) │
|
||
└──────────────────┘ └──────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 关键工程设计
|
||
|
||
### 3.1 REDCap InstanceID 标准化(RedcapAdapter 层洗线)
|
||
|
||
**问题**:REDCap API 对 `redcap_repeat_instance` 的返回值不一致:
|
||
- 非重复表单:字段不存在或为空字符串
|
||
- 重复表单第一行:有时为空字符串,有时为 `"1"`
|
||
- 重复表单后续行:`"2"`, `"3"`, ...
|
||
|
||
**解法**:在 `RedcapAdapter` 返回数据之前,强制标准化所有记录:
|
||
|
||
```typescript
|
||
// RedcapAdapter.ts — 新增 normalizeInstance() 方法
|
||
|
||
private normalizeInstances(records: RedcapRecord[]): NormalizedRecord[] {
|
||
return records.map(record => {
|
||
const formName = record.redcap_repeat_instrument || this.inferFormName(record);
|
||
let instanceId: number;
|
||
|
||
if (!record.redcap_repeat_instrument) {
|
||
// 非重复表单:强制 instanceId = 1
|
||
instanceId = 1;
|
||
} else {
|
||
// 重复表单:空或无效值强制为 1,否则取实际值
|
||
const raw = record.redcap_repeat_instance;
|
||
instanceId = (raw && !isNaN(Number(raw)) && Number(raw) > 0)
|
||
? Number(raw)
|
||
: 1;
|
||
}
|
||
|
||
return {
|
||
...record,
|
||
_normalized: {
|
||
recordId: String(record.record_id),
|
||
eventId: record.redcap_event_name || 'default',
|
||
formName,
|
||
instanceId,
|
||
},
|
||
};
|
||
});
|
||
}
|
||
```
|
||
|
||
**原则**:`RedcapAdapter` 之后的所有下游(引擎、状态表、报告)都使用标准化后的五级坐标,无需二次处理。
|
||
|
||
### 3.2 QcExecutor 重构(统一执行入口)
|
||
|
||
将分散在 `SkillRunner`、`iitBatchController`、`QcReportService` 中的执行逻辑统一收归:
|
||
|
||
```typescript
|
||
class QcExecutor {
|
||
|
||
/**
|
||
* 单记录单事件质控(实时触发 / AI 调用)
|
||
*/
|
||
async executeSingle(
|
||
projectId: string,
|
||
recordId: string,
|
||
eventId: string,
|
||
options?: { triggeredBy: 'webhook' | 'manual' }
|
||
): Promise<void> {
|
||
// 1. RedcapAdapter 拉取并标准化(含 InstanceID 洗线)
|
||
// 2. 加载适用规则(按 applicableEvents + applicableForms 过滤)
|
||
// 3. 逐 Form × Instance × Field 执行规则
|
||
// 4. 写入 qc_logs(追加审计)
|
||
// 5. upsert qc_field_status(五级坐标)
|
||
// 6. 标记需聚合(推入防抖队列,不立即冒泡)
|
||
}
|
||
|
||
/**
|
||
* 批量质控(定时 / 手动 / 一键全量)
|
||
*/
|
||
async executeBatch(
|
||
projectId: string,
|
||
options?: { triggeredBy: 'cron' | 'manual' }
|
||
): Promise<BatchResult> {
|
||
// 1. RedcapAdapter 拉取全量并标准化
|
||
// 2. 基线数据合并
|
||
// 3. 逐 record × event 复用 executeSingle 核心逻辑
|
||
// 4. 批量完成后触发一次聚合(而非逐条)
|
||
// 5. 刷新 qc_reports
|
||
}
|
||
|
||
/**
|
||
* 异步防抖聚合(解决冒泡并发死锁问题)
|
||
*/
|
||
async aggregateDeferred(projectId: string): Promise<void> {
|
||
// 详见 3.3 节
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.3 状态优先级与 SKIPPED 处理
|
||
|
||
聚合时始终取"最严重的状态":
|
||
|
||
```
|
||
FAIL (3) > WARNING (2) > UNCERTAIN (1) > PASS (0)
|
||
```
|
||
|
||
- **事件状态** = 其下所有变量状态中最严重的
|
||
- **记录状态** = 其下所有事件状态中最严重的
|
||
|
||
**SKIPPED 处理**:当某个变量在某个事件中不存在时(如随访期没有 age 字段),**不写入 `qc_field_status`**,不参与聚合计算。只有被规则实际检查过的变量才写入。
|
||
|
||
### 3.4 状态冒泡:异步防抖聚合方案
|
||
|
||
**问题**:如果批量执行 1000 条规则,每次 Field 变更都同步 UPDATE 上级,会导致数据库行锁冲突甚至死锁。
|
||
|
||
**解法**:冒泡不做同步级联,改为"批量完成后统一聚合"。
|
||
|
||
```
|
||
执行阶段(高频写入,无锁竞争):
|
||
HardRuleEngine 执行 1000 条规则
|
||
↓ 逐条 upsert
|
||
qc_field_status(各自独立行,无锁冲突)
|
||
|
||
聚合阶段(受试者级防抖,避免项目级锁竞争):
|
||
executeSingle 完成后
|
||
↓ 推入 pg-boss 防抖队列
|
||
singletonKey: `aggregate_${projectId}_${recordId}`
|
||
↓ 只重算该受试者的 event_status 和 record_summary
|
||
|
||
executeBatch 完成后
|
||
↓ 直接调用 aggregateDeferred(projectId)
|
||
↓ 一次性 SQL 聚合全项目
|
||
|
||
项目级统计(qc_project_stats)
|
||
↓ 由独立的低频定时任务刷新(如每 5 分钟)
|
||
↓ 或在 aggregateDeferred 末尾追加一次
|
||
```
|
||
|
||
```typescript
|
||
async aggregateDeferred(projectId: string): Promise<void> {
|
||
// 1. 事件级聚合:一条 SQL 搞定
|
||
await prisma.$executeRaw`
|
||
INSERT INTO iit_schema.qc_event_status
|
||
(id, project_id, record_id, event_id, status,
|
||
fields_total, fields_passed, fields_failed, fields_warning,
|
||
d1_issues, d2_issues, d3_issues, d5_issues, d6_issues, d7_issues,
|
||
triggered_by, last_qc_at, created_at, updated_at)
|
||
SELECT
|
||
gen_random_uuid(),
|
||
fs.project_id, fs.record_id, fs.event_id,
|
||
-- 最严重状态
|
||
CASE
|
||
WHEN COUNT(*) FILTER (WHERE fs.status = 'FAIL') > 0 THEN 'FAIL'
|
||
WHEN COUNT(*) FILTER (WHERE fs.status = 'WARNING') > 0 THEN 'WARNING'
|
||
ELSE 'PASS'
|
||
END,
|
||
COUNT(*),
|
||
COUNT(*) FILTER (WHERE fs.status = 'PASS'),
|
||
COUNT(*) FILTER (WHERE fs.status = 'FAIL'),
|
||
COUNT(*) FILTER (WHERE fs.status = 'WARNING'),
|
||
-- 维度计数
|
||
COUNT(*) FILTER (WHERE fs.rule_category = 'D1' AND fs.status = 'FAIL'),
|
||
COUNT(*) FILTER (WHERE fs.rule_category = 'D2' AND fs.status = 'FAIL'),
|
||
COUNT(*) FILTER (WHERE fs.rule_category = 'D3' AND fs.status = 'FAIL'),
|
||
COUNT(*) FILTER (WHERE fs.rule_category = 'D5' AND fs.status = 'FAIL'),
|
||
COUNT(*) FILTER (WHERE fs.rule_category = 'D6' AND fs.status = 'FAIL'),
|
||
COUNT(*) FILTER (WHERE fs.rule_category = 'D7' AND fs.status = 'FAIL'),
|
||
'manual', NOW(), NOW(), NOW()
|
||
FROM iit_schema.qc_field_status fs
|
||
WHERE fs.project_id = ${projectId}
|
||
GROUP BY fs.project_id, fs.record_id, fs.event_id
|
||
ON CONFLICT (project_id, record_id, event_id)
|
||
DO UPDATE SET
|
||
status = EXCLUDED.status,
|
||
fields_total = EXCLUDED.fields_total,
|
||
fields_passed = EXCLUDED.fields_passed,
|
||
fields_failed = EXCLUDED.fields_failed,
|
||
fields_warning = EXCLUDED.fields_warning,
|
||
d1_issues = EXCLUDED.d1_issues,
|
||
d2_issues = EXCLUDED.d2_issues,
|
||
d3_issues = EXCLUDED.d3_issues,
|
||
d5_issues = EXCLUDED.d5_issues,
|
||
d6_issues = EXCLUDED.d6_issues,
|
||
d7_issues = EXCLUDED.d7_issues,
|
||
updated_at = NOW()
|
||
`;
|
||
|
||
// 2. 记录级聚合:同理从 qc_event_status 聚合到 record_summary
|
||
// 3. 项目级聚合:从 record_summary 聚合到 qc_project_stats
|
||
}
|
||
```
|
||
|
||
**核心优势**:
|
||
- 执行阶段只写 `qc_field_status`,各行互不冲突,可高并发
|
||
- 聚合阶段用 SQL `INSERT ... ON CONFLICT` 一次性完成,无应用层循环
|
||
- 实时触发用受试者级防抖(`singletonKey: aggregate_${projectId}_${recordId}`),多 CRC 同时录入不同受试者时互不干扰
|
||
- 项目级统计(`qc_project_stats`)独立刷新,避免单行频繁锁竞争
|
||
|
||
### 3.5 规则分类体系(D1-D7 维度枚举)
|
||
|
||
扩展 `QCRule.category` 和 `IitSkill.skillType`:
|
||
|
||
| 维度代码 | 含义 | 对应规则类型 | 当前状态 |
|
||
|---------|------|------------|---------|
|
||
| `D1` | 入排合规性 | HardRule | 已有 `inclusion`/`exclusion`,需重新标注 |
|
||
| `D2` | 数据完整性 | CompletenessEngine(批次 C) | 未实现 |
|
||
| `D3` | 变量准确性 | HardRule | 已有 `lab_values`/`logic_check`,需重新标注 |
|
||
| `D4` | 数据质疑管理 | 状态机(eQuery 流转) | 已有 eQuery 表 |
|
||
| `D5` | 安全性监测 | HardRule + SoftRule(批次 C) | SoftRuleEngine 已有框架 |
|
||
| `D6` | 方案偏离 | HardRule + SoftRule(批次 C) | 未实现 |
|
||
| `D7` | 药物管理 | HardRule(批次 C) | 未实现 |
|
||
|
||
**改造点**:
|
||
- `QCRule` 接口的 `category` 字段从 4 种值改为 D1-D7
|
||
- 种子规则 `seed-iit-qc-rules.ts` 重新标注所有现有规则的维度
|
||
- `qc_field_status.rule_category` 存储维度代码,支持按维度聚合
|
||
|
||
### 3.6 字段语义化(IitFieldMapping 反向增强)
|
||
|
||
当前 `IitFieldMapping` 只用于 LLM 输入方向(alias→actual)。增强为双向:
|
||
|
||
```
|
||
LLM 输入方向(已有): "年龄" → age (LLM 对话中说"年龄",系统查 age)
|
||
LLM 输出方向(新增): age → "年龄(岁)" (报告中展示"年龄(岁)"而非 age)
|
||
```
|
||
|
||
**数据来源**:REDCap Data Dictionary 的 `field_label` 字段天然就是中文语义标签。项目初始化时自动同步:
|
||
|
||
```typescript
|
||
async function syncSemanticLabels(projectId: string) {
|
||
const metadata = await redcapAdapter.exportMetadata();
|
||
for (const field of metadata) {
|
||
await prisma.iitFieldMapping.upsert({
|
||
where: { projectId_aliasName: { projectId, aliasName: field.field_name } },
|
||
update: { semanticLabel: field.field_label, formName: field.form_name },
|
||
create: {
|
||
projectId,
|
||
aliasName: field.field_name,
|
||
actualName: field.field_name,
|
||
semanticLabel: field.field_label,
|
||
formName: field.form_name,
|
||
fieldType: field.field_type,
|
||
},
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
**使用场景**:`QcReportService` 生成 LLM XML 时,用 `semanticLabel` 替代物理字段名。
|
||
|
||
### 3.7 LLM 三不原则(正式化为设计规范)
|
||
|
||
所有 LLM-facing 输出必须遵循:
|
||
|
||
| 原则 | 含义 | 实现方式 |
|
||
|------|------|---------|
|
||
| **不喂全量** | 只传 FAIL/WARNING 的切片 | `QcReportService` 从 `qc_field_status` WHERE status IN ('FAIL','WARNING') |
|
||
| **不喂物理字段** | 字段名用中文语义 | 查 `IitFieldMapping.semanticLabel` 替换 |
|
||
| **不让 LLM 算数** | 百分比、天数差等由 Node.js 预算 | 所有数值结论以"标签"形式传入(如"超窗 5 天"而非让 LLM 算日期差) |
|
||
|
||
### 3.8 Record-Level Context:跨表单质控上下文全拉取
|
||
|
||
**问题**:REDCap DET Webhook 推送的 payload 只包含**当前保存的表单**数据。如果 CRC 保存了"实验室检查表",Webhook 只带这一个表单。但 D5 规则(如"ALT 异常但没报 AE")需要同时查看 AE 表的数据——拿不到就会误判。
|
||
|
||
**设计原则**:在 IIT 场景下,单个患者的全量数据通常只有几十到几百行(几 KB),不存在性能问题。因此,**不论 Webhook 传来了什么表单,`QcExecutor` 一律拉取该患者的全量数据**。
|
||
|
||
```typescript
|
||
// QcExecutor.executeSingle() 中的数据拉取逻辑
|
||
async executeSingle(projectId: string, recordId: string, eventId: string, ...) {
|
||
// ⚠️ 关键:不论 Webhook 传来的 instrument 是什么,都拉全量
|
||
// 这样跨表单规则(D5 AE 漏报、D6 合并用药禁忌)才能正确执行
|
||
const allRecords = await redcapAdapter.exportRecords({ records: [recordId] });
|
||
|
||
// 标准化五级坐标(含 InstanceID 洗线)
|
||
const normalized = redcapAdapter.normalizeInstances(allRecords);
|
||
|
||
// 按 event 分组,但在内存中保持全量,供跨表单规则访问
|
||
const patientContext = this.buildPatientContext(normalized);
|
||
|
||
// 执行规则时传入完整上下文
|
||
for (const rule of applicableRules) {
|
||
const result = engine.executeRule(rule, patientContext);
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
**关键区别**:
|
||
- 旧设计(V3.0):`getRecordById()` 拉全量但 **merge 成扁平对象**,丢失五级结构
|
||
- 新设计(V3.1):`exportRecords()` 拉全量且 **保持原始多行结构**,每行 = 一个 event × form × instance
|
||
|
||
### 3.9 eQuery 自动闭环:State Transition Hook
|
||
|
||
**问题**:当质控发现 Field 为 FAIL 时会生成 eQuery。但当 CRC 修正数据后,Webhook 再次触发质控,Field 变回 PASS——此时**没有任何机制自动关闭之前的 eQuery**。系统变成了"只开 Query 不销账"的半成品。
|
||
|
||
**设计**:在 `QcExecutor` 的 `upsert qc_field_status` 逻辑中,比较新旧状态,触发自动闭环。
|
||
|
||
```typescript
|
||
// QcExecutor 内部:upsert 前先读取旧状态
|
||
async upsertFieldStatus(data: FieldStatusData): Promise<void> {
|
||
const oldRecord = await prisma.qcFieldStatus.findUnique({
|
||
where: {
|
||
uq_field_status: {
|
||
project_id: data.projectId,
|
||
record_id: data.recordId,
|
||
event_id: data.eventId,
|
||
form_name: data.formName,
|
||
instance_id: data.instanceId,
|
||
field_name: data.fieldName,
|
||
}
|
||
}
|
||
});
|
||
|
||
const oldStatus = oldRecord?.status;
|
||
|
||
// 执行 upsert
|
||
await prisma.qcFieldStatus.upsert({ ... });
|
||
|
||
// ===== State Transition Hook =====
|
||
// FAIL → PASS:自动关闭关联的 eQuery
|
||
if (oldStatus === 'FAIL' && data.status === 'PASS') {
|
||
await prisma.iitEquery.updateMany({
|
||
where: {
|
||
projectId: data.projectId,
|
||
recordId: data.recordId,
|
||
eventId: data.eventId,
|
||
formName: data.formName,
|
||
instanceId: data.instanceId,
|
||
fieldName: data.fieldName,
|
||
status: { in: ['pending', 'reopened'] },
|
||
},
|
||
data: {
|
||
status: 'auto_closed', // 区分于人工 'closed'
|
||
respondedAt: new Date(),
|
||
responseText: 'AI 自动复核通过:数据已修正,质控结果变为 PASS',
|
||
}
|
||
});
|
||
}
|
||
|
||
// PASS → FAIL:如果之前有 auto_closed 的 eQuery,自动重开
|
||
if (oldStatus === 'PASS' && data.status === 'FAIL') {
|
||
// 新建 eQuery(不重开旧的,保持审计链清晰)
|
||
await equeryService.create({ ... });
|
||
}
|
||
}
|
||
```
|
||
|
||
**设计决策**:
|
||
- 自动关闭使用 `auto_closed` 状态(而非复用 `closed`),便于审计区分"AI 自动销账"和"CRC 回复后人工关闭"
|
||
- `IitEquery` 的 status 枚举需扩展:`pending | responded | reviewing | closed | reopened | auto_closed`
|
||
- 回退场景(PASS → FAIL)创建新 eQuery 而非重开旧的,保持审计链完整
|
||
|
||
---
|
||
|
||
## 4. 批次 A:数据底座加固(预估 1.5-2 周)
|
||
|
||
### 4.1 任务清单
|
||
|
||
| # | 任务 | 工作量 | 依赖 |
|
||
|---|------|--------|------|
|
||
| A1 | Prisma Schema 升级 + Migration | 1 天 | 无 |
|
||
| | - 新建 `QcFieldStatus` model(五级坐标) | | |
|
||
| | - `IitQcLog` 加 `formName`, `instanceId` | | |
|
||
| | - `IitEquery` 加 `instanceId` | | |
|
||
| | - `IitFieldMapping` 加 `semanticLabel`, `formName`, `ruleCategory` | | |
|
||
| A2 | `RedcapAdapter.normalizeInstances()` | 0.5 天 | 无 |
|
||
| | - InstanceID 幽灵状态洗线 | | |
|
||
| | - 非重复表单强制 `instanceId: 1` | | |
|
||
| | - 重复表单第一行强制 `1` | | |
|
||
| A3 | `QcExecutor` 核心服务 | 2.5 天 | A1, A2 |
|
||
| | - `executeSingle()` 方法(含 Record-Level Context 全量拉取) | | |
|
||
| | - `executeBatch()` 方法 | | |
|
||
| | - 五级坐标 upsert `qc_field_status` | | |
|
||
| | - State Transition Hook:FAIL→PASS 自动关闭 eQuery | | |
|
||
| | - `IitEquery.status` 枚举扩展 `auto_closed` | | |
|
||
| | - 改造 `iitBatchController` 调用 `QcExecutor` | | |
|
||
| A4 | 规则维度重新标注 | 0.5 天 | A1 |
|
||
| | - `seed-iit-qc-rules.ts` 所有规则加 `ruleCategory: 'D1'|'D3'` | | |
|
||
| | - `HardRuleEngine.QCRule.category` 枚举扩展 | | |
|
||
| A5 | 字段语义同步 + 报告语义化 | 1 天 | A1 |
|
||
| | - `syncSemanticLabels()` 从 REDCap Metadata 自动填充 | | |
|
||
| | - `QcReportService` 生成报告时查 `semanticLabel` 替换字段名 | | |
|
||
| A6 | 验证脚本 | 0.5 天 | A3, A4, A5 |
|
||
| | - 验证 `qc_field_status` 五级坐标正确性 | | |
|
||
| | - 验证 LLM 报告字段名已语义化 | | |
|
||
| | - 回归测试:现有 D1 + D3 功能不退化 | | |
|
||
|
||
### 4.2 验收标准
|
||
|
||
- [ ] 执行一键全量质控后,`qc_field_status` 包含完整的 project × record × event × form × instance × field 数据
|
||
- [ ] 非重复表单的 `instance_id` 统一为 1
|
||
- [ ] 重复表单(如 AE)的多个 Instance 各自独立记录
|
||
- [ ] LLM XML 报告中字段名为中文语义(如"年龄"而非"age")
|
||
- [ ] 每条 `qc_field_status` 记录的 `rule_category` 正确标注为 D1-D7
|
||
- [ ] 跨表单规则可正确执行(如 D5 规则需要同时访问实验室表和 AE 表)
|
||
- [ ] Field 从 FAIL 变 PASS 时,关联的 eQuery 自动变为 `auto_closed`
|
||
- [ ] 现有的入排标准检查 + 变量范围检查功能不回归
|
||
|
||
---
|
||
|
||
## 5. 批次 B:聚合层与冒泡机制(预估 1.5-2 周)
|
||
|
||
### 5.1 任务清单
|
||
|
||
| # | 任务 | 工作量 | 依赖 |
|
||
|---|------|--------|------|
|
||
| B1 | Prisma Schema:新建 `QcEventStatus`,改造 `RecordSummary` | 0.5 天 | 批次 A 完成 |
|
||
| B2 | `QcExecutor.aggregateDeferred()` | 1.5 天 | B1 |
|
||
| | - SQL 聚合 field_status → event_status | | |
|
||
| | - SQL 聚合 event_status → record_summary | | |
|
||
| | - SQL 聚合 record_summary → project_stats | | |
|
||
| | - 批量执行后触发一次聚合(防抖) | | |
|
||
| | - 单条执行后推入 pg-boss 防抖队列 | | |
|
||
| B3 | 多维报告框架 | 1 天 | B2 |
|
||
| | - `QcReportService` 按 D1-D7 分章节生成 LLM XML | | |
|
||
| | - 新增 `<event_overview>` 章节 | | |
|
||
| | - 新增 `<dimension_summary>` 章节(各维度通过率) | | |
|
||
| B4 | 定时质控灵活配置 | 1 天 | 批次 A 完成 |
|
||
| | - 后端:`registerProjectCrons()` 从全局硬编码改为读取项目 `cronExpression` | | |
|
||
| | - 前端:可视化配置面板(每天/每周/每N小时/高级 Cron) | | |
|
||
| | - Cron 表达式参考:`0 8 * * *`(每天8:00) / `0 9 * * 1`(每周一) / `0 8 * * 1,3,5`(一三五) | | |
|
||
| B5 | 前端:受试者×表单热力图原型 | 1 天 | B2 |
|
||
| | - 从 `qc_event_status` 读取数据 | | |
|
||
| | - 行 = 受试者,列 = 事件/表单,颜色 = 状态 | | |
|
||
| B6 | 实时质控激活 | 0.5 天 | B2 |
|
||
| | - `WebhookController` 接入 `QcExecutor.executeSingle()` | | |
|
||
| | - 执行后推入防抖聚合队列 | | |
|
||
| B7 | 端到端验证 | 0.5 天 | B2-B6 |
|
||
|
||
### 5.2 验收标准
|
||
|
||
- [ ] `qc_field_status` FAIL 后,对应 `qc_event_status` 自动为 FAIL
|
||
- [ ] `qc_event_status` FAIL 后,对应 `record_summary.latestQcStatus` 自动为 FAIL
|
||
- [ ] 维度计数正确:`qc_event_status.d1_issues` 等于该事件下 D1 类别 FAIL 的数量
|
||
- [ ] 批量 1000 条规则执行无死锁(聚合在执行完成后统一进行)
|
||
- [ ] 不同项目可独立配置定时质控策略
|
||
- [ ] REDCap 保存表单后 30 秒内三级状态表更新(实时触发)
|
||
- [ ] 热力图正确展示红/黄/绿状态
|
||
- [ ] LLM 报告包含维度分章节 + 事件概览
|
||
- [ ] 报告生成时间 < 2 秒(100 条记录、500 个变量规模)
|
||
|
||
---
|
||
|
||
## 6. 批次 C:新维度引擎(按需,依赖临床专家输入)
|
||
|
||
### 6.1 前提条件
|
||
|
||
- 批次 A + B 已稳定运行至少 1 周
|
||
- 临床专家已确认各维度规则的可行性和优先级
|
||
- 有真实 IIT 项目数据可供验证
|
||
|
||
### 6.2 任务清单(按优先级排序)
|
||
|
||
| # | 任务 | 工作量 | 说明 |
|
||
|---|------|--------|------|
|
||
| C1 | D2 CompletenessEngine(简化版) | 2 天 | 仅统计 required=true 且无 branching_logic 的绝对必填字段缺失率 |
|
||
| C2 | D6 方案偏离引擎 | 2 天 | 访视超窗检测(目标日 ± N 天) |
|
||
| C3 | D5 AE 漏报侦测 | 2-3 天 | SoftRule + RAG,实验室异常 → 检查 AE 表有无匹配 |
|
||
| C4 | 项目健康度评分 | 1 天 | D1-D7 加权综合,可视化展示 |
|
||
| C5 | 沙盒测试机制 | 1 天 | 历史数据回放 + 结果导出 Excel |
|
||
|
||
### 6.3 D2 缺失率的折中过渡法(V1.1 修正:增加时序过滤)
|
||
|
||
**问题 1(字段维度)**:在完整的 Branching Logic 解析器实现之前,简单的"总字段数 - 实填数"会严重高估缺失率(把因分支逻辑隐藏的字段也算作缺失)。
|
||
|
||
**问题 2(时序维度,V1.1 新增)**:如果一个项目有 V1-V10 共 10 次访视,患者昨天刚入组(当前在 V1),系统去算缺失率会把 V2-V10 的必填字段全部算作"已缺失",导致新入组患者缺失率高达 90%。这是**致命的临床逻辑错误**——那些访视根本还没发生。
|
||
|
||
**解法**:双重过滤——字段过滤 + 时序过滤。
|
||
|
||
```typescript
|
||
async function calculateMissingRate(
|
||
projectId: string,
|
||
recordId: string
|
||
): Promise<{ rate: number; denominator: number; numerator: number }> {
|
||
|
||
// 1. 字段过滤:只统计绝对必填字段(required=y 且无 branching_logic)
|
||
const metadata = await redcapAdapter.exportMetadata();
|
||
const absoluteRequired = metadata.filter(
|
||
f => f.required_field === 'y' && !f.branching_logic
|
||
);
|
||
|
||
// 2. 时序过滤(V1.1 关键补丁):
|
||
// 找出该患者在 REDCap 中有实质数据的事件列表
|
||
const patientRecords = await redcapAdapter.exportRecords({ records: [recordId] });
|
||
const activeEvents = new Set(
|
||
patientRecords
|
||
.filter(r => hasSubstantiveData(r)) // 排除只有 record_id 的空行
|
||
.map(r => r.redcap_event_name)
|
||
);
|
||
|
||
// 3. 只统计已到达事件中的绝对必填字段
|
||
const formEventMapping = await redcapAdapter.getFormEventMapping();
|
||
let denominator = 0;
|
||
let filled = 0;
|
||
|
||
for (const field of absoluteRequired) {
|
||
// 该字段所属的表单,在哪些事件中出现
|
||
const fieldEvents = formEventMapping
|
||
.filter(m => m.form === field.form_name)
|
||
.map(m => m.unique_event_name);
|
||
|
||
// 只计算患者已到达的事件
|
||
for (const event of fieldEvents) {
|
||
if (!activeEvents.has(event)) continue; // 未来事件,跳过
|
||
denominator++;
|
||
const record = patientRecords.find(
|
||
r => r.redcap_event_name === event
|
||
);
|
||
if (record && record[field.field_name] != null && record[field.field_name] !== '') {
|
||
filled++;
|
||
}
|
||
}
|
||
}
|
||
|
||
const numerator = denominator - filled;
|
||
const rate = denominator > 0 ? Math.round((numerator / denominator) * 1000) / 10 : 0;
|
||
return { rate, denominator, numerator };
|
||
}
|
||
|
||
function hasSubstantiveData(record: Record<string, any>): boolean {
|
||
// 排除只有 record_id / redcap_event_name 等元数据的空行
|
||
const metaFields = ['record_id', 'redcap_event_name', 'redcap_repeat_instrument', 'redcap_repeat_instance'];
|
||
return Object.entries(record).some(
|
||
([key, val]) => !metaFields.includes(key) && val != null && val !== ''
|
||
);
|
||
}
|
||
```
|
||
|
||
**总结**:
|
||
- **字段过滤**:`required=y` 且无 `branching_logic` → 排除条件字段
|
||
- **时序过滤**:只统计患者已有数据的事件 → 排除未来访视
|
||
- 两层过滤后,分子分母才是临床上无争议的
|
||
|
||
### 6.4 沙盒测试机制
|
||
|
||
**问题**:临床专家不懂代码,怎么验证新规则的准确性?
|
||
|
||
**解法**:开发"历史数据回放"功能:
|
||
|
||
```
|
||
1. 管理员配置新的 D5/D6 规则(状态设为"草稿",不在生产环境生效)
|
||
2. 点击"沙盒测试"按钮
|
||
3. 系统拿该项目所有历史患者数据,用草稿规则跑一遍
|
||
4. 结果不写入 qc_field_status(只存临时表或内存)
|
||
5. 导出 Excel:"AI 抓出了这 5 个疑似漏报 AE,请主任确认"
|
||
6. 专家确认 OK → 规则状态改为"已发布",正式生效
|
||
```
|
||
|
||
**实现要点**:
|
||
- `IitSkill` 增加 `status` 字段:`draft` | `published` | `archived`
|
||
- `QcExecutor` 加载规则时只取 `status = 'published'` 的
|
||
- 沙盒执行复用 `QcExecutor` 核心逻辑,但结果写入临时存储
|
||
|
||
---
|
||
|
||
## 7. LLM 报告升级(批次 B 产出)
|
||
|
||
### 7.1 多维报告 XML 结构
|
||
|
||
```xml
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<qc_context project_id="xxx" project_name="test0207" generated="2026-03-15T08:00:00Z">
|
||
|
||
<!-- 1. 宏观统计 -->
|
||
<summary>
|
||
- 通过率: 92.9%(14 条记录,13 通过,1 失败)
|
||
- 事件覆盖: 42 个事件已质控,39 通过
|
||
- 严重问题: 1 | 警告: 2
|
||
</summary>
|
||
|
||
<!-- 2. 维度概览(各维度独立通过率)-->
|
||
<dimension_summary>
|
||
- D1 入排合规: 13/14 通过 (92.9%) — 1 条排除标准违规
|
||
- D3 变量准确: 550/556 通过 (98.9%) — 6 条极值异常
|
||
- D2 数据完整: --(尚未启用)
|
||
- D5 安全性: --(尚未启用)
|
||
- D6 方案偏离: --(尚未启用)
|
||
</dimension_summary>
|
||
|
||
<!-- 3. 事件维度统计 -->
|
||
<event_overview>
|
||
- 筛选期: 14/14 通过 (100%)
|
||
- 基线期: 13/14 通过 (92.9%)
|
||
- 随访1: 12/12 通过 (100%)
|
||
</event_overview>
|
||
|
||
<!-- 4. 严重问题详情(按受试者 × 事件 × 表单 × 实例定位)-->
|
||
<critical_issues count="1">
|
||
<record id="3">
|
||
<event name="基线期">
|
||
<form name="入排标准表" instance="1">
|
||
1. [D1][exc_001] **排除标准检查**: 排除标准第1项 = **1**(标准: 0)
|
||
</form>
|
||
</event>
|
||
</record>
|
||
</critical_issues>
|
||
|
||
<!-- 5. 警告问题 -->
|
||
<warnings count="2">
|
||
<record id="7">
|
||
<event name="随访2">
|
||
<form name="实验室检查" instance="1">
|
||
1. [D3][lab_003] **谷丙转氨酶(ALT)**: 当前值 **52 U/L**(正常上限: 50 U/L)
|
||
</form>
|
||
</event>
|
||
</record>
|
||
</warnings>
|
||
|
||
</qc_context>
|
||
```
|
||
|
||
**对比 V3.0**:
|
||
- 新增 `<dimension_summary>` —— 各维度独立呈现
|
||
- 新增 `<event_overview>` —— 事件级统计
|
||
- 问题定位从 `record → field` 精确到 `record → event → form → instance → field`
|
||
- 每条问题标注维度代码(如 `[D1]`、`[D3]`)
|
||
|
||
---
|
||
|
||
## 8. 与四大工具的集成变更
|
||
|
||
| 工具 | 批次 A 变更 | 批次 B 变更 |
|
||
|------|------------|------------|
|
||
| `read_report` | 报告字段名语义化 | 新增 `section=dimension_summary` / `section=event_overview` |
|
||
| `look_up_data` | 无变更 | 可附带每个字段的 `qc_status`(从 `qc_field_status` 读取) |
|
||
| `check_quality` | 调用 `QcExecutor` 替代旧逻辑 | 自动触发聚合,结果写入三级状态表 |
|
||
| `search_knowledge` | 无变更 | 无变更 |
|
||
|
||
---
|
||
|
||
## 9. 风险与缓解
|
||
|
||
| 风险 | 概率 | 影响 | 缓解措施 |
|
||
|------|------|------|---------|
|
||
| REDCap InstanceID 返回值不一致 | 高 | 五级坐标不完整 | `RedcapAdapter.normalizeInstances()` 强制标准化(3.1 节) |
|
||
| 跨表单规则拿不到完整上下文 | 高 | D5 规则误判 | Record-Level Context 全量拉取(3.8 节) |
|
||
| eQuery 只开不关 | 高 | 系统不可信 | State Transition Hook 自动闭环(3.9 节) |
|
||
| 批量质控冒泡导致死锁 | 中 | 数据库卡顿 | 受试者级防抖聚合(3.4 节) |
|
||
| D2 缺失率被高估(无分支逻辑) | 高 | 临床不信任 | 绝对必填字段 + 时序过滤(6.3 节) |
|
||
| D2 缺失率计入未来访视 | 高 | 新入组患者 90% 缺失 | Event-Aware 过滤,只统计已到达事件(6.3 节) |
|
||
| 新规则上线后误报过多 | 中 | 影响用户信心 | 沙盒测试 + 专家确认后再发布(6.4 节) |
|
||
| Migration 影响现有数据 | 低 | 数据丢失 | 新增表/列均为追加型,不修改现有数据 |
|
||
|
||
---
|
||
|
||
## 10. 关键决策记录
|
||
|
||
| 决策 | 选择 | 理由 |
|
||
|------|------|------|
|
||
| 数据层级 | 五级(Record → Event → Form → Instance → Field) | 对齐 CDISC ODM,解决重复表单定位 |
|
||
| InstanceID 标准化位置 | `RedcapAdapter` 层统一处理 | 下游无需关心 REDCap 返回值不一致 |
|
||
| 状态优先级 | FAIL(3) > WARNING(2) > UNCERTAIN(1) > PASS(0) | 聚合时始终取最严重状态 |
|
||
| SKIPPED 变量处理 | 不写入 `qc_field_status` | 减少无意义数据,简化聚合逻辑 |
|
||
| 实时质控数据拉取范围 | Record-Level(全量拉取该患者所有事件/表单) | 跨表单规则必须有完整上下文 |
|
||
| eQuery 闭环策略 | State Transition Hook(FAIL→PASS 自动关闭) | 避免"只开不关",状态用 `auto_closed` 区分人工关闭 |
|
||
| 冒泡防抖粒度 | 实时触发用受试者级,批量触发用项目级 | 多 CRC 同时录入不同受试者时互不干扰 |
|
||
| D2 缺失率过渡方案 | 绝对必填字段 + Event-Aware 时序过滤 | 排除条件字段和未来访视,分子分母临床无争议 |
|
||
| 规则发布流程 | draft → sandbox test → published | 临床专家可验证准确性 |
|
||
| 规则分类 | D1-D7 七大维度 | 对齐 CRA 工作维度,支撑多维报告 |
|
||
| 字段语义化数据源 | REDCap Data Dictionary `field_label` | 已有数据,自动同步,DM 可微调 |
|
||
| 自动映射 vs 半自动映射 | 半自动(系统建议 + DM 确认) | 全自动 LLM 映射准确率不可控 |
|
||
| V3.0 文档处置 | 保留为历史版本 | 记录从三级到五级的设计演进 |
|
||
|
||
---
|
||
|
||
## 11. 时间线总览
|
||
|
||
```
|
||
批次 A 批次 B 批次 C
|
||
(数据底座加固) (聚合与冒泡) (新维度引擎)
|
||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐
|
||
│ 1.5-2 周 │ │ 1.5-2 周 │ │ 按需 │
|
||
│ │ │ │ │ │
|
||
Week 1-2 │ A1 Schema 升级 │ │ │ │ │
|
||
│ A2 InstanceID 洗线│ │ │ │ │
|
||
│ A3 QcExecutor │ │ │ │ │
|
||
│ A4 规则维度标注 │ │ │ │ │
|
||
Week 2-3 │ A5 语义化同步 │ │ │ │ │
|
||
│ A6 验证 │ │ │ │ │
|
||
└────────┬─────────┘ │ │ │ │
|
||
│ 验收通过 │ │ │ │
|
||
Week 3-4 └──────────────►│ B1 Event 表 │ │ │
|
||
│ B2 防抖聚合 │ │ │
|
||
│ B3 多维报告 │ │ │
|
||
Week 4-5 │ B4 定时配置 │ │ │
|
||
│ B5 热力图 │ │ │
|
||
│ B6 实时质控 │ │ │
|
||
│ B7 端到端验证 │ │ │
|
||
└────────┬─────────┘ │ │
|
||
│ 验收通过 + │ │
|
||
│ 临床专家确认 │ │
|
||
Week 6+ └──────────────►│ C1 D2 缺失率 │
|
||
│ C2 D6 偏离 │
|
||
│ C3 D5 AE │
|
||
│ C4 健康度 │
|
||
│ C5 沙盒测试 │
|
||
└──────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
> **一句话总结**:五级坐标让每个质控结论都有精确的 GPS 定位,维度分类让报告从扁平变为多维,异步防抖聚合让冒泡机制可靠落地——这三者共同构成 V3.1 质控引擎的架构基石。先把底座做对(批次 A),再把聚合做稳(批次 B),最后把覆盖做广(批次 C)。
|