Commit 6ee7b8e9 by luoqi

fix(摄入): 姑息处置改为 route 到 review —— 不进真治疗类目,但也不再丢弃

季炎萍 TS0B010543 牙41~37:下颌活动义齿戴数年,2026-03-19 因压痛来修整边缘。
  主诉「下颌活动义齿戴牙数年余」/ 现病史「…有压痛,今来就诊」
  检查所见「义齿密合良好,边缘过长,牙龈略红肿」/ 处置「调磨过长边缘,抛光」
  本次治疗 treatName =「调磨」→ 被  块丢弃 → **她库里零条治疗事实**,
  系统看成"有诊断、从来没治过" → 判「缺失牙未启动修复」,还推种植(75 岁)。
客服反馈:abandon_reasons={inaccurate,other} / inaccurate_treatments={implant} /
备注「BPS已修复」—— 客服抓的是"推种植不准",根子是那条治疗根本没入库。

── 纠正原  块的根因分析 ──
原文:「category 同时被①治疗史完整性 ②resolver 判缺口已解 两处消费,只有一个旋钮,
只能二选一(现选都不要=丢弃)。要两全需在 category 之外再加一维"是否根治"」。
**前提不成立** —— 第二个旋钮一直都在,就是 review 类目,且它就是为这件事设计的:
  PACTreatmentCategories.review:「医生做了某个流程节点 / 临床判断本次不动手」
    「不该塞 preventive(会污染已做预防判定),也不该丢弃(丢失事实)」
  结构家族注释:「刻意排除 periodontic / orthodontic / preventive / **review**」
  实测:review 出现在 0 个 resolver 家族里
即 review 精确地给①不给② —— 冠周炎冲洗上药之后仍照常召拔牙,但治疗史里看得到。
(我中途还错误建议过映射到 preventive —— review 的注释明写着那样会污染"已做预防"判定。)

── 影响面(DW 全量实测)──
含这五词的 EMR 5,184;treat_plan **全是**这五词的 4,336 → 进治疗史,不解任何缺口。

──  单独一条 route,不并进 &review_terms ──
那个 anchor 被 C.9 dispose 闸门复用(blank_or_all_in)。并进去会同时打开闸门,而那
4,336 份处置 **100% 非空**:TOP400 处置跑分类器,约 216 条落进结构 resolver 家族,
且含「冠周冲洗,派力奥上药」→prosthodontic(因为含"冠"字)、「去暂封…玻璃离子暂封」
→restorative 这类误判 → 会误销缺口。dispose 闸门维持现状(不开 = 不回退,是放弃
一块带风险的额外收益),要开另行评估。
两个消费方语义本就不同:route 问"哪些名字不是真治疗",闸门问"何时可安全抽处置"。
route 算子支持两条规则指向同一 output(outputs 初始化在推数据之前完成),已实测。

顺带:TREATED_EVIDENCE_COMPLETION_RE 量词补「数/多/几」—— 真实主诉大量写
「戴牙数年余」「多年」而非确切数字,不认就漏(季炎萍即是)。DW 实测 +4 份。

测试新增 palliative-route-to-review.spec.ts(11 例),直接加载真实 manifest 锁住:
① 五词落 review 表、真治疗不受影响、建议仍走 recommendation;
② dispose 闸门词表仍是原 34 词且不含这五词、两条 route 指向同一 output 是手段不是笔误。
98 suites / 1708 tests,tsc 0 error。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent 79e63437
...@@ -78,13 +78,27 @@ keyword_mapping: ...@@ -78,13 +78,27 @@ keyword_mapping:
- { value: surgical, any: [拔除, 拔牙, 切开, 翻瓣, 切除, 系带, 脓肿, 囊肿, 植骨, 缝线] } - { value: surgical, any: [拔除, 拔牙, 切开, 翻瓣, 切除, 系带, 脓肿, 囊肿, 植骨, 缝线] }
- { value: pediatric, any: [乳牙, 儿童, 年轻恒牙] } - { value: pediatric, any: [乳牙, 儿童, 年轻恒牙] }
# ⛔ 刻意不收(2026-08-27 评估过,别"顺手补全"): # ⛔ 不收进本表(冲洗 / 换药 / 上药 / 调磨 / 试戴)—— 但**不再丢弃**:
# 冲洗 / 换药 / 上药(~3,500 条)—— **对症处置 ≠ 根治**。阻生牙冠周炎「冲洗上药」之后那颗牙 # 它们是「对症处置 ≠ 根治」:阻生牙冠周炎「冲洗上药」之后那颗牙仍然要拔;
# 仍然要拔;归 surgical 会把 K01 阻生牙缺口消掉 = 该召的不召(少召不报错,最难发现)。 # 归 surgical 会把 K01 阻生牙缺口消掉 = 该召的不召(少召不报错,最难发现)。
# 调磨(817)/ 试戴(213)—— 姑息调整 / 修复流程中途,都不代表问题已解决。 # 所以不能进本表的任何真治疗类目。
# ⚠️ 根因:本表的 category 同时被两处消费 —— ①治疗史完整性 ②resolver 判"缺口已解"。 #
# 对这几个词我们想要①不想要②,而现在只有一个旋钮,只能二选一(现选"都不要"=丢弃)。 # 🔴 2026-08-28 重评并纠正了此处原来的根因分析。原文写着:
# 要两全需在 category 之外再加一维"是否根治",那是更大的改动,不在本表解决。 # 「category 同时被①治疗史完整性 ②resolver 判缺口已解 两处消费,只有一个旋钮,
# 只能二选一(现选都不要=丢弃)。要两全需在 category 之外再加一维"是否根治"」
# —— **前提不成立**。第二个旋钮一直都在,就是 `review` 类目,而且它就是为这件事设计的:
# PACTreatmentCategories.review 注释:「医生做了某个流程节点 / 临床判断本次不动手」
# 「不该塞 preventive(会污染已做预防判定),也不该丢弃(丢失事实)」;
# 结构家族注释:「刻意排除 periodontic / orthodontic / preventive / **review**」;
# 实测 review 出现在 **0 个** resolver 家族里。
# 即 review 精确地给①不给② —— 冠周炎冲洗上药之后仍照常召拔牙,但治疗史里看得到。
#
# 现状:这五个词由 manifest 的 route_by_pattern 分流到 _treatment_review_raw
# (category=review),不进本含词表。DW 全量 treat_plan 全是这五词的 EMR 4,336 份。
# 触发个案:季炎萍 TS0B010543 —— treat_plan 只有「调磨」被丢 → 她库里零条治疗事实。
# ⛔ 别把它们补进本表:进来就等于归了某个真治疗类目 = 会解缺口。
# 也别把它们并进 manifest 的 &review_terms —— 那个 anchor 被 dispose 闸门复用。
# 两条约束都由 tests/palliative-route-to-review.spec.ts 锁住。
# actual 语义:这些词开头的从句 = 本次没做(条件/未来/建议)→ 切段丢弃后再匹配。 # actual 语义:这些词开头的从句 = 本次没做(条件/未来/建议)→ 切段丢弃后再匹配。
# 例 "充填,必要时根管治疗" → 丢"必要时根管治疗" → "充填" → restorative(不误判成 endodontic)。 # 例 "充填,必要时根管治疗" → 丢"必要时根管治疗" → "充填" → restorative(不误判成 endodontic)。
......
...@@ -572,6 +572,27 @@ transforms: ...@@ -572,6 +572,27 @@ transforms:
- 拒绝拍片 - 拒绝拍片
- 治疗中 - 治疗中
- 已交付纸质病历 - 已交付纸质病历
# ── 姑息处置 → 同样落 _treatment_review_raw(category=review)──
# 2026-08-28 重评「⛔ 刻意不收」那几个词(见 treatment-category-actual-rules.yaml 文末)。
# 那个块的根因写着「category 被治疗史与 resolver 两处消费,只有一个旋钮,只能二选一
# (现选都不要=丢弃)」—— **前提不成立**:第二个旋钮一直都在,就是 review 类目,
# 而且它就是为这件事设计的(见 PACTreatmentCategories.review 注释:「医生做了某个流程
# 节点/临床判断本次不动手」「不该塞 preventive,也不该丢弃」),且 review 出现在
# **0 个** resolver 家族里(结构家族注释明写「刻意排除 …preventive / review」)。
# 所以 review 精确地给①治疗史完整性、不给②解缺口 —— 冠周炎冲洗上药之后仍照常召拔牙。
# 触发个案:季炎萍 TS0B010543 牙41~37,下颌活动义齿戴数年,本次「调磨」过长边缘,
# treat_plan 只有「调磨」被丢 → 她库里**零条治疗事实**,系统看成"从没治过"。
# 影响面:DW 全量 treat_plan 全是这五词的 EMR 4,336 份 → 进治疗史,不解任何缺口。
#
# ⛔ **单独一条 route,不并进上面的 &review_terms** —— 那个 anchor 被 C.9 dispose 闸门
# 复用(blank_or_all_in),并进去会同时打开闸门:那 4,336 份处置 100% 非空,实测
# TOP400 处置里约 216 条会落进结构 resolver 家族,且含「冠周冲洗,派力奥上药」
# →prosthodontic(含"冠"字)、「去暂封…玻璃离子暂封」→restorative 这类误判,
# 会误销缺口。dispose 闸门维持现状(不开=不回退),要开另行评估。
# 两个消费方语义本就不同:route 问"哪些名字不是真治疗",闸门问"何时可安全抽处置"。
- output: _treatment_review_raw
when:
equals: [调磨, 试戴, 冲洗, 换药, 上药]
# 真治疗动作 → treatment_actual_rows ⭐ kind=actual(临床真实) # 真治疗动作 → treatment_actual_rows ⭐ kind=actual(临床真实)
- output: _treatment_actual_raw_emr - output: _treatment_actual_raw_emr
when: when:
......
...@@ -201,3 +201,19 @@ describe('调合 / 调颌 → prosthodontic(2026-08-28 陈惠英 TS0K028757)', ( ...@@ -201,3 +201,19 @@ describe('调合 / 调颌 → prosthodontic(2026-08-28 陈惠英 TS0K028757)', (
expect(cat('调磨')).toBeUndefined(); expect(cat('调磨')).toBeUndefined();
}); });
}); });
// ═══════════════════════════════════════════════════════════════
// 2026-08-28 重评「⛔ 刻意不收」:调磨/试戴/冲洗/换药/上药 改为**路由到 review**
// (manifest route_by_pattern),不再整条丢弃。它们仍然**不进本含词表** ——
// review 由 route 分流产生,词表里没有任何映射到 review 的条目。
// 这里锁住"不要顺手把它们补进含词表"(补进去 = 归了某个真治疗类目 = 会解缺口)。
// ═══════════════════════════════════════════════════════════════
describe('⛔ 姑息处置词不得出现在含词表里(它们走 route → review)', () => {
it.each(['调磨', '试戴', '冲洗', '换药', '上药'])('%s 仍不被含词表接管', (s) => {
expect(cat(s)).toBeUndefined();
});
it('⭐ 但「调合」是例外 —— 宿主精确表早已判定它是修复动作,不是姑息调整', () => {
expect(cat('调合')).toBe('prosthodontic');
});
});
/**
* 姑息处置词(调磨/试戴/冲洗/换药/上药)→ route 到 review,不再整条丢弃。
*
* ── 为什么改 ──
* 原「⛔ 刻意不收」块的根因写着「category 被治疗史与 resolver 两处消费,只有一个旋钮,
* 只能二选一(现选都不要=丢弃)」—— 前提不成立:第二个旋钮就是 review 类目,
* 且它就是为这件事设计的(PACTreatmentCategories.review:「医生做了某个流程节点/
* 临床判断本次不动手」「不该塞 preventive,也不该丢弃」),review 在 0 个 resolver 家族里。
* 触发个案:季炎萍 TS0B010543 —— treat_plan 只有「调磨」被丢 → 库里零条治疗事实。
*
* ── 🔴 本测试锁两件事,任一被"顺手优化"掉都会静默出事 ──
* ① 这五个词必须落 _treatment_review_raw(既不被丢,也不进真治疗表)
* ② **它们不得进 &review_terms 那个 anchor** —— 那张表被 C.9 dispose 闸门复用,
* 并进去会同时打开闸门。实测那 4,336 份 EMR 处置 100% 非空,TOP400 里约 216 条
* 会落进结构 resolver 家族,含「冠周冲洗,派力奥上药」→prosthodontic(含"冠"字)、
* 「去暂封…玻璃离子暂封」→restorative 这类误判 → 误销缺口。
*
* 跑:
* pnpm test -- palliative-route-to-review
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as yaml from 'js-yaml';
import { runRouteByPattern } from '../src/modules/sync/transforms/operators/route-by-pattern.op';
const PALLIATIVE = ['调磨', '试戴', '冲洗', '换药', '上药'] as const;
const manifest = yaml.load(
fs.readFileSync(path.join(__dirname, '../data/jvs-dw/manifest.yaml'), 'utf-8'),
) as { transforms: Array<Record<string, unknown>> };
const treatPlanRoute = manifest.transforms.find(
(t) => t.kind === 'route_by_pattern' && t.input === '_treat_plan_raw',
) as { routes: Array<{ output: string; when: Record<string, unknown> }> };
const disposeGate = manifest.transforms.find(
(t) => t.kind === 'filter' && t.output === '_emr_dispose_gate',
) as { where: { treat_plan: { blank_or_all_in: string[] } } };
const route = (names: string[]): Record<string, string[]> => {
const { outputs } = runRouteByPattern(
treatPlanRoute as never,
names.map((n) => ({ treat_name: n })) as never,
);
return Object.fromEntries(
Object.entries(outputs).map(([k, rows]) => [
k,
(rows as Array<{ treat_name: string }>).map((r) => r.treat_name),
]),
);
};
describe('① 姑息处置 → _treatment_review_raw', () => {
it('五个词全部落 review 表', () => {
const out = route([...PALLIATIVE]);
expect(out['_treatment_review_raw']).toEqual([...PALLIATIVE]);
expect(out['_treatment_actual_raw_emr'] ?? []).toEqual([]);
});
it('⛔ 真治疗不受影响,仍走 actual', () => {
const out = route(['树脂充填', '调合', '抛光', '种植戴牙']);
expect(out['_treatment_actual_raw_emr']).toEqual(['树脂充填', '调合', '抛光', '种植戴牙']);
expect(out['_treatment_review_raw'] ?? []).toEqual([]);
});
it('原有复查词行为不变(同落 review 表)', () => {
expect(route(['常规复查'])['_treatment_review_raw']).toContain('常规复查');
});
it('「建议…」仍走 recommendation,不被本条 route 抢走', () => {
expect(route(['建议种植修复'])['_recommendation_raw']).toEqual(['建议种植修复']);
});
});
describe('② 🔴 dispose 闸门必须维持原状(anchor 不得合并)', () => {
it.each([...PALLIATIVE])('%s 不得出现在 dispose 闸门词表里', (w) => {
expect(disposeGate.where.treat_plan.blank_or_all_in).not.toContain(w);
});
it('闸门词表仍是原来那 34 个复查/流程词', () => {
expect(disposeGate.where.treat_plan.blank_or_all_in).toHaveLength(34);
expect(disposeGate.where.treat_plan.blank_or_all_in).toContain('常规复查');
});
it('⭐ 两条 route 指向同一个 review 表 —— 这是拆 anchor 的手段,不是笔误', () => {
const toReview = treatPlanRoute.routes.filter((r) => r.output === '_treatment_review_raw');
expect(toReview).toHaveLength(2);
});
});
...@@ -43,6 +43,10 @@ describe('正例 —— 生产真实主诉/现病史,必须判为已治疗', () ...@@ -43,6 +43,10 @@ describe('正例 —— 生产真实主诉/现病史,必须判为已治疗', ()
['种植戴牙半年余', 'prosthodontic'], ['种植戴牙半年余', 'prosthodontic'],
['种植戴牙4月余按约复查', 'prosthodontic'], ['种植戴牙4月余按约复查', 'prosthodontic'],
['上颌活动义齿不贴合1周余,今来就诊', 'prosthodontic'], ['上颌活动义齿不贴合1周余,今来就诊', 'prosthodontic'],
// ⭐ 量词含「数/多/几」—— 季炎萍 TS0B010543,不认就漏
['下颌活动义齿戴牙数年余', 'prosthodontic'],
['患者下颌活动义齿戴牙数年余,有压痛,今来就诊', 'prosthodontic'],
['全口义齿戴用多年', 'prosthodontic'],
['戴牙后2周复查,自述无特殊不适。', 'prosthodontic'], ['戴牙后2周复查,自述无特殊不适。', 'prosthodontic'],
])('%s → %s', (text, cat) => { ])('%s → %s', (text, cat) => {
expect(evidenceCats(text)).toContain(cat); expect(evidenceCats(text)).toContain(cat);
...@@ -118,6 +122,12 @@ describe('表自身自洽', () => { ...@@ -118,6 +122,12 @@ describe('表自身自洽', () => {
expect(pats).toContain('戴牙'); expect(pats).toContain('戴牙');
}); });
it('⭐ 量词收「数/多/几」—— 主诉常写"数年余/多年"而非确切数字', () => {
expect(TREATED_EVIDENCE_COMPLETION_RE).toContain('数');
expect(new RegExp(TREATED_EVIDENCE_COMPLETION_RE).test('戴牙数年余')).toBe(true);
expect(new RegExp(TREATED_EVIDENCE_COMPLETION_RE).test('戴用多年')).toBe(true);
});
it('🔴 ⛔ 症状词不能当完成标记(脱落=需要重做,该召回)', () => { it('🔴 ⛔ 症状词不能当完成标记(脱落=需要重做,该召回)', () => {
expect(TREATED_EVIDENCE_COMPLETION_RE).not.toContain('脱落'); expect(TREATED_EVIDENCE_COMPLETION_RE).not.toContain('脱落');
expect(TREATED_EVIDENCE_COMPLETION_RE).not.toContain('松动'); expect(TREATED_EVIDENCE_COMPLETION_RE).not.toContain('松动');
......
...@@ -591,8 +591,10 @@ export const TREATED_EVIDENCE_RESTORATION_TERMS: ReadonlyArray<{ ...@@ -591,8 +591,10 @@ export const TREATED_EVIDENCE_RESTORATION_TERMS: ReadonlyArray<{
/// 🔴 ⛔ **不收症状词**(脱落/松动/折断)。原先收了,理由是"不存在的东西不会掉" —— /// 🔴 ⛔ **不收症状词**(脱落/松动/折断)。原先收了,理由是"不存在的东西不会掉" ——
/// 那个推理只证明它**曾经**存在,不证明它**现在**在位。而对「缺失牙未修复」这个判定, /// 那个推理只证明它**曾经**存在,不证明它**现在**在位。而对「缺失牙未修复」这个判定,
/// 脱落恰恰等于需要重做 = 该召回。生产实测误销:「前牙牙冠脱落数日」「金属固定桥松动」。 /// 脱落恰恰等于需要重做 = 该召回。生产实测误销:「前牙牙冠脱落数日」「金属固定桥松动」。
/// 量词含「数/多/几」:真实主诉大量写「戴牙**数年余**」「多年」「几个月」而非确切数字
/// (季炎萍 TS0B010543 主诉「下颌活动义齿戴牙数年余」——不认就漏)。
export const TREATED_EVIDENCE_COMPLETION_RE = export const TREATED_EVIDENCE_COMPLETION_RE =
'[0-90-9一二三四五六七八九十两半]+[个]?(天|周|月|年)|按约|按计划|复查|复诊'; '[0-90-9一二三四五六七八九十两半数多几]+[个]?(天|周|月|年)|按约|按计划|复查|复诊';
/// 未发生标记 —— 命中即整段不作数。语义与 keyword_strip_clauses 一致(剥"还没做"), /// 未发生标记 —— 命中即整段不作数。语义与 keyword_strip_clauses 一致(剥"还没做"),
/// 另补主诉特有的诉求词(要求/拟/打算/咨询/意向)。⛔「计划」不收 —— 会误伤「按计划复诊」。 /// 另补主诉特有的诉求词(要求/拟/打算/咨询/意向)。⛔「计划」不收 —— 会误伤「按计划复诊」。
......
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