Commit 3e31eb58 by luoqi

fix: 牙周链(lifelong_maintenance + wholeMouth)误标已闭环 + S3 步骤 0/3

路遥 case:
  K05 全口牙周 + actual 牙周刮治术全口 26 牙
  另有 K08 21;41 + planned 延期种植 21;41
  → 之前:牙周链被"种植修复·21;41" alt-close → 显示"已闭环",且 S3 显示"0/3 步骤"
  → 修后:牙周链保持 ongoing 维护期(终身),S3 "牙周刮治 · 1/3"

修 2 处:
1. chain-composer.markAlternativeClosed 加 2 个豁免:
   - lifelong_maintenance(periodontic 等 maxStage=4)→ 永不被替代闭环
     (慢性病终身维护,做种植 21;41 不代表整体牙周不需要继续治疗)
   - wholeMouth(牙位 ≥ 20 颗 视为全口)→ 不被 per-tooth 链替代
     (全口治疗 vs 局部种植 临床上是"维护 + 局部",不是替代)
2. canonical-codes.periodontic milestone steps:
   ['全口洁治','龈下刮治','牙周维护'] → ['洁治','刮治','维护']
   旧版全词匹配导致"牙周刮治术" 命中 0/3,改"词根"匹配后 1/3 ✓

不需要重导数据(chain-composer 实时算);只需重启 pac-service。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
parent 53d0127e
......@@ -150,6 +150,13 @@ function markAlternativeClosed(chains: ComposedChain[]): void {
for (const c of chains) {
if (c.status !== 'discovered' && c.status !== 'entered') continue;
if (!c.toothPosition || !c.diagnosedAt) continue;
// ⭐ W4 末新加豁免 #1:lifelong_maintenance 链(periodontic 牙周等)永不被替代闭环
// 牙周治疗是慢性病终身维护,做种植 21;41 不代表牙周不需要继续 → 路遥误标 closed 修
const cLifecycle = lookupTreatmentLifecycleByCategory(c.category);
if (cLifecycle.maxStage === 4) continue;
// ⭐ W4 末新加豁免 #2:wholeMouth 链(全口治疗,牙位 ≥ 20 颗)不被 per-tooth 链替代
// 全口洁治覆盖 26 颗牙 vs 种植 21;41 这 2 颗 — 临床上是"维护 + 局部治疗",不是替代
if (isWholeMouthTooth(c.toothPosition)) continue;
for (const other of chains) {
if (other === c) continue;
if (!ALT_CATS.has(other.category)) continue;
......@@ -170,6 +177,20 @@ function markAlternativeClosed(chains: ComposedChain[]): void {
}
}
/// W4 末:全口治疗判定(牙位字符串拆分后 ≥ 20 颗 视为全口;26~28 颗是成人全恒牙)
function isWholeMouthTooth(tp: string): boolean {
if (!tp) return false;
if (tp === '*whole' || tp.toLowerCase() === 'whole') return true;
const teeth = tp.split(/[;,,;\s]+/).map((s) => s.trim()).filter(Boolean);
return teeth.length >= 20;
}
/// W4 末:按 category 反查 lifecycle(给 markAlternativeClosed 用)
function lookupTreatmentLifecycleByCategory(category: string) {
const ms = lookupTreatmentMilestone(category);
return ms ? lookupTreatmentLifecycle(ms.lifecycle) : DEFAULT_LIFECYCLE;
}
// ─────────────────────────────────────────────
// 5 阶段引擎核心
// ─────────────────────────────────────────────
......
......@@ -299,7 +299,10 @@ export const TreatmentMilestones = {
implant: { steps: ['种植体植入', '种植上部修复', '种植牙冠修复', '种植冠修复'], minSteps: 2, lifecycle: 'linear' },
endodontic: { steps: ['开髓', '根管充填'], minSteps: 2, lifecycle: 'linear' },
orthodontic: { steps: ['矫治器', '保持器'], minSteps: 1, lifecycle: 'long_term' },
periodontic: { steps: ['全口洁治', '龈下刮治', '牙周维护'], minSteps: 1, lifecycle: 'lifelong_maintenance' },
// W4 末:steps 改为更宽的"治疗动作词根",匹配 host subtype 包含即可
// "牙周刮治术" includes "刮治" / "全口龈上洁治" includes "洁治" / "牙周维护" includes "维护"
// 旧版用全词("全口洁治""龈下刮治")导致路遥牙周刮治术匹配 0/3 步骤误判
periodontic: { steps: ['洁治', '刮治', '维护'], minSteps: 1, lifecycle: 'lifelong_maintenance' },
restorative: { steps: ['充填', '嵌体'], minSteps: 1, lifecycle: 'one_shot' },
prosthodontic: { steps: ['冠', '桩核', '修复'], minSteps: 1, lifecycle: 'one_shot' },
surgical: { steps: ['拔除', '拔牙', '手术'], minSteps: 1, lifecycle: 'one_shot' },
......
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