1. 25 May, 2026 12 commits
    • feat: lifecycle linear_then_crown — 根管闭环额外要冠保护 · cadbe1d6
      临床背景:
        根管治疗后牙变脆(髓腔抽空 + 牙体含水量下降),不戴冠保护 2-3 年内
        ~30% 概率牙冠折裂(literature: Caplan & Weintraub 1997 等)
        所以"根管完成 + 牙体充填" 不算真闭环,必须 + "冠修复" 才算完整治疗
      
      改动:
        1. canonical-codes.ts:
           - TreatmentLifecycle 加 requiresCrownProtection?: boolean
           - 新加 lifecycle = 'linear_then_crown' (maxStage=5, 需冠保护)
           - endodontic.lifecycle 'linear' → 'linear_then_crown'
        2. chain-composer:
           - 新加 hasCrownProtection(bucketTooth, byType, anchor) helper
             检查同牙位 prosthodontic actual,treat_stages 含 crown_restoration/post_core
             fallback subtype.includes('冠'/'桩核')
           - S5 闸口加 crownOk 条件:lifecycle.requiresCrownProtection 时必须满足
           - buildStageNodes 透 crownOk 给 S5 节点 → hint "待冠保护(...防牙冠折裂)"
      
      验证(杨光宗 27 K04):
        改前:closed stage=5(根管+充填+复诊 触发 S5 → 误闭环)
        改后:ongoing stage=4 → S5 "未闭环 · 待冠保护(根管后建议戴冠,防牙冠折裂)"
      
      副作用:其他做了根管但没戴冠的患者也会从 closed → ongoing
        → 客服可视化看到缺口,适合补做"根管后冠修复召回"scenario(future)
      
      不影响 SQL recall(scenario 仍按 treatment_initiation_recall ⑤a actual overlap 排除,
      K04 27 有 endodontic actual 仍被排除,不会重新进召回池;
      真正"该补冠"的召回是另一个 scenario,后续加)
      
      Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
      luoqi committed
    • feat: 摄入 treat_stages 字段 + PAC 标准 step enum · a57a58a8
      设计原则:字段名 + 内容都用 PAC 标准,host 字面值在 yaml 翻译
        - canonical 跟宿主无关(其他宿主接入只改 yaml,代码不动)
        - host "开髓"/"拔髓" → PAC pulp_extirpation
        - host "根备"/"根管预备" → PAC canal_preparation
        - host "根充"/"根管充填" → PAC canal_filling
        - host "种植体植入"/"种植一期" → PAC implant_placement
        - host "种植上部修复"/"种植冠修复"/"种植戴牙" → PAC crown_placement
        - 等 22 个 PAC 标准 step
      
      改动:
        1. canonical-codes.ts:
           - 新加 PACTreatmentStep enum(22 个,英文 snake_case + 中文 label)
           - TreatmentMilestones.steps 改用 PAC step key(不再用中文词)
           - 新加 LegacyStepSubtypeKeywords:host 没填 stages 时词根 fallback
        2. canonical.ts: TreatmentCanonicalSchema 加 treatStages?: string[]
        3. fact-content-schemas.ts: treatment_record.content 加 treat_stages
        4. manifest.yaml § C.1/C.2: element_fields 加 treatStages
        5. treatment_actual.yaml + treatment_planned.yaml:
           - field_mapping 加 treatStages
           - enum_mapping 新加 treatStages 段(host 字面 → PAC enum,22 条)
        6. assembler-engine.applyEnum: 支持 array 字段(每元素 lookup mapping)
        7. treatment.parser: content.treat_stages 存数组(已 PAC 标准)
        8. chain-composer.matchMilestoneSteps:
           - 优先用 treat_stages 精确匹配 PAC step
           - fallback 用 LegacyStepSubtypeKeywords + subtype.includes()
           - S3 title 用 PACTreatmentStep 字典渲染中文(非 enum key)
      
      验证(杨光宗 27 牙):
        入库:
          4-10 stages=["pulp_extirpation"] ← host "开髓"
          4-20 stages=["canal_preparation"] ← host "根备"
          5-04 stages=null ← host 没填,落空
          5-18 stages=null ← 充填 actual host 没填
        Chain:
          根管治疗 S3 done ✓ "开髓/拔 → 根管预备" / 已完成 3 次
          牙体修复 S3 done ✓ "树脂充填" / 已完成 1 次(走 subtype 词根 fallback)
      
      待办(后续):
        - lifecycle="linear_then_crown" 新加,endodontic 闭环额外要求 prosthodontic actual(冠)
        - 当前杨光宗 27 没戴冠仍 closed,临床其实差最后一步
      
      Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
      luoqi committed
    • ui: S3 步骤展示改"已完成 N 次",字典分子分母不直观 · e44e599e
      旧版 "1/3 步骤" 含义 = 命中字典 step 种类数 / 字典总 step 种类
         杨光宗 27 根管 3 次 actual 同 subtype → 字典只 hit 1 种 "根管" → 1/3
         客服看 1/3 像"差 2 步"误导,实际 minSteps=1 已满足,临床完成
      
      新版:
        s3Reached 满足   → "已完成 N 次"(N=actual.length,直观)
        s3Reached 不满足 + multi-step(种植 minSteps=2) → "X/Y 步骤(进行中)"
        s3Reached 不满足 + single-step → "N 次治疗"
      
      验证:
        杨光宗 K04 根管(3 次 actual): "根管 · 已完成 3 次"
        杨光宗 K03 充填(1 次 actual): "充填 · 已完成 1 次"
        路遥 K05 牙周(2 次刮治):      "刮治 · 已完成 2 次"
      Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
      luoqi committed
    • fix: endodontic milestone 字典词根化(杨光宗 27 根管 bug) · fd7ab151
      杨光宗 27 牙真实临床:
        2026-04-10/04-20/05-04  3 次根管治疗(磨牙) actual
        2026-05-18              K03 + 非美学区树脂充填 actual
      临床:完整"根管 + 后续充填修复"链 ✓ 已闭环
      
      改前 UI:根管治疗·27 ★潜在新链(0/2 步骤)  错!
      根因:endodontic milestone steps=["开髓","根管充填"] minSteps=2
           host subtype "根管治疗(磨牙)" includes 任一 step 都 false → matched=0/2
           → s3Reached=false → status=discovered → ★ 误标
      
      修法(跟 periodontic 同款词根化):
        steps: ["开髓","根管充填"] → ["根管","根充","开髓"]
        minSteps: 2 → 1
        rationale:
          - "根管治疗(磨牙)" includes "根管" ✓
          - "根管充填" includes "根充" ✓
          - "开髓" includes "开髓" ✓
        minSteps 降 1 因:host subtype 不区分 step 类型(同次 actual 字典只能 hit 1),
        强制 minSteps=2 实际上要求 2 个 distinct step,但 host 永远只 1 个匹配
      
      改后验证:
        根管治疗·27: status=closed stage=5 target=False(已闭环,跟 K03 后充填+复诊一起)
        S3 done=True "根管·1/3 步骤"
        SQL ⑤a 排除生效:K04 被 endodontic actual overlap 排除,不进召回池
      
      向下兼容:其他患者根管动作真正写"开髓"/"根管充填"的也照常 hit
      Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
      luoqi committed
    • fix: S2 done 加 S3 反推 — 已治疗必然已进入 · a73f5d7c
      逻辑漏洞:
        之前 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>
      luoqi committed
    • fix: S2 严格化 — planned treatment 不再触发 entered 状态 · 9416620a
      路遥 case(N=4 chains):
        改前:K01 智齿拔除 / K08 种植 因 EMR.plan 有医生计划 → status=entered(误)
        改后:status=discovered(正确)— 患者未预约/付款/到诊
              plannedHint 仍展示 "延期种植术 · 已开计划(待执行)" 给客服暗示
      
      W2/W3 旧版用 planned treatment 当 S2 fallback,跟 collectS2Facts 顶部注释
      "医生侧动作 ≠ 患者承诺" 自相矛盾。W4 末彻底清理。
      
      S2 真信号(只 1 路):
        appointment.complaint_category 匹配 → 患者主动预约(挂号/约时间/到店)
      
      planned treatment 信息没浪费:
        - 不进 s2Hits(不升 status=entered)
        - 在 S2 node 渲染时作为 plannedHint 展示 "已开计划(待执行)" + done=false
        - 客服看到"医生计划是 XX 但患者还没动" — 仍属召回目标
      
      副作用预期:
        - 召回率会上升(之前误升 entered 的现在回 discovered,被召回)
        - 准确率上升(真没动作的患者被正确召回)
      
      新加 helper:findPlannedTreatmentHint(category, byType, s1AnchorTime)
        从 byType.treatment 找同 category 的 planned(s1 之后,最早一条)— 纯展示用
      
      Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
      luoqi committed
    • fix: 牙周链(lifelong_maintenance + wholeMouth)误标已闭环 + S3 步骤 0/3 · 3e31eb58
      路遥 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>
      luoqi committed
    • fix: 删 yaml duplicate key(OHI,涂氟 / OHI,涂氟) · 53d0127e
      js-yaml strict mode 不允许 duplicate key,cold-import 时报错:
        treatment_actual.yaml:272 'OHI,涂氟' 重复(连续两行同 key 同值)
        treatment_planned.yaml:266-267 'OHI,涂氟'(中逗号)+ 'OHI,涂氟'(英逗号) 共存
      
      normalize op 上线后 DW 中文逗号版自动归一到英文版,中逗号 yaml 条目变 dead config。
      删掉 dup / dead,留唯一 'OHI,涂氟' → preventive。
      
      100 患者验证:cold-import success patients=100 txns=0 dups=10235 facts(c=0 u=9810)
      (数据全部 unchanged,因 source_event_id 幂等,只刷新 fact metadata)
      召回:18 plans(瑞泰 13 + 瑞尔 5),持平 / 略降于改前 23 plans —
      EMR 单源 actual 让排除更准(真治疗才算覆盖),净效应略降但更精确。
      luoqi committed
    • W4: scenario SQL 排除升级牙位级 overlap(EMR.treat_plan 带 48.7% 牙位) · 89e68c1a
      之前 ⑤a 排除是 patient + category 级("DW 限制" 注释);
      EMR.treat_plan 进来后 actual 带牙位,可以升级到 tooth-level overlap。
      
      逻辑(3 路 OR):
        ① 信号无牙位(K05 全口诊断)→ 仍 patient/category 级
        ② actual 无牙位(全口洁治/牙周治疗)→ 视为"全口覆盖"→ 仍排除
        ③ 双方都有牙位 → tooth array overlap(PG && 操作符)
      
      实现细节:
      - regexp_replace 把"15 B;24 B"非数字非分号字符替换为 ; → "15;24"
      - string_to_array + array_remove '' 去空元素(关键:两 array 都有 '' 时 '' = '' 误返 true)
      - PG && 任一元素相同即 overlap
      
      收益例子:
      - 罗国标 K04 14;15 + actual endodontic 36 → tooth overlap false → 正确召回 14;15
      - 之前 patient/category 级会被误排除
      
      向下兼容:48.7% actual 带牙位 → 这部分走 tooth-level 精筛;
      其余 52% actual 无牙位(全口治疗)→ 走 ② 分支,跟现状一致(全口覆盖)。
      
      不需要重导(SQL 改动,数据不动);下次 recompute-plans 立刻生效。
      
      Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
      luoqi committed
    • W4: transforms 加 normalize op + enum_mapping 覆盖率 74→98% · 194778d9
      背景:
      DW top 200 treatName 实测,treatment 字典覆盖率仅 74%(15.8% 漏配 _default skip)。
      3 大根因:
        ① 中英文标点不一致(yaml 写英文,DW 实际中文)— 50%+ 漏配
        ② review/recommendation route 关键词不全 — 已交付纸质病历/转诊等流程性误吃 actual
        ③ 真治疗新词漏配 — 牙周序列治疗/桩冠修复/根管治疗后冠修复 等
      
      修复(原则:代码跟宿主无关,宿主个性化只在 yaml):
      1. transforms.derive 加 op=normalize(trim 升级版 + CJK→ASCII 中段标点)
         - 中括号 ()→ ()  | 中逗号 , → ,  | 中分号 ; → ;
         - 中冒号 : → :  | 中尖括号 <> → < >  | 中百分号 % → %
         - 顿号 、 → ,(语义等价分隔符)
         - 任何中文宿主通用,不是 jvs-dw 特化 — 进 transforms(通用层),不进 yaml(宿主层)
      
      2. manifest:
         - § C 加 normalize derive on treat_name(treat_plan + plan 两路 in-place 覆盖)
         - § B.1 diagnosis message 从 trim 升级到 normalize
         - § C.3 review route 关键词补 ~22 项(正畸复诊/检查/咨询/会诊/复查/转诊/已交付病历/缴费等)
         - § C.4 plan 字段 review drop 也同步补
      
      3. treatment_actual.yaml + treatment_planned.yaml 同步补 ~15 个新词:
         periodontic:  牙周序列治疗 / 系统性牙周治疗 / 全口洁治+OHI / 龈上洁治术/.../洁牙/洗牙
         endodontic:   根管治疗后冠修复 / RCT+冠修复
         implant:      拔除后种植
         prosthodontic: 桩冠修复
         restorative:  树脂充填术
         orthodontic:  更换新矫治器 / 粘接上半口矫治器 / 粘接全口附件 /
                       精调粘接附件 / 发放新矫治器 / 去除矫正器,配戴保持器保持现有咬合关系
         清理 1 个中文顿号 dead key("全口龈上洁治、抛光。" → normalize 后自动落到现有 ASCII 字典)
      
      4. diagnosis.yaml 补 2 个高频:
         K05 菌斑性牙龈炎(928 hits;yaml 原有"菌斑性龈炎"长写变体)
         K02 深窝沟(7613 hits;早期龋兆,临床归 K02)
      
      实测覆盖率(DW top 200,512K rows):
        treatment_actual:  74.0% → 99.9% (mapping 85.5 + review 11.7 + rec 2.6)
                           漏配从 80,879 → 598 行(剩 1 条长文本"拟涂氟知情同意"无业务价值)
        diagnosis:         87.1% → 90.5%
                           剩漏配 95% 是故意 drop(乳牙列/混合牙列/种植术后等 Z 类术后状态)
      
      不需要重导(代码先稳定);下次 cold-import 自动生效。
      
      Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
      luoqi committed
    • W4: 治疗事实信号源切到 EMR(treat_plan→actual / plan→planned),settlement 退出 · cc8c217c
      语义修正背景(真实数据验证):
      - EMR.treat_plan = 本次实际治疗(actual),字段名误导但语义就是 actual
      - EMR.plan        = 未来计划(planned),2023+ host 启用
      - settlement      = 财务事件,不是临床事件;颗粒度 1:N、0% 带牙位
      
      改造前:
      - treat_plan(actual)被当成 planned 
      - plan 字段完全没消费 
      - settlement 反推 treatment_actual,跟 EMR 双源混叠
      
      改造后:
      - treat_plan 真治疗 → treatment_actual_rows kind=actual  带 48.7% 牙位
      - plan       真治疗 → treatment_planned_rows kind=planned  带牙位
      - settlement 不再产 treatment_record,职责单一(LTV/退费)
      
      文件改动:
      - manifest.yaml § C:treat_plan + plan 双源 split + route(MVP:plan 的复查/建议暂 drop)
      - manifest.yaml § D:删除 settlement → _treatment_actual_raw / treatment_actual_rows 派生
      - assemblers/treatment_actual.yaml:source 切到 EMR,字典复用 treatment_planned.yaml 同款(200+ entries)
      - assemblers/treatment_planned.yaml:仅头部注释更新(源切到 plan 字段)
      - assemblers/refund.yaml / payment.yaml:不动(settlement 继续走这两路)
      
      不动的下游:
      - treatment.parser.ts:kind 由 emits.action 决定,yaml 改完自动正确
      - chain-composer.service.ts / treatment-initiation-recall.scenario.ts:读 fact.kind 抽象层,自动受益
      
      待办:
      - 暂不重导(代码层先稳定);下一轮 TRUNCATE + cold-import 看真实效果
      - recommendation_rows 双源 union(transforms 加 union op 或 parser 侧 dedup)
      - treatment_review_rows 双源 union + kind 区分(actual review vs planned review)
      - EMR 漏录 fallback:某些治疗只有 settlement 没 EMR.treat_plan 的兜底策略
      
      Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
      luoqi committed
    • W4: 治疗链 5 阶段 + AI 话术 + DB 持久化 + 真实诊所多 brand 接入 · 36686f66
      主要工作(自 W3 末快照以来):
      
      数据层(canonical-fact-layer):
      - 治疗链 5 阶段模型(chain-composer S1/S2/S3/S4/S5)+ TreatmentMilestones + TreatmentLifecycles 字典
      - alternative-closed 闸:同位置后续替代治疗覆盖原诊断 → 标 closed
      - 同 (category, code) 桶按 tooth set overlap 合并(union-find);wholeMouth 桶 S2 修正
      - chain.target = SQL 为准(按 plan_reasons.signals.triggers 对齐,plan-aggregate 注入)
      - diagnosis name_zh 末尾标点清理;cooldown 内不标 ★ 潜在新链
      
      召回算法:
      - DiagnosisTreatmentMap K00-K09 全覆盖(加 K00/K01/K03/K06/K07/K09)
      - treatment_initiation_recall 10 个 sub-scenarios + 配套 *_RECOMMENDED 推荐码
      - 移除 INTAKE_MAX_DAYS 上界;scenario SQL 加预约排除(任何 sig 后预约即排)
      - S2 改用预约主诉类别;S2 fallback 显示 planned 治疗
      
      画像 Persona:
      - treatment_chain_status feature 直接复用 ChainComposerService
      - value/recall_risk/dnc feature 切到独立 fact_type(v2.1)
      - status: in ['active', 'fulfilled'] 加载兼容已完成 actual
      
      Plan 详情聚合:
      - /plans/:id/full 接 personas + chains + facts + 话术
      - script 从 plan_scripts DB 加载,markdown 反 parse 4 段 sections,前端零适配
      - 列表页搜索/过滤改服务端(W3 末)
      
      AI 话术(B 方案 重写):
      - DeepSeek V4 Pro → Flash 切换(call defaultModelId + config defaultModel)
      - schema 4 段 markdown 字符串(opening/followup/objection/close)对齐前端 mock
      - prompt @2026-05-24-d:few-shot demo + / 反例 + 事实约束硬要求
      - 事实漂移防护:诊所名 JVS_DW_CLINIC_NAMES 字典翻译、牙位 FDI→俗称、
        主诊医生从 facts 抽、通话称呼 nameSpokenForm(姓+先生/女士)
      - scenario 内部 label 禁外露,opening 必须用临床事实开场
      
      鉴权:
      - A 方案 refresh token 真实实现(Redis jti rotation,无 host SSO 回调)
      - 详情页电话查看 icon + reveal 接口
      
      UI:
      - 详情页 TopBar 跟列表页 PageHeader 风格统一
      - 治疗链 5 阶段词表(chainStatusVisual)
      - 闭环链去"建议下一步" + 闭环时间
      - WhyCard 过滤 alternative-closed reasons
      
      数据源接入:
      - 5 家试点诊所 JVS DW 实接(瑞尔/瑞泰双 brand,tenant_id 路由)
      - yaml transforms(Layer A.5)6 operator 白名单:split/route/derive/filter/project/pick_first_nonzero
      - 实测 5000 患者 cohort,408K facts / 2207 plans / 0 failed
      
      待办:
      - task #46 cold-import --reparse mode(yaml 改后强制重 parse)
      - EMR.treat_plan 语义错位(标记为 planned 实际是 actual);EMR.plan 字段未消费
      - 双源 actual 去重(EMR vs settlement)
      
      Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
      luoqi committed
  2. 22 May, 2026 1 commit
    • chore: 初始化仓库 — PAC 患者召回平台快照(W3 末) · 30196953
      含本阶段全部成果:
      - L1 摄入:4 层规范化(transforms 6+2 op / AssemblerEngine / parser+zod / Layer C 预留),
        4 入口统一管道,单 host 多 tenant(瑞尔/瑞泰),__source_row 真原文留底
      - 诊断覆盖率 18%→63%:中文名→K码白名单 + 无码落 fact 留 LLM(code_source 溯源)
      - 算法:treatment_initiation_recall 4 子规则(K08/K02/K05 + 新增 K04 根管),6 因子打分
      - 单一真理源 DiagnosisTreatmentMap(诊断→治疗类别,4 处引用)
      - 命名统一(encounter/emr/image),chain-composer 去中文关键词猜码
      - Persona 4 特征,plan targetClinicId 回填,token dictionary(诊所名)
      - 前端:话术 3 模式(伴飞/卡片/原文)、我的任务沉浸抽屉(触底分页)、
        列表 density 拉开、去头像、去无意义的推荐渠道/时间/角色
      - 文档:canonical-fact-layer / potential-treatment-recall(-flow) 对齐
      
      Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
      luoqi committed