Commit 1e716414 by luoqi

fix(recall): 拆线/种植复查计入"治疗已启动"证据 — 修术后复诊期误召(一线反馈①)

口径(与产品对齐):诊断信号保真,新诊断必然参与召回判定,不动;
缺口在治疗证据侧 — 光杆"拆线"落 review 类不算治疗("种植拆线"在 assembler 已映
implant,同动作两种录法),术后复诊日 EMR 又录了缺失诊断 → 同日拆线无法证明
"已启动" → 误召(李然 BA38586 牙47,92 分被认领)。

review 类全量盘点后只放行【术后护理】两项(铁证:该牙做过手术/种植):
  拆线(带牙位 3672 条)/ 种植复查(739 条)
不放行:暂观/观察/暂观必要时拔除(明确未治疗,恰是召回对象)、泛复查/流程项。

实现:types 加 GAP_POSTOP_CARE_REVIEW_PATTERNS 单一源;gap 核心 resolvedTeethSql
加 (a') 术后护理分支(时间方向与治疗家族同口径);fact 层不动(chain S4 保留);
召回+画像共用同步生效。时间方向片段重构为 afterDxFor(alias)。

验证:服务器只读 李然47 新分支=t;本地全量 recompute-plans exit=0 零错;
影响面:带牙位术后护理记录覆盖 3,338 患者。
(此前"黄金窗回看"方案已 revert — 会压住治疗后窗内的真新诊断,口径不如本方案干净)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parent 3f979faa
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { import {
GAP_POSTOP_CARE_REVIEW_PATTERNS,
RESTORATION_INELIGIBLE_DX_NAMES, RESTORATION_INELIGIBLE_DX_NAMES,
STRUCTURAL_DX_CODE_LIST, STRUCTURAL_DX_CODE_LIST,
type DxTreatmentRule, type DxTreatmentRule,
...@@ -119,9 +120,12 @@ export function buildGapCore(input: GapCoreInput): GapCorePieces { ...@@ -119,9 +120,12 @@ export function buildGapCore(input: GapCoreInput): GapCorePieces {
: Prisma.sql`sig.content->>'tooth_position'`; : Prisma.sql`sig.content->>'tooth_position'`;
const wholeMouthFlag = rule.wholeMouth ? Prisma.sql`TRUE` : Prisma.sql`FALSE`; const wholeMouthFlag = rule.wholeMouth ? Prisma.sql`TRUE` : Prisma.sql`FALSE`;
const afterDxFragRtx = rule.excludeIfEverTreated /// 治疗时间方向片段(按表别名生成;rtx=治疗家族 / ptx=术后护理证据 共用同一口径)
? Prisma.sql`AND rtx.occurred_at >= ${latestDxOfCode}` const afterDxFor = (alias: string): Prisma.Sql =>
: Prisma.sql`AND rtx.occurred_at >= COALESCE(sig.occurred_at, sig.planned_for)`; rule.excludeIfEverTreated
? Prisma.sql`AND ${Prisma.raw(alias)}.occurred_at >= ${latestDxOfCode}`
: Prisma.sql`AND ${Prisma.raw(alias)}.occurred_at >= COALESCE(sig.occurred_at, sig.planned_for)`;
const afterDxFragRtx = afterDxFor('rtx');
// §E (d) 正畸减数位(仅 missing_tooth):该牙有外科拔除 + 患者有正畸语境 → 折进 resolved 减掉。 // §E (d) 正畸减数位(仅 missing_tooth):该牙有外科拔除 + 患者有正畸语境 → 折进 resolved 减掉。
const orthoExtractBranch = cfgFlags.excludeOrthoExtractionSites const orthoExtractBranch = cfgFlags.excludeOrthoExtractionSites
...@@ -155,6 +159,20 @@ export function buildGapCore(input: GapCoreInput): GapCorePieces { ...@@ -155,6 +159,20 @@ export function buildGapCore(input: GapCoreInput): GapCorePieces {
AND rtx.content->>'category' = ANY(${resolverCats}::text[]) AND rtx.content->>'category' = ANY(${resolverCats}::text[])
${afterDxFragRtx} ${afterDxFragRtx}
UNION UNION
-- (a') 术后护理 = 该牙已做手术/种植的铁证(拆线/种植复查;单一源 GAP_POSTOP_CARE_REVIEW_PATTERNS)。
-- 光杆"拆线"落 review 类(chain S4 信号,fact 保真),gap 侧将其计入处理证据 —
-- 否则术后复诊 EMR 复述诊断(如 李然 47:1/25 种植,2/7 拆线+复述"牙齿缺少")
-- 会把最新诊断推到治疗后 → 时间方向误判"未启动"。暂观/观察 = 未治疗,不在此列。
SELECT pct AS t
FROM patient_facts ptx
CROSS JOIN unnest(${toothArrSql(Prisma.sql`ptx.content->>'tooth_position'`)}) AS pct
WHERE ptx.patient_id = p.id
AND ptx.type = 'treatment_record' AND ptx.kind = 'actual'
AND ptx.status IN ('active', 'fulfilled')
AND ptx.content->>'category' = 'review'
AND ptx.content->>'subtype' LIKE ANY(${[...GAP_POSTOP_CARE_REVIEW_PATTERNS]}::text[])
${afterDxFor('ptx')}
UNION
-- (b) 同牙位以【最新诊断】为准:更晚的真实结构诊断 → 旧诊断对该牙失效 -- (b) 同牙位以【最新诊断】为准:更晚的真实结构诊断 → 旧诊断对该牙失效
SELECT ldt AS t SELECT ldt AS t
FROM patient_facts ldx FROM patient_facts ldx
......
# 一线反馈核验记录(2026-06-11 批次,21 例)
> 背景:试点客服(user 576)认领 21 个召回任务后逐一外呼/核档,手写反馈异议。
> 本文档逐条核验:一线说的对不对 → 系统为什么这么判 → 根因定性 → 修复与验证。
> 病历号/姓名已与库内真值比对(21/21 确认,与 assigned 计划一一对应)。
## 核验方法
1. 拉该患者 assigned plan 的 `plan_reasons`(scenario/牙位/天数/evidence factIds);
2. 拉证据 fact + 该患者相关牙位/类目的全部当前版 facts,还原时间线;
3. 与一线反馈对照,定性:误召(算法/数据问题)or 正确召回(一线信息差)or 数据源问题;
4. 误召 → 本地修复 + 新旧谓词对照验证 + 记录于此。
## 状态总览
| # | 病历号 | 姓名 | 反馈要点 | 核验结论 | 状态 |
|---|---|---|---|---|---|
| ① | BA38586 | 李然 | 47 已种植+拆线,不该召 | **误召 — 拆线未计为治疗证据** | ✅ 已修复 |
| ② | BJ0U005102 | 李姝妤 | 外院矫正,4.21 引(转)院 | 待核验 | ⬜ |
| ③ | BJ0A057103 | 秦溢泽 | 2026.1.27 取资料未回复 | 待核验 | ⬜ |
| ④ | BJ0U016979 | 祁小夏 | 36/41 缺,11.22 修复 | 待核验 | ⬜ |
| ⑤ | BJ0U017412 | 王敏 | 26.37.47;26 无间隙 | 待核验 | ⬜ |
| ⑥ | BJ0U017401 | 李强 | 46.36 间隙不足→建议正畸? | 待核验 | ⬜ |
| ⑦ | BJ0U015883 | 黄琳 | ✓(认可) | 待核验 | ⬜ |
| ⑧ | BJ0F022277 | 余奕铭 | "穿"→外院;"病历"→无意愿 | 待核验 | ⬜ |
| ⑨ | BJ0U016929 | 刘强 | ✓(认可) | 待核验 | ⬜ |
| ⑩ | BJ0U017487 | 李石明 | 37.47 缺失,活动义齿 25.12.23 | 待核验 | ⬜ |
| ⑪ | BJ0U017563 | 韩俊和 | 26.27;2.3 已种 2.12 戴牙/拆线 | 待核验(预期 ① 同款) | ⬜ |
| ⑫ | BJ0U017233 | 刘哲昕 | ✓ 正畸 | 待核验 | ⬜ |
| ⑬ | BJ0U016815 | 王晨 | ✓ 动态,回访过,未停,2次 | 待核验 | ⬜ |
| ⑭ | BJ0U007377 | 关平 | 25 磨牙萌出 ✗;2020.9.18 增平;外院矫正→完成后全面修复 | 待核验 | ⬜ |
| ⑮ | BJ0U017637 | 李鹏 | ✓ 15.27.47 缺失 | 待核验 | ⬜ |
| ⑯ | BJ0U016360 | 高美玲 | ✗ 36 缺失;EMR 固定桥修复,误:已修复 | 待核验 | ⬜ |
| ⑰ | BJ0U017344 | 陈秀玲 | ✗ 27;1.20 修复,活动义齿 | 待核验 | ⬜ |
| ⑱ | BJ0U017423 | 陈葳 | ✓ 46 缺失 | 待核验 | ⬜ |
| ⑲ | BJ0U017440 | 王延春 | ✓ 24.25 缺失 | 待核验 | ⬜ |
| ⑳ | BJ0E012466 | 宗明 | ✗ 15.25/36.44;正畸关闭了;①病历信息正确 ②无片子 | 待核验 | ⬜ |
| ㉑ | BJ0U010770 | 王颖 | ✓ 47;空间不足? | 待核验 | ⬜ |
---
## ① 李然 BA38586 — 47 缺失牙召回异议 ✅ 已修复
**一线反馈**:47 牙 2026.1.25 已种植、2.7 已拆线(治疗已启动),系统不该召回。
**库内时间线(47 牙)**:
```
2024-04-20 47 拔除(surgical actual)+ 种植 planned
2024-08-11 诊断「缺失 47」(K08)+ 种植 planned(患者爽约/改期,链中断)
2026-01-25 ✅ 简单种植术 actual · category=implant · 牙位47
+ 同日 planned「种植冠修复 47」+ CBCT
2026-02-07 EMR 诊断「牙齿缺少 47」(K08,数据事实,保真)
+ 同日 拆线 actual · 牙位47 · category=review ← 关键
2026-06-08 计划生成:「缺失牙未启动修复·牙位47 — 诊断 120 天前(=02-07),
未启动种植/修复/冠桥」priority 92 → 入池 → 被认领
```
**系统误判依据**:排除规则要求「同牙位的种植/修复类(implant/prosthodontic)治疗
≥ 最新诊断日」。2/7 有新诊断(数据事实,**保真,不动它**);同日的拆线本可证明
治疗已启动,但光杆"拆线"被归 category=review(流程类),**不计入治疗证据**
47 被判"未启动" → 误召。(注:"种植拆线"在 assembler 已映 implant,光杆"拆线"
才落 review —— 同一动作两种录法,一线录入习惯不可控。)
**review 类盘点(服务器全量,决定哪些算治疗证据)**:
| 类型 | subtype(带牙位条数) | 算治疗证据? |
|---|---|---|
| 术后护理(做过手术的铁证) | 拆线(3672)· 种植复查(739) | ✅ 计入 |
| 明确未治疗 | 暂观(1786)· 观察(1165)· 暂观必要时拔除(528)· 无治疗 | ❌ 恰是召回对象 |
| 流程/泛复查 | 常规复查/检查/取资料/缴费等(基本不带牙位) | ❌ 不构成证据 |
**修复**(诊断信号保真不动;只补"治疗证据"的缺口):
- `packages/types` 新增 `GAP_POSTOP_CARE_REVIEW_PATTERNS = ['%拆线%','%种植复查%']`
(单一真理源,含取舍说明);
- gap 核心 `resolvedTeethSql` 新增 (a') 分支:review 类中命中术后护理模式、带牙位、
时间方向与治疗家族同口径(≥ 最新诊断)→ 计入该牙"已处理"。
- fact 层不动(拆线仍是 review,chain S4 信号保留);仅 gap 消费侧补证据口径;
- 召回 + 潜在治疗画像共用 gap 核心,同步生效。
**验证**:
- 服务器只读:李然 47 — 新分支证据存在 = **t**(拆线 2/7 ≥ 最新诊断 2/7)✅
- 本地全量 plan 重算 exit=0、0 错误 ✅
- 影响面预估:全租户带牙位的拆线/种植复查记录覆盖 **3,338 患者**(部署后全量
重算,预期此类"术后复诊期"误召批量消失;对比报告部署时出)。
---
## ②-㉑ 待逐条核验
(按 ① 的格式逐条补充)
...@@ -228,6 +228,14 @@ export interface DxTreatmentRule { ...@@ -228,6 +228,14 @@ export interface DxTreatmentRule {
excludeIfEverTreated?: boolean; excludeIfEverTreated?: boolean;
} }
/// 术后护理型 review 子类(gap 排除侧"治疗已启动"的牙位级铁证)。
/// review 类是流程性事实(chain S4 信号,fact 层保真不动),但其中【术后护理】两项
/// 出现在某牙位上 = 该牙必然做过手术/种植(2026-06-11 一线反馈① 李然 47:1/25 种植、
/// 2/7"拆线"落 review 不算治疗 → 同日 EMR 复述"牙齿缺少47"成最新诊断 → 误召 92 分)。
/// ⚠️ 只放行术后护理:暂观/观察(带牙位 1786/1165 条)= 明确未治疗,恰是召回对象,不放;
/// 泛复查/检查不构成牙位级证据,不放。"种植拆线"在 assembler 已映 implant,不经此口。
export const GAP_POSTOP_CARE_REVIEW_PATTERNS = ['%拆线%', '%种植复查%'] as const;
export const DiagnosisTreatmentMap = { export const DiagnosisTreatmentMap = {
// 诊断码(K0x)— 跟同临床类目的推荐码(*_RECOMMENDED)窗口/临界严格一致 // 诊断码(K0x)— 跟同临床类目的推荐码(*_RECOMMENDED)窗口/临界严格一致
// W3 末扩 K00-K08 全覆盖(数据驱动:host DW 真实数据 244 万 EMR diag 频次扫描后落码,见 dw-data-source-issues #15) // W3 末扩 K00-K08 全覆盖(数据驱动:host DW 真实数据 244 万 EMR diag 频次扫描后落码,见 dw-data-source-issues #15)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment