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
This commit is contained in:
2026-03-08 21:54:35 +08:00
parent ac724266c1
commit a666649fd4
57 changed files with 28637 additions and 316 deletions

View File

@@ -0,0 +1,141 @@
# IIT / CRA Agent 指标口径与 SSOT 对照表Phase 0
> 文档版本v1.0
> 创建日期2026-03-08
> 适用范围IIT 业务端大盘 / 报告与关键事件 / AI 实时工作流水 / AI 对话
> 目标:先统一“同名指标”的定义和数据源,再进入修复,避免反复打补丁
---
## 1. 背景与问题定义
当前线上/测试反馈的核心不是单点报错,而是同一指标在多个页面口径不一致,例如:
- 大盘通过率与趋势通过率不一致
- 报告页健康分与大盘健康分不一致
- 时间线问题数与待处理 eQuery 不一致
- 报告中 D1/D2/D3D4/D6 与下方维度评分对不上
这些问题在工程上属于**口径漂移metric drift**,必须先冻结 SSOTSingle Source of Truth再排查数据同步、规则执行、前端展示。
---
## 2. 指标分层模型(必须统一)
IIT 指标按 4 层定义,禁止跨层混算:
1. **事实层Raw Facts**REDCap 原始记录与事件数据
2. **执行层QC Results**`qc_field_status` / `qc_event_status` 的规则执行结果
3. **聚合层Project Stats**`iit_qc_project_stats``iit_record_summary` 的聚合快照
4. **呈现层UI/API**Dashboard/Reports/AiStream/AiChat
约束:
- 呈现层不允许重新发明计算公式,只消费聚合层或执行层
- 同一名称指标只能有一个“主口径”
- 不同用途的指标必须显式命名(例如“按受试者通过率”与“按事件通过率”)
---
## 3. 核心指标 SSOT 对照表(冻结版)
## 3.1 项目健康度与通过率
| 指标名 | 业务定义 | SSOT 来源 | 当前代码入口 | 备注 |
|---|---|---|---|---|
| healthScore | 健康度总分0-100 | `iit_qc_project_stats.health_score` | `iitQcCockpitService.getStats()` | 不允许前端 fallback 推导 |
| healthGrade | 健康度等级 | `iit_qc_project_stats.health_grade` | `iitQcCockpitService.getStats()` | 同上 |
| passRate (大盘) | 按受试者通过率 | `passed_records / total_records` | `iitQcCockpitService.getStats()` | 保留 1 位小数 |
| passRate (趋势) | 按日志条目通过率(旧) | `iit_qc_logs` 分组计算 | `iitQcCockpitController.getTrend()` | 与大盘不是同一口径,必须重命名或替换 |
结论:
- 当前“通过率”至少有两种口径UI 未标注必然引发“100% vs 33%”类问题。
## 3.2 D1/D2/D3D4/D6 报表
| 报表 | 业务定义 | SSOT 来源 | 当前查询入口 |
|---|---|---|---|
| D1 筛选入选 | 入排规则是否通过 | `qc_field_status`D1 + `record_summary`(受试者全集) | `iitQcCockpitService.getEligibilityReport()` |
| D2 完整性 | 缺失字段率 | `qc_field_status`D2 | `iitQcCockpitService.getCompletenessReport()` |
| D3/D4 质疑跟踪 | eQuery 生命周期 | `iit_equeries` | `iitQcCockpitService.getEqueryLogReport()` |
| D6 方案偏离 | 访视超窗等偏离 | `qc_field_status`D6 | `iitQcCockpitService.getDeviationReport()` |
结论:
- D 类报表与大盘维度评分来自不同聚合路径时,必须明确“时点一致性”(同一批次同一时刻)。
## 3.3 AI 实时工作流水
| 指标 | 业务定义 | SSOT 来源 | 当前入口 | 风险 |
|---|---|---|---|---|
| timeline total | 时间线受试者总数 | `qc_field_status` 聚合 + pass 补充 | `iitQcCockpitController.getTimeline()` | 与 eQuery 总数不是同一概念 |
| red/yellow 问题数 | 每受试者 FAIL/WARNING 汇总 | `qc_field_status` | `getTimeline()` | 应与 field-issues 可对账 |
| 事件中文名 | event display label | `qc_event_status.event_label` | `getTimeline()` LEFT JOIN | 空值时会回退技术名 |
## 3.4 AI 对话
| 语义工具 | 主要数据源 | 适用问题 | 当前实现 |
|---|---|---|---|
| `read_report` | `QcReportService` 缓存报告 | 通过率、问题统计、趋势摘要 | `ToolsService` |
| `look_up_data` | REDCap 原始数据 | 单患者字段值、原始记录核查 | `ToolsService` |
| `check_quality` | `QcExecutor` 实时执行 | 用户明确要求“重跑质控” | `ToolsService` |
| `search_knowledge` | 项目知识库RAG | 方案文本、入排标准文本 | `ToolsService` |
结论:
- 对话错误不等于“模型幻觉”,优先排查是否选错工具或工具查询不完整。
---
## 4. 现存口径漂移点(已识别)
1. **趋势口径漂移**
- 趋势接口仍从 `iit_qc_logs` 计算通过率;大盘通过率来自 `iit_qc_project_stats`
- 导致“卡片 100%,趋势 33%”。
2. **健康分 fallback 风险**
- 大盘前端在 `healthScore` 缺失时回退为 `Math.round(passRate)`
- 数据未准备好时会把通过率误当健康分,造成“待处理质疑很多但仍 100 分”。
3. **报告缓存时点风险**
- `read_report` 优先读缓存报告(默认有效期 24h若未及时刷新会滞后于大盘/流水。
4. **“总数”语义混淆**
- 时间线问题数、eQuery 待处理数、D3D4 报表数本质是不同对象UI 文案未区分。
5. **展示字段名兼容风险**
-`eventLabel/fieldLabel` 空,前端回退技术字段名;用户会误判为“事件名错误”。
---
## 5. 冻结规则(修复期强制执行)
1. 页面上所有“通过率”必须标注口径:
- 按受试者record
- 按事件record-event
- 按规则检查条目field-level checks
2. 健康度评分只允许来自 `healthScoreEngine` 落库结果,不允许前端推算。
3. 报告页、大盘、AI 对话必须显示“统计快照时间”,便于用户识别时点差异。
4. UI 文案新增说明:
- “待处理质疑”来自 `iit_equeries`
- “时间线问题总数”来自 `qc_field_status`
5. 涉及 D1/D2/D3D4/D6 的变更,必须附带“口径回归测试”记录。
---
## 6. Phase 1 进入条件
满足以下条件后,才进入根因排查与代码修复:
- [ ] 本文“SSOT 对照表”已被团队确认(产品/研发/质控)
- [ ] 每个页面核心指标已标注口径
- [ ] 已选定“最小复现项目”与冻结时间窗口
Phase 1 执行手册见:
`docs/03-业务模块/IIT Manager Agent/09-技术评审报告/2026-03-08-IIT-CRA-最小复现项目对账执行手册-Phase1.md`

View File

@@ -0,0 +1,225 @@
# IIT / CRA Agent 最小复现项目对账执行手册Phase 1
> 文档版本v1.0
> 创建日期2026-03-08
> 目标:用最小复现项目定位根因,不做盲修
> 适用问题报告与事件不一致、AI 实时流水不一致、AI 对话结论错误
---
## 1. 执行原则
1. **先对账,后修复**:每个异常必须先确认“错在数据、规则、聚合、还是展示”。
2. **单项目封闭验证**:只使用“纳入排除标准测试”项目,不混其他项目。
3. **同一时间窗口**:所有 API / SQL 在同一时段采样,避免时间漂移。
4. **一条问题一条证据链**:必须保留“输入 -> 处理中间态 -> 输出”。
---
## 2. 最小复现项目准备
项目:`纳入排除标准测试`
固定对象:
- 患者:`2 / 3 / 4`(至少包含不完整录入与违规样本)
- 事件:筛选期 + 第一次月经周期结束时(若项目定义如此)
- 规则D1/D2/D3/D4/D6 至少各有 1 条命中场景
冻结窗口:
- 执行前停止自动批任务(避免数据持续变化)
- 手动执行一次“一键全量质控”,记录触发时间 T0
- 所有采样以 T0 后 1-3 分钟为准
---
## 3. 四层对账流程(按顺序)
## 3.1 事实层REDCap 是否联通)
目标:确认“源头有数据”。
检查项:
- REDCap 中是否存在目标患者/事件数据
- `record_summary` 是否包含对应 `record_id`
- `SyncManager` 最近同步时间是否晚于 T0
判定:
- REDCap 有、平台无 -> 联通/同步问题
- REDCap 无 -> 上游录入问题,不是质控引擎问题
## 3.2 执行层(规则是否正确执行)
目标:确认“规则判断是否符合临床预期”。
检查项:
- `qc_field_status` 是否有该患者对应 D1/D2/D3/D6 记录
- `status/severity/message/actual_value/expected_value` 是否合理
- `qc_event_status` 是否与字段级状态一致
判定:
- 执行结果本身错 -> 规则定义/引擎问题
- 执行结果正确、展示错 -> 聚合/前端问题
## 3.3 聚合层(报表和大盘是否同口径)
目标:确认“同名指标是否来自同一口径与同一快照”。
检查项:
- 大盘:`getQcCockpitData`
- 报告:`getQcReport` / `refreshQcReport`
- 趋势:`getTrend`
- D1/D2/D3D4/D6各报表 API
重点看:
- 通过率口径(按受试者 vs 按日志)
- 健康分是否来自 `healthScore` 真实值
- 报告缓存是否过期/未刷新
## 3.4 对话层AI 工具链路是否选对)
目标确认“AI 回答错误是查询错还是推理错”。
检查项:
- 问题触发了哪个工具(`read_report` / `look_up_data` / `check_quality` / `search_knowledge`
- 工具返回数据是否完整(如实验室检查字段是否缺失)
- 最终回答是否与工具结果一致
判定:
- 工具返回错 -> 数据查询/映射/项目隔离问题
- 工具返回对、回答错 -> Prompt/回答策略问题
---
## 4. 针对当前三大类问题的定位矩阵
## 4.1 第一大类:报告与关键事件
| 现象 | 优先怀疑 | 第一检查点 |
|---|---|---|
| D1/D2/D3D4/D6 与维度评分不一致 | 聚合口径漂移 | `getStats()` vs 各报表 SQL |
| 质控 0 个受试者 | 执行未落库或 projectId 错 | `qc_field_status` 是否有该项目数据 |
| 待处理 252 但健康分 100 | 健康分 fallback / 评分权重缺陷 | 前端 `healthScore` fallback、HealthScoreEngine 维度权重 |
| 上方通过率 100下方趋势 33 | 趋势口径不同 | `getTrend()` 当前读 `iit_qc_logs` |
| 质控完成无热力图 | `qc_event_status` 空或事件列构建失败 | `getHeatmapData()` 查询结果 |
| 执行摘要无信息 | 报告缓存旧 / 生成失败 | `iit_qc_reports` 最新记录 + refresh |
| 报告分 64 vs 大盘 100 | 报告快照与实时不一致 | `report.generatedAt` vs cockpit now |
| D1 无数据 | D1 规则缺失或 rule_category 错 | `qc_field_status where D1` |
| D2 事件数=1 且明细异常 | D2 统计规则/activeEvents定义不清 | `getCompletenessReport()` 的 byRecordEvent |
| 已回复仍显示 0 | 状态机未推进或统计口径错 | `iit_equeries.status` 分布 |
| 无“重开质疑”操作 | 前端缺动作入口(后端有状态) | EQueryPage 操作列 |
## 4.2 第二大类AI 实时工作流水
| 现象 | 优先怀疑 | 第一检查点 |
|---|---|---|
| 流水问题总数 444 vs 待处理 252 | 指标对象不同 | 时间线来自 `qc_field_status`,待处理来自 `iit_equeries` |
| 事件编码生成逻辑不明 | 事件标签映射缺失 | `eventLabel` 来源:`qc_event_status` / fallback |
| 日期筛选按钮无效 | API 过滤未生效或前端未传 | `getTimeline(date=YYYY-MM-DD)` 返回是否变化 |
## 4.3 第三大类AI 对话助手
| 现象 | 优先怀疑 | 第一检查点 |
|---|---|---|
| 已签署知情显示 0 | 工具选路错误或字段未映射 | `look_up_data` 查询字段覆盖 |
| 3号患者严重问题被说成无问题 | read_report 缓存滞后或筛选逻辑错 | `QcReportService` 缓存时间与 issue 列表 |
| 总体通过率异常分项非0总体0 | 聚合口径错误 | 报告 summary.passRate 公式 |
| 查询患者2信息不全 | 工具默认返回字段不完整 | `look_up_data` 默认 data 结构 |
| 4号患者访视名错误/状态描述偏差 | eventLabel 回退技术ID + 业务叙述模板粗糙 | `eventLabel` 链路与回答模板 |
---
## 5. 执行清单(逐项打勾)
## 5.1 一次完整排查(建议 2-3 小时)
- [ ] 执行 `batch-qc`,记录 T0、返回 `totalRecords/totalEventCombinations/passRate`
- [ ] 拉取驾驶舱:`qc-cockpit`
- [ ] 拉取报告:`qc-cockpit/report`(先读缓存,再 refresh
- [ ] 拉取趋势:`qc-cockpit/trend`
- [ ] 拉取时间线:`qc-cockpit/timeline`
- [ ] 拉取 D1/D2/D3D4/D6 报表
- [ ] 拉取 `field-issues`critical + warning
- [ ] 拉取 eQuery stats 与 list
- [ ] 对 5 条 AI 问答样例做工具链路记录
## 5.2 SQL 对账模板(只读)
> 以下为只读核对 SQL请在测试库或只读会话执行。
```sql
-- 1) 项目级统计快照
SELECT project_id, total_records, passed_records, failed_records, warning_records,
health_score, health_grade, d1_pass_rate, d2_pass_rate, d3_pass_rate, d5_pass_rate, d6_pass_rate, d7_pass_rate
FROM iit_schema.qc_project_stats
WHERE project_id = :project_id;
```
```sql
-- 2) 字段级问题总量SSOT
SELECT status, severity, rule_category, COUNT(*) AS cnt
FROM iit_schema.qc_field_status
WHERE project_id = :project_id
GROUP BY status, severity, rule_category
ORDER BY rule_category, status, severity;
```
```sql
-- 3) 事件级状态
SELECT record_id, event_id, event_label, status, fields_total, fields_passed, fields_failed, fields_warning
FROM iit_schema.qc_event_status
WHERE project_id = :project_id
ORDER BY record_id, event_id;
```
```sql
-- 4) eQuery 状态分布
SELECT status, COUNT(*) AS cnt
FROM iit_schema.iit_equeries
WHERE project_id = :project_id
GROUP BY status
ORDER BY status;
```
---
## 6. 修复优先级建议(按风险)
P0先修
1. 通过率口径统一(大盘/趋势/报告明确区分)
2. 健康分来源统一(禁前端 fallback 推算)
3. 报告缓存刷新策略(批量质控后强制刷新并透出快照时间)
4. 时间线与 eQuery 统计文案区分(避免同名误读)
P1随后
1. EQuery “重开”前端操作入口(后端状态机已支持)
2. D2“活跃事件/缺失字段”定义可视化说明
3. AI 对话输出模板升级(访视表格化 + 证据引用)
P2优化
1. 工具调用 trace 可视化(问题 -> 工具 -> 结果 -> 回答)
2. 指标字典落地到接口 schema前后端共享类型
---
## 7. 本阶段交付物要求
完成 Phase 1 后,必须输出:
1. `问题-根因对照表`(你列出的每条问题都要有结论)
2. `证据包`API 返回 + SQL 结果 + 截图)
3. `修复方案分批计划`P0/P1/P2 + 影响面 + 回归用例)
没有这三项,不进入大规模代码修改。

View File

@@ -0,0 +1,486 @@
# IIT / CRA Agent 最小复现项目对账结果Phase 1
> 执行日期2026-03-08
> 环境本地开发环境backend + postgres docker + local REDCap
> 目标:验证“报告/大盘/流水/对话”异常是否来自 REDCap 联通、规则执行、聚合链路或展示口径。
---
## 1. 本次对账对象
- IIT 项目名称:`原发性痛经0302`
- IIT 项目 ID`1d80f270-6a02-4b58-9db3-6af176e91f3c`
- 项目 REDCap PID`18`
- 项目 REDCap URL`http://localhost:8080`
- 数据库:`ai_clinical_research``ai-clinical-postgres`
项目映射校验结果:
- `iit_schema.projects` 中存在且唯一:
- `id=1d80f270-6a02-4b58-9db3-6af176e91f3c`
- `name=原发性痛经0302`
- `redcap_project_id=18`
- `status=active`
---
## 2. 第一轮 SQL 证据(核心)
### 2.1 聚合结果与原始质控事实明显不一致
- `iit_schema.qc_project_stats`(该项目):
- `total_records=0`
- `passed_records=0`
- `failed_records=0`
- `warning_records=0`
- `health_score=82.3`
- `d1_pass_rate=41.2`, `d2=100`, `d3=100`, `d5=100`, `d6=69.6`, `d7=100`
- `iit_schema.record_summary`(该项目):
- `COUNT(DISTINCT record_id)=0`
- `iit_schema.qc_field_status`(该项目):
- `COUNT(*)=713`
- `COUNT(DISTINCT record_id)=12`
- 存在 FAIL/WARNING/PASS 混合状态,且 D1/D2/D3/D5/D6 均有数据
- `iit_schema.qc_event_status`(该项目):
- `COUNT(*)=25`
- `iit_schema.equery`(该项目):
- `pending=262`
判定:`qc_field_status/qc_event_status/equery` 已有大量有效数据,但 `record_summary` 为空,导致项目级统计口径断裂。
### 2.2 历史项目残留与项目隔离异常迹象
- `record_summary` 总表仅有 14 行,且全部属于旧项目 ID`test0102-pd-study`
- 当前项目UUID`record_summary` 无行
判定:当前项目未建立或未插入 `record_summary` 基础行,后续“仅 UPDATE 的聚合语句”无法生效。
---
## 3. 第一轮 API 证据(核心)
使用管理员账号登录后,调用 `/api/v1/admin/iit-projects/:projectId/...`
### 3.1 Dashboard / Report / Trend 三口径冲突
- `qc-cockpit` 返回:
- `qualityScore=82`
- `totalRecords=0`
- `passRate=100`
- `criticalCount=137`
- `queryCount=262`
- `qc-cockpit/report` 返回:
- `totalRecords=0`
- `criticalIssues=156`
- `warningIssues=284`
- `passRate=0`
- `qc-cockpit/trend` 返回:
- 最新点:`total=148`, `passed=48`, `passRate=32`
判定:同一项目同一时间,`passRate` 至少出现 `100 / 0 / 32` 三个版本,属于 P0 级口径漂移。
### 3.2 时间线日期筛选无效(复现)
调用 `qc-cockpit/timeline`
- 不带日期:`total=12`
- `startDate=2026-03-01&endDate=2026-03-02``total=12`
- `startDate=2026-03-08&endDate=2026-03-08``total=12`
判定:前端传 `startDate/endDate` 未在后端生效,日期筛选失效可稳定复现。
---
## 4. 对话层冒烟(第一轮)
调用 `/api/v1/iit/chat` 的典型问答可得到答复,但存在“解释与口径不稳定”的风险:
- “签署知情人数”问题:答复给出“表单通过数 10/12”但承认无法直接给出人数。
- “最新质控报告”问题:引用了健康分、严重问题、警告问题、待处理 eQuery整体和 `qc_report` 可对齐。
- “患者访视次数”问题返回“第2次访视”但需和 REDCap 事件表进一步逐条对账Phase 2 再做字段级真值核验)。
判定:对话层主要风险当前不是“工具不可用”,而是上游口径漂移会向下传导。
---
## 5. 根因初判(带代码入口)
### 根因 AP0`record_summary` 聚合是“仅 UPDATE”无 UPSERT
- 代码入口:`backend/src/modules/iit-manager/engines/QcAggregator.ts`
- `aggregateRecordSummary()` 当前逻辑:
- 只执行 `UPDATE iit_schema.record_summary rs ... FROM agg`
- 依赖 `rs` 预先存在
- 当项目没有预写 `record_summary` 行时:
- 聚合更新行数为 0
- `HealthScoreEngine.queryRecordStats()` 基于 `record_summary` 得到 `totalRecords=0`
- Dashboard/Report 中基于该数据的统计出现“0记录 + 非0问题”的冲突
### 根因 BP0时间线接口参数约定不一致
- 代码入口:`backend/src/modules/admin/iit-projects/iitQcCockpitController.ts`
- `getTimeline` 仅读取 `query.date`,未处理 `startDate/endDate`
- 前端使用区间筛选时,后端实际不生效
### 根因 CP1同名指标多来源并行无统一冻结口径
- `passRate` 在 cockpit/report/trend 来源不一致
- 导致用户看到“同一页不同值”的信任问题
---
## 6. 与三大类问题的对应关系(第一轮)
- 第一大类(报告/关键事件):
- 已确认强相关于根因 A + 根因 C
- 第二大类AI 实时工作流水):
- 已确认日期筛选问题由根因 B 直接导致
- 总问题数差异与根因 C 强相关
- 第三大类AI 对话助手):
- 目前更像“上游口径漂移传导”而非对话工具不可用
- 仍需在 Phase 2 做患者级真值核验
---
## 7. 下一步(建议立即执行)
1. P0 修复 `QcAggregator.aggregateRecordSummary`:改为 UPSERT或先补齐 record_summary 基础行再 UPDATE
2. P0 修复 `getTimeline`:支持 `startDate/endDate`,并与 `date` 兼容。
3. P0 统一 `passRate` 口径(按 Phase0 SSOT至少先让 cockpit/report/trend 展示同一主口径。
4. 修复后回归本项目 3 个关键断言:
- `record_summary` 记录数应为 `12`
- Dashboard/Report/Trend 的主通过率在同一时间窗内一致
- 时间线区间筛选对 `2026-03-08` 返回 `0`(当前数据下)
---
## 8. 第二轮回归看板18 问题闭环状态)
> 执行日期2026-03-08第二轮
> 结论口径:
> - `已闭环`:问题已修复并复测通过
> - `部分闭环`:核心问题缓解,但仍有体验/口径尾项
> - `未闭环`:仍存在明确缺陷或未完成验证
### 8.1 第一大类报告与关键事件11 项)
| # | 问题 | 当前状态 | 证据 / 说明 |
|---|---|---|---|
| 1 | D1/D2/D3D4/D6 与维度评分不一致 | **部分闭环** | 主通过率口径已统一;但 D2 的 `eventsChecked=1` 仍需继续核验业务定义。 |
| 2 | 质控 0 个受试者 | **已闭环** | `record_summary` 已恢复 `12` 条,`qc_project_stats.total_records=12`。 |
| 3 | 健康分与问题规模冲突(如健康分高但问题多) | **部分闭环** | 评分与问题计数可同时存在(不同公式);需补“评分解释文案”避免误读。 |
| 4 | 上方通过率 100 / 下方趋势 33 | **已闭环** | 当前 cockpit/report/trend 均为 `passRate=0`(同一快照)。 |
| 5 | 质控完成无热力图 | **已闭环** | cockpit 返回 heatmap `rows/columns` 非空。 |
| 6 | 执行摘要无信息 | **已闭环** | report summary 可稳定返回 `totalRecords/criticalIssues/warningIssues/pendingQueries`。 |
| 7 | 报告分数 vs 大盘分数冲突 | **部分闭环** | 主指标已一致;健康分展示仍建议增加“更新时间+公式说明”。 |
| 8 | D1 无数据 | **已闭环** | D1 报表已恢复:`eligible=0, ineligible=11, incomplete=1`。 |
| 9 | D2 事件数=1、明细异常 | **部分闭环** | 已修复 `eventsChecked=5`(按活跃事件);同时新增 `d2EventsChecked=1` 标识 D2 覆盖事件数,避免误读。 |
| 10 | 已回复仍显示 0 | **已闭环** | 已执行 `pending -> responded -> closed(ai_review)` 实流回归,`equeries/stats` 与 D3D4 报表最终一致。 |
| 11 | 无“重开质疑”操作 | **未闭环** | 前端仍缺 reopen 操作入口(后端状态机支持)。 |
### 8.2 第二大类AI 实时工作流水3 项)
| # | 问题 | 当前状态 | 证据 / 说明 |
|---|---|---|---|
| 12 | 流水问题总数 vs 待处理总数不一致 | **部分闭环** | 已在口径上解释为“字段问题数 vs eQuery 数”;需在 UI 文案继续强化区分。 |
| 13 | 事件编码生成逻辑不明 | **部分闭环** | 已补齐后端 `event_id -> event_label` 统一映射(报表/对话可显示“筛选期”等);仍保留 event_id 作为证据辅助。 |
| 14 | 日期筛选按钮无效 | **已闭环** | timeline 支持 `startDate/endDate``2026-03-01~03-02` 返回 `0`。 |
### 8.3 第三大类AI 对话助手5 项)
| # | 问题 | 当前状态 | 证据 / 说明 |
|---|---|---|---|
| 15 | 已签署知情显示 0 | **已闭环** | 回归问答返回 `11/12`(签署率 91.7%)。 |
| 16 | 3号患者被误判为无问题 | **已闭环** | 回归问答已返回“不符合”并列出 rule 证据。 |
| 17 | 总体通过率答复异常 | **已闭环** | 回归问答返回 `0%`,并给出公式 `passedRecords/totalRecords`。 |
| 18 | 患者2信息不全 / 患者4访视描述偏差 | **已闭环** | 患者2信息完整度提升患者4访视描述已优先输出业务事件名保留 event_id 仅作证据)。 |
---
## 9. 严重未闭环问题(当前 P0/P1
### P0必须优先清零
1. ~~**D2 统计口径残留问题**`eventsChecked=1` 与多事件现实不一致。~~(已完成:`eventsChecked` 修复 + `d2EventsChecked` 解释字段)
2. ~~**“已回复仍为0”缺少场景化回归**:需要构造 responded/reviewing/closed 数据流转验证。~~(已完成:状态流转回归通过)
### P1应在下一轮完成
1. ~~**事件标签可读性**technical event_id 需稳定映射到 eventLabel报表 + 对话统一)。~~(已完成第一阶段:后端统一映射,前端显示可读事件名)
2. **EQuery 重开操作入口**:前端补 `reopen` 按钮与状态回流。
3. **评分解释文案**:健康分与问题数量并存时给出公式与更新时间提示。
---
## 10. 下一步执行计划(建议)
1. **P0-AD2 口径修复**
- 明确 activeEvents 定义(按患者真实到访事件)
- 修正 `getCompletenessReport()``eventsChecked` 计算
- 回归断言:`eventsChecked``qc_event_status` 的 D2 事件集合一致
2. **P0-BeQuery 状态流转回归**
- 构造一条 `pending -> responded -> reviewing/closed` 流程
- 验证eQuery 列表、stats、D3D4 报表、对话回答四处一致
3. **P1事件标签统一**
- 补全 `event_id -> event_label` 映射策略(优先 metadata其次兜底格式化
- 前端与 AI 对话统一使用 `eventLabel`,避免技术 ID 直出
---
## 11. 本轮执行证据(新增)
### 11.1 D2 口径修复结果
- 接口:`GET /api/v1/admin/iit-projects/:projectId/qc-cockpit/report/completeness`
- 修复后结果:
- `eventsChecked=5`(项目活跃事件数)
- `d2EventsChecked=1`D2 当前覆盖事件数)
- 受试者样例 `recordId=1``activeEvents=5``d2CoveredEvents=1`
### 11.2 eQuery 状态流转回归结果
- 操作:选取一条 pending eQuery调用 `POST /equeries/:id/respond`
- 实际状态流:
- `pending -> responded -> closed (ai_review)`(异步作业触发)
- 最终一致性(等待异步稳定后):
- `GET /equeries/stats`: `pending=261, closed=1, responded=0`
- `GET /qc-cockpit/report/equery-log`: 同步为 `pending=261, closed=1, responded=0`
---
## 12. eQuery 专项 P0 修复2026-03-08 夜间增量)
### 12.1 已落地代码改动
1. **eQuery 生成去重策略升级(后端)**
- 文件:`backend/src/modules/iit-manager/services/DailyQcOrchestrator.ts`
- 从原来的 `recordId + fieldName` 去重,升级为 `recordId + eventId + ruleName` 去重。
- 目的:减少“同一业务问题因不同字段重复建单”(对应问题 9
2. **高噪音规则抑制(后端)**
- 文件:`backend/src/modules/iit-manager/services/DailyQcOrchestrator.ts`
- 新增 `shouldSuppressIssue()`,当前先抑制三类已确认噪音:
- `不良事件记录与知情同意状态一致性检查`(缺上下文时高频误报)
- `所有纳入标准完整性检查` 且实际值为全 `1,1,...` 的错误告警
- `访视日期早于知情同意书签署日期` 但两日期相同的误报文本
3. **eQuery 文案可读性增强(后端)**
- 文件:`backend/src/modules/iit-manager/services/DailyQcOrchestrator.ts`
- 新增 `normalizeQueryText()`
- 清理 `(标准: [object Object])`
- 去掉 markdown `**` 噪音
- 对“入组状态与排除标准冲突检查”改写为业务可读提示语
4. **eQuery 上下文字段补齐(后端)**
- 文件:`backend/src/modules/iit-manager/services/DailyQcOrchestrator.ts`
- 创建 eQuery 时补写 `eventId/formName`,并在 `expectedAction` 中带出事件上下文。
- 目的:缓解“详情中表单/访视点显示不全(-)”。
5. **规则消息层通用序列化修复(后端)**
- 文件:
- `backend/src/modules/iit-manager/engines/HardRuleEngine.ts`
- `backend/src/modules/iit-manager/engines/SkillRunner.ts`
- 新增逻辑字面量与实际值格式化,避免 `expectedValue`/`actualValue` 落成 `[object Object]` 或不可读数组。
6. **eQuery 列表可用性增强(前端)**
- 文件:`frontend-v2/src/modules/iit/pages/EQueryPage.tsx`
- 新增:
- 质疑序号列
- 访视点列eventId
- 按严重程度过滤
- 按受试者号过滤
- 前端默认排序:按受试者号升序、同受试者按创建时间降序
### 12.2 状态评估(对应 14 条中的关键项)
- **部分闭环(需复测)**
- #4 排序混乱 / 建议增加序号:已实现前端排序+序号
- #9 同一目的重复记录:已增强去重键
- #10/#14 事件/表单显示不足:新单已补上下文,历史单仍需数据回填
- #12 告警文案与全 1 误报:已在派单前抑制
- **仍需继续(下一轮 P0**
- #3/#7/#11 的“规则本体逻辑”仍建议在规则配置层做根因修正(当前先做了派单层防噪)
- #5 纳入标准检查覆盖不全:仍需补齐规则集合与事件适用范围
- #13 缺少访视定位的业务解释:需在报表与详情页增加 eventLabel 映射显示
### 12.3 第二批 P0规则执行层护栏已落地
1. **规则执行层新增业务护栏HardRuleEngine / SkillRunner 双端一致)**
- 文件:
- `backend/src/modules/iit-manager/engines/HardRuleEngine.ts`
- `backend/src/modules/iit-manager/engines/SkillRunner.ts`
- 新增 `forcePassByBusinessGuard()`,在规则命中前做语义纠偏:
- `访视日期早于知情同意书签署日期`:当访视日期 >= 签署日期(含同日)直接判通过
- `SF-MPQ和CMSS评估日期与访视日期不一致`:存在缺失值时不判“不一致”
- `所有纳入标准完整性检查`:实际值全 1 时直接判通过
- `入组状态与排除标准冲突检查`:实际值全 0 时直接判通过
2. **D2 缺失告警事件名可读化**
- 文件:`backend/src/modules/iit-manager/engines/CompletenessEngine.ts`
- 告警文案从技术事件码切换为 `eventLabel`(如“筛选期”),减少 `65a64dbbd9_arm_1` 直接暴露。
> 以上为执行层兜底能立即压误报下一步仍建议在规则配置skill.config.rules层做同名规则逻辑重构避免长期依赖护栏。
### 12.4 同步完成 P1eQuery 重开入口
- 后端新增 `POST /api/v1/admin/iit-projects/:projectId/equeries/:equeryId/reopen`
- 文件:
- `backend/src/modules/admin/iit-projects/iitEqueryService.ts`
- `backend/src/modules/admin/iit-projects/iitEqueryController.ts`
- `backend/src/modules/admin/iit-projects/iitEqueryRoutes.ts`
- 状态流:`closed -> reopened`
- 前端已接入“重开”按钮
- 文件:
- `frontend-v2/src/modules/iit/api/iitProjectApi.ts`
- `frontend-v2/src/modules/iit/pages/EQueryPage.tsx`
- 仅在 `closed` 状态显示“重开”操作,执行后刷新列表。
---
## 13. 执行验证与卡点结论2026-03-08 晚)
### 13.1 复跑验证结论
- 脚本:`backend/scripts/run_iit_qc_once.ts`
- 项目:`1d80f270-6a02-4b58-9db3-6af176e91f3c`原发性痛经0302
- 结果(成功):
- `batch.totalRecords=12`
- `batch.totalEvents=37`
- `batch.fieldStatusWrites=1015`
- `orchestrate.pushSent=true`
- `orchestrate.equeriesCreated=0`
### 13.2 “卡在哪里”的根因
- 本次并非主流程卡死,`QcExecutor -> DailyQcOrchestrator` 已完整跑通并返回结果。
- 之前观察到的告警根因是:
- 审计日志 SQL 使用了旧列 `action`
- 当前表 `iit_schema.audit_logs` 实际列为 `action_type`
- 已修复后复跑,相关 `audit_logs.action` 报错未再出现。
### 13.3 历史 eQuery 上下文回填结果
- 脚本:`backend/scripts/backfill_equery_context.ts`
- 回填结果:
- `missingBefore=262`
- `updatedRows=203`
- `missingAfter=59`
### 13.4 剩余 59 条未回填原因
- 脚本:`backend/scripts/analyze_missing_equery_context.ts`
- 统计:`B_RULE_MATCH_FIELD_MISMATCH = 59`100%
- 含义:
- 能匹配到同受试者 + 同规则名
- 但匹配不到同字段名(历史 eQuery 以 `exclusion_criteria2/3/4/5` 多字段拆条,现有 `qc_field_status` 粒度/字段记录不一致)
- 代表样例:`category=入组状态与排除标准冲突检查``event_id/form_name` 为空。
### 13.5 容错回填(二阶段)执行结果
- 已升级脚本:`backend/scripts/backfill_equery_context.ts`
- Phase 1严格匹配`record + rule + field`
- Phase 2容错匹配`record + rule`,取最近 QC 事件/表单)
- 本次执行结果:
- `missingBefore=59`
- `strictUpdatedRows=0`
- `relaxedUpdatedRows=59`
- `missingAfter=0`
- 复核脚本:`backend/scripts/analyze_missing_equery_context.ts`
- `reasonStats=[]`
- `sample=[]`
- 结论:历史 eQuery 的 `event_id/form_name` 缺失已清零。
---
## 14. 新增端到端脚本(四流程一体)
- 脚本:`backend/scripts/e2e_iit_full_flow.ts`
- 运行方式:
- `npx tsx scripts/e2e_iit_full_flow.ts 1d80f270-6a02-4b58-9db3-6af176e91f3c`
- 可选:`--with-chat`(增加 LLM 问答链路)
### 14.1 覆盖的 4 个流程
1. REDCap 结构与真实数据拉取metadata/events/form-event/records-by-event
2. 规则配置加载与覆盖校验(`qc_process` 活跃规则)
3. 执行质控与报告编排(`QcExecutor` + `DailyQcOrchestrator`
4. 多消费端一致性校验Cockpit / Report / Tools 的通过率一致)
### 14.2 本次执行结果(通过)
- Stage1_REDCap通过
- `metadata=74`, `events=5`, `formEventMapping=19`, `recordEventRows=37`, `uniqueRecords=12`
- Stage2_Rules通过
- `ruleCount=79`, `multiFieldRules=29`, `categories={D1,D3,D5,D6}`
- Stage3_Execution通过
- `totalRecords=12`, `totalEvents=37`, `fieldStatusWrites=1015`
- DB`qc_field_status=713`, `qc_event_status=25`, `record_summary=12`
- Stage4_Consumption通过
- `reportPassRate=0`, `cockpitPassRate=0`, `toolPassRate=0`(一致)
---
## 15. 元数据驱动护栏落地(去硬编码)
### 15.1 执行内核收敛
- 已完成:`SkillRunner` 的 HARD_RULE 执行改为单路径复用 `HardRuleEngine.executeWithRules()`
- 价值:删除重复实现,避免“同一规则两套行为”。
### 15.2 guardType 元数据写回(项目级)
- 脚本:`backend/scripts/suggest_guard_types_for_project.ts`
- 运行:`npx tsx scripts/suggest_guard_types_for_project.ts 1d80f270-6a02-4b58-9db3-6af176e91f3c --apply`
- 结果:`updated=5`
- `date_not_before_or_equal` ×1
- `pass_if_exclusion_all_zero` ×1
- `pass_if_all_ones` ×1
- `skip_if_any_missing` ×2
### 15.3 回归脚本拆分(职责清晰)
- 引擎机制 smoke`backend/scripts/regression_hardrule_guards.ts`(可写死样例)
- 项目动态回归:`backend/scripts/regression_hardrule_guards_by_project.ts`(从 `qc_process` 读取规则)
- 项目动态回归结果:`skipped=[]`,核心 guard 断言全部通过。
### 15.4 复跑端到端验证
- 脚本:`backend/scripts/e2e_iit_full_flow.ts`
- 结果Stage1~Stage4 全部通过(口径一致性维持)。
- 备注:本次编排阶段 `pushSent=false`(非主流程阻断项),不影响质控执行与报告一致性断言。
---
## 16. 守护门禁与兼容开关(新增)
### 16.1 guardType 门禁脚本
- 新增:`backend/scripts/validate_guard_types_for_project.ts`
- 用法:
- 检查:`npx tsx scripts/validate_guard_types_for_project.ts <projectId>`
- 严格失败:`npx tsx scripts/validate_guard_types_for_project.ts <projectId> --strict`
- 本项目执行结果strict`missingCount=0`, `mismatchCount=0`
### 16.2 E2E 增加严格 guard 覆盖断言
- 脚本:`backend/scripts/e2e_iit_full_flow.ts`
- 新增参数:
- `--strict-guards`
- 或环境变量 `E2E_REQUIRE_GUARD_TYPES=1`
- 严格模式下Stage2 会校验 guardType 候选规则的覆盖率(未覆盖即 fail
### 16.3 兼容兜底可控下线
- 文件:`backend/src/modules/iit-manager/engines/HardRuleEngine.ts`
- 新增开关:`IIT_GUARD_LEGACY_NAME_FALLBACK`
- 默认开启(兼容历史规则名)
- 设为 `0` 可关闭规则名兜底,仅按 `metadata.guardType` 生效
- 目的:支持“先迁移配置,再逐步下线兼容逻辑”的平滑策略。