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
21 KiB
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-6af176e91f3cname=原发性痛经0302redcap_project_id=18status=active
2. 第一轮 SQL 证据(核心)
2.1 聚合结果与原始质控事实明显不一致
iit_schema.qc_project_stats(该项目):total_records=0passed_records=0failed_records=0warning_records=0health_score=82.3d1_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(*)=713COUNT(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=82totalRecords=0passRate=100criticalCount=137queryCount=262
qc-cockpit/report返回:totalRecords=0criticalIssues=156warningIssues=284passRate=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=12startDate=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. 根因初判(带代码入口)
根因 A(P0):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问题”的冲突
根因 B(P0):时间线接口参数约定不一致
- 代码入口:
backend/src/modules/admin/iit-projects/iitQcCockpitController.ts getTimeline仅读取query.date,未处理startDate/endDate- 前端使用区间筛选时,后端实际不生效
根因 C(P1):同名指标多来源并行,无统一冻结口径
passRate在 cockpit/report/trend 来源不一致- 导致用户看到“同一页不同值”的信任问题
6. 与三大类问题的对应关系(第一轮)
- 第一大类(报告/关键事件):
- 已确认强相关于根因 A + 根因 C
- 第二大类(AI 实时工作流水):
- 已确认日期筛选问题由根因 B 直接导致
- 总问题数差异与根因 C 强相关
- 第三大类(AI 对话助手):
- 目前更像“上游口径漂移传导”而非对话工具不可用
- 仍需在 Phase 2 做患者级真值核验
7. 下一步(建议立即执行)
- P0 修复
QcAggregator.aggregateRecordSummary:改为 UPSERT(或先补齐 record_summary 基础行再 UPDATE)。 - P0 修复
getTimeline:支持startDate/endDate,并与date兼容。 - P0 统一
passRate口径(按 Phase0 SSOT),至少先让 cockpit/report/trend 展示同一主口径。 - 修复后回归本项目 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(必须优先清零)
D2 统计口径残留问题:(已完成:eventsChecked=1与多事件现实不一致。eventsChecked修复 +d2EventsChecked解释字段)“已回复仍为0”缺少场景化回归:需要构造 responded/reviewing/closed 数据流转验证。(已完成:状态流转回归通过)
P1(应在下一轮完成)
事件标签可读性:technical event_id 需稳定映射到 eventLabel(报表 + 对话统一)。(已完成第一阶段:后端统一映射,前端显示可读事件名)- EQuery 重开操作入口:前端补
reopen按钮与状态回流。 - 评分解释文案:健康分与问题数量并存时给出公式与更新时间提示。
10. 下一步执行计划(建议)
-
P0-A:D2 口径修复
- 明确 activeEvents 定义(按患者真实到访事件)
- 修正
getCompletenessReport()的eventsChecked计算 - 回归断言:
eventsChecked与qc_event_status的 D2 事件集合一致
-
P0-B:eQuery 状态流转回归
- 构造一条
pending -> responded -> reviewing/closed流程 - 验证:eQuery 列表、stats、D3D4 报表、对话回答四处一致
- 构造一条
-
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=0GET /qc-cockpit/report/equery-log: 同步为pending=261, closed=1, responded=0
12. eQuery 专项 P0 修复(2026-03-08 夜间增量)
12.1 已落地代码改动
-
eQuery 生成去重策略升级(后端)
- 文件:
backend/src/modules/iit-manager/services/DailyQcOrchestrator.ts - 从原来的
recordId + fieldName去重,升级为recordId + eventId + ruleName去重。 - 目的:减少“同一业务问题因不同字段重复建单”(对应问题 9)。
- 文件:
-
高噪音规则抑制(后端)
- 文件:
backend/src/modules/iit-manager/services/DailyQcOrchestrator.ts - 新增
shouldSuppressIssue(),当前先抑制三类已确认噪音:不良事件记录与知情同意状态一致性检查(缺上下文时高频误报)所有纳入标准完整性检查且实际值为全1,1,...的错误告警访视日期早于知情同意书签署日期但两日期相同的误报文本
- 文件:
-
eQuery 文案可读性增强(后端)
- 文件:
backend/src/modules/iit-manager/services/DailyQcOrchestrator.ts - 新增
normalizeQueryText():- 清理
(标准: [object Object]) - 去掉 markdown
**噪音 - 对“入组状态与排除标准冲突检查”改写为业务可读提示语
- 清理
- 文件:
-
eQuery 上下文字段补齐(后端)
- 文件:
backend/src/modules/iit-manager/services/DailyQcOrchestrator.ts - 创建 eQuery 时补写
eventId/formName,并在expectedAction中带出事件上下文。 - 目的:缓解“详情中表单/访视点显示不全(-)”。
- 文件:
-
规则消息层通用序列化修复(后端)
- 文件:
backend/src/modules/iit-manager/engines/HardRuleEngine.tsbackend/src/modules/iit-manager/engines/SkillRunner.ts
- 新增逻辑字面量与实际值格式化,避免
expectedValue/actualValue落成[object Object]或不可读数组。
- 文件:
-
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(规则执行层护栏)已落地
-
规则执行层新增业务护栏(HardRuleEngine / SkillRunner 双端一致)
- 文件:
backend/src/modules/iit-manager/engines/HardRuleEngine.tsbackend/src/modules/iit-manager/engines/SkillRunner.ts
- 新增
forcePassByBusinessGuard(),在规则命中前做语义纠偏:访视日期早于知情同意书签署日期:当访视日期 >= 签署日期(含同日)直接判通过SF-MPQ和CMSS评估日期与访视日期不一致:存在缺失值时不判“不一致”所有纳入标准完整性检查:实际值全 1 时直接判通过入组状态与排除标准冲突检查:实际值全 0 时直接判通过
- 文件:
-
D2 缺失告警事件名可读化
- 文件:
backend/src/modules/iit-manager/engines/CompletenessEngine.ts - 告警文案从技术事件码切换为
eventLabel(如“筛选期”),减少65a64dbbd9_arm_1直接暴露。
- 文件:
注:以上为执行层兜底,能立即压误报;下一步仍建议在规则配置(skill.config.rules)层做同名规则逻辑重构,避免长期依赖护栏。
12.4 同步完成 P1:eQuery 重开入口
-
后端新增
POST /api/v1/admin/iit-projects/:projectId/equeries/:equeryId/reopen- 文件:
backend/src/modules/admin/iit-projects/iitEqueryService.tsbackend/src/modules/admin/iit-projects/iitEqueryController.tsbackend/src/modules/admin/iit-projects/iitEqueryRoutes.ts
- 状态流:
closed -> reopened
- 文件:
-
前端已接入“重开”按钮
- 文件:
frontend-v2/src/modules/iit/api/iitProjectApi.tsfrontend-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=12batch.totalEvents=37batch.fieldStatusWrites=1015orchestrate.pushSent=trueorchestrate.equeriesCreated=0
13.2 “卡在哪里”的根因
- 本次并非主流程卡死,
QcExecutor -> DailyQcOrchestrator已完整跑通并返回结果。 - 之前观察到的告警根因是:
- 审计日志 SQL 使用了旧列
action - 当前表
iit_schema.audit_logs实际列为action_type
- 审计日志 SQL 使用了旧列
- 已修复后复跑,相关
audit_logs.action报错未再出现。
13.3 历史 eQuery 上下文回填结果
- 脚本:
backend/scripts/backfill_equery_context.ts - 回填结果:
missingBefore=262updatedRows=203missingAfter=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 事件/表单)
- Phase 1:严格匹配(
- 本次执行结果:
missingBefore=59strictUpdatedRows=0relaxedUpdatedRows=59missingAfter=0
- 复核脚本:
backend/scripts/analyze_missing_equery_context.tsreasonStats=[]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 个流程
- REDCap 结构与真实数据拉取(metadata/events/form-event/records-by-event)
- 规则配置加载与覆盖校验(
qc_process活跃规则) - 执行质控与报告编排(
QcExecutor+DailyQcOrchestrator) - 多消费端一致性校验(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=5date_not_before_or_equal×1pass_if_exclusion_all_zero×1pass_if_all_ones×1skip_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生效
- 目的:支持“先迁移配置,再逐步下线兼容逻辑”的平滑策略。