Commit c9435b74 by luoqi

fix(recall): 移除 ⑤d 预约科目排除 — 全口/牙位统一靠治疗判定

数据验证(905 样本)推翻'全口需要 ⑤d'的假设:
- 全口(K05/K07):正在治的治疗都有记录 → resolvedTeeth 已全覆盖(K05挡669/K07挡73);
  ⑤d 独占多挡仅 0~1 个,且那个无近期预约=stalled(看过没继续)本就该召。'在治没录'风险=0。
- 牙位(K02/K03/K08…):⑤d 按科目(非牙位)误排'同科目别牙在治、这颗没治'(沈静芳 34;43 修复被
  25;26 修复预约连带排除)。
→ ⑤d 既冗余(全口)又有害(牙位)→ 移除。全口/牙位统一: resolvedTeeth(治疗) + ⑤b(未来预约) + ⑤f(近期到诊)。
  '已进入链'的细粒度跟踪留 W5+ 治疗链内召回。
- 顺带删 complaintTexts / APPT_COMPLAINT_TO_CATEGORY import;verify-recall.sql scanner 同步去 ⑤d 桶(oracle 与生产一致)。
- 验证:FP 硬闸 0/0、FP=0、真·无法解释 FN=0;未治→召 1524→1556(+32 合法);沈静芳 34;43 修复召回(分30,image_ai+老→排末尾,不冲高)。
- 注:年龄门(K07>40 不召正畸)按用户决定暂不做 → persona/召回在'正畸'上仍有意保留不一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 5cbde837
......@@ -135,7 +135,7 @@ SELECT *,
CASE
WHEN x_future THEN '1_未来预约⑤b'
WHEN x_recent THEN '2_近期到诊⑤f'
WHEN x_appt THEN '3_预约科目⑤d'
-- ⑤d(预约科目)W7 已从生产移除 → scanner 同步不再用它解释 FN(保持 oracle 与生产一致)
WHEN x_inelig OR x_decid THEN '4_废用牙/乳牙'
WHEN x_thirdmolar OR x_congenital OR x_orthoextract THEN '5_§E剔除(智齿/先天/正畸减数)'
WHEN x_superseded THEN '6_同牙更晚诊断取代'
......
......@@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import {
PlanScenario,
APPT_COMPLAINT_TO_CATEGORY,
lookupDxTreatment,
resolverCategoriesFor,
diagnosisCodeNameZh,
......@@ -237,15 +236,6 @@ export class TreatmentInitiationRecallScenario implements PlanScenarioPlugin {
const expectedCats = rule.categories as readonly string[];
const resolverCats = resolverCategoriesFor(cfg.primaryCode) as readonly string[];
// W4 末:按 expectedCats 算出对应的预约 complaint 文本(host appointment.complaint_category 字段值)
// APPT_COMPLAINT_TO_CATEGORY 反查:filter complaint where category ∈ expectedCats
// 例 K07 expectedCats=['orthodontic'] → complaintTexts=['正畸','早矫']
// K08 expectedCats=['implant','prosthodontic'] → ['种植','修复']
// 用于 ⑤d:sig 之后有 complaint 匹配的 appointment → 患者已 entered 治疗链,不召回
const complaintTexts = Object.entries(APPT_COMPLAINT_TO_CATEGORY)
.filter(([, cat]) => expectedCats.includes(cat))
.map(([text]) => text);
// 单 patient 收窄(详情页"刷新"):设了 scope.patientId → 只扫该患者,O(全租户)→O(1)
const patientFilter = scope.patientId
? Prisma.sql`AND p.id = ${scope.patientId}::uuid`
......@@ -364,22 +354,11 @@ export class TreatmentInitiationRecallScenario implements PlanScenarioPlugin {
AND appt.status = 'active'
AND COALESCE(appt.planned_for, appt.occurred_at) > ${scope.now}::timestamptz
)
AND NOT EXISTS ( -- ⑤d 排除:sig 之后 complaint 匹配的预约(W4 末加)
-- 跟 chain-composer.collectS2Facts 同口径:complaint_category 命中 expectedCategories → 患者已 entered
-- 例 林菲菲:K07@2026-04-14 诊断 + appointment(complaint=正畸)@2026-04-26 → 已进入正畸链 → 排除
-- "现在召回只做新链" 原则:entered/ongoing 患者不属于新链召回,等 W5+ 治疗链内召回 scenario
-- active 未到诊 + fulfilled 已到诊都算(只要有 complaint 匹配)
SELECT 1 FROM patient_facts appt
WHERE appt.patient_id = p.id
AND appt.type = 'appointment_record'
AND appt.status IN ('active', 'fulfilled')
AND COALESCE(appt.planned_for, appt.occurred_at) >= COALESCE(sig.occurred_at, sig.planned_for)
AND EXISTS (
SELECT 1
FROM unnest(string_to_array(COALESCE(appt.content->>'complaint_category', ''), ',')) AS c
WHERE trim(c) = ANY(${complaintTexts}::text[])
)
)
-- ⑤d(预约科目排除)已移除(W7):
-- 数据验证 → 全口场景 resolvedTeeth(看治疗)已覆盖在治患者(K05 挡669/K07 挡73,⑤d 独占仅0~1
-- 且那1个无近期预约=stalled本就该召);牙位场景 ⑤d 按科目(非牙位)误排"同科目别牙在治、这颗没治"
-- (沈静芳 34;43 修复 被 25;26 修复预约连带排除)。⑤d 既冗余又有害 → 去掉,全口/牙位统一靠
-- resolvedTeeth(治疗) + ⑤b(未来预约) + ⑤f(近期到诊)。"已进入链"的细粒度留 W5+ 治疗链内召回。
-- (⑤e 同牙位替代治疗 种植/冠桥 已折进上面 resolved_teeth 的 implant/prosthodontic 分支 — 诊断后做了的从 remaining 减掉)
AND NOT EXISTS ( -- ⑤f 就诊冷静:近 N 天到过诊 → 别催刚来过的人(防打扰)
-- 患者级、与具体信号无关:最近一次到诊(encounter/emr)在 N 天内 → 本轮不召。
......
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