Commit a73f5d7c by luoqi

fix: S2 done 加 S3 反推 — 已治疗必然已进入

逻辑漏洞:
  之前 s2Done = !!s2Earliest(只看强信号)
  → 牙周链 S3 已治疗(actual 刮治),但因无 appointment 信号 → S2 done=(矛盾)

修复:
  s2Done = !!s2Earliest || s3Reached
  → S3 reached 反推 S2 必然已经过(已治疗 ⇒ 已进入治疗链)

S2 node 文案优先级(5 路):
  ① s2Earliest=appointment → "预约就诊 · {主诉}"
  ② s2Earliest=payment → "已付款 · ¥X"
  ③ plannedHint 存在 → "{subtype} · 已开计划"(已执行)/"已开计划(待执行)"(未执行)
  ④ S3 reached 但无 planned → "直接执行 · 未经预约"(急诊)
  ⑤ status=discovered 无信号 → "尚未启动" + hint

路遥牙周链验证:
  S2 ✓ "牙周刮治术 · 已开计划 · 段路路 · 2026.03.11"(用 plannedHint 展示)
  S3 ✓ "刮治 · 1/3 步骤"

K01/K07/K08 仍 ★ 召回(医生计划只是 hint,患者未行动)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
parent 9416620a
......@@ -858,19 +858,28 @@ function buildStageNodes(opts: {
})();
// ─── S2 进入治疗链 ─────────────────────────────────
// W4 末:S2 done 标准严格化 — **必须有患者主动行为**(预约 complaint 匹配 / 付款 / 到诊)
// ❌ 不再把"医生 EMR 开了 planned treatment" 算 S2 done(医生动作 ≠ 患者承诺)
// ✅ planned treatment 仅作为 hint 展示("医生计划是 XX 但患者还没动"),done=false
// W4 末两阶段判定:
// 1. status=entered/ongoing/closed 的判定看 collectS2Facts(必须患者主动行为)
// 2. S2 node 的 done 展示标准更宽:**只要 S3 达到 → S2 自动 done**(已治疗 ⇒ 已进入)
// 因为患者实际接受了治疗,必然"进入"过治疗链(不管经预约还是直接到诊)
//
// 临床场景:
// - 急诊补牙 / 检查发现龋直接当场修(S1→S3 直跳,没经过预约修复主诉)→ "直接执行"
// - 医生开了计划但患者没行动 → "已开计划(待执行)"提示客服
// S2 node 文案优先级(从强到弱):
// ① 预约 complaint 匹配 → "预约就诊 · {主诉}"
// ② 付款 → "已付款 · ¥X"
// ③ 有 plannedHint(医生开了计划)→ "{subtype} · 已开计划"
// - 如果 S3 reached:"已开计划"(已执行)
// - 如果 status=discovered:"已开计划(待执行)" 给客服暗示
// ④ S3 reached 无任何上面信号 → "直接执行 · 未经预约"(急诊场景)
// ⑤ status=discovered 无任何信号 → "尚未启动" + hint
const plannedHint = findPlannedTreatmentHint(category, byType, s1Earliest?.occurredAt ?? null);
const s3Reached = matchedSteps.matched.length >= (milestone?.minSteps ?? 1) || actuals.length > 0;
const s2Node: ChainNode = (() => {
const s2Done = !!s2Earliest;
// S2 done:① 有 s2Earliest 强信号 OR ② S3 已到达(治疗都做了一定经过 S2)
const s2Done = !!s2Earliest || s3Reached;
const n: ChainNode = { stage: 2, at: '—', done: s2Done, current: cur(2), missing: !s2Done };
// 文案优先级 ① s2Earliest 强信号
if (s2Earliest) {
// S2 真命中(患者主动行为)
const c = s2Earliest.content as Record<string, unknown>;
if (s2Earliest.type === FactType.APPOINTMENT_RECORD) {
const complaint = String(c.complaint_text ?? c.complaint_category ?? '').trim();
......@@ -887,23 +896,24 @@ function buildStageNodes(opts: {
n.at = fmt(effectiveTime(s2Earliest));
return n;
}
// S2 无命中(患者未行动)— 看 status 决定文案
if (status === 'discovered') {
if (plannedHint) {
// 医生已开计划但患者还没动 — 给客服暗示,但不打 done ✓
const pc = plannedHint.content as Record<string, unknown>;
n.title = shortLabel(String(pc.subtype ?? '') || '医生计划');
n.detail = '已开计划(待执行)';
n.doctor = resolveDoctorName(plannedHint, doctorMap);
n.at = fmt(effectiveTime(plannedHint));
} else {
n.title = '尚未启动';
n.hint = `建议${expectedHint}`;
}
} else {
// S3+ 状态但 S2 没命中 → 患者跳过预约直接来诊(常见于急诊/复诊连带)
// ② 没强信号 — 优先用 plannedHint 展示(医生开了计划)
if (plannedHint) {
const pc = plannedHint.content as Record<string, unknown>;
n.title = shortLabel(String(pc.subtype ?? '') || '医生计划');
n.detail = s3Reached ? '已开计划' : '已开计划(待执行)';
n.doctor = resolveDoctorName(plannedHint, doctorMap);
n.at = fmt(effectiveTime(plannedHint));
return n;
}
// ③ 没强信号 也没 planned — 看 status
if (s3Reached) {
n.title = '直接执行';
n.detail = '未经预约';
} else {
n.title = '尚未启动';
n.hint = `建议${expectedHint}`;
}
return 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