fix(recall): 缺牙建议治疗按年龄排序 —— >70 岁活动义齿在前、种植并行

现象(业务反馈):90 岁(吕学文)、78 岁(李石明)的缺牙召回,界面一律显示
「目标 · 种植」,话术也推种植。业务反弹"为什么给 90 岁老人推种植"。

根因:UI 取 signals.expectedCategories[0],而 K08 的 categories 首项恒为 implant。
     整条链路(目标标签 / 理由文案 / goal / 话术)都没有年龄维度 ——
     召回场景 SQL 与 priority-scorer 里一个年龄条件都没有。

## 修法:改在诊断→治疗的源头,不在流程末端打特例

canonical-codes.ts 新增 recommendedCategoriesForAge(categories, age) 作建议侧单一源:
候选同时含 implant + prosthodontic 时,age > DENTURE_FIRST_AGE(70)→ prosthodontic 提前。
scenario / 前端 reason-line / 话术 prompt 全部调这一个 resolver,不各写年龄分支。

## 业务方口径(对齐后修正)

初版做成了"高龄禁推种植",过重。业务反馈:**70-80 不是种植禁忌**,能否种植取决于
骨量骨质 / 全身状况 / 患者意愿,由医生评估。故改为:
  - 建议**只调顺序**:活动义齿在前,**种植并行保留**(不删)
  - 话术从"严禁推荐种植"改为"先讲活动义齿,种植可并行提及;不主推手术、
    不承诺能不能种 —— 落到来院让医生按身体条件评估"
  - PAC 不替医生下禁忌结论

## 三条设计红线(各有用例锁住)

1. 只排序不增删 —— 任何年龄下返回集合恒等于入参(种植不会被抹掉)
2. 不碰排除闸 —— DiagnosisTreatmentMap[].categories 仍是完整候选集
   (否则高龄患者做过种植反而排除不掉 → 重复召回)
3. 不碰"要不要召回" —— 缺牙影响咀嚼营养,高龄同样该管

## 改动

- canonical-codes:DENTURE_FIRST_AGE=70 + recommendedCategoriesForAge + treatmentCategoryNameZhFor
- reason-signals:signals 加 focusCategory / patientAge(均 optional,旧 plan 回落原行为)
- scenario:SQL 取年龄 → 走 resolver;高龄 goal 文案(义齿为主、种植并行)
- 前端:目标标签用 focusCategory;理由行调同一 resolver 显示「活动义齿 / 种植」
- 话术:fact-block + stable prompt 加高龄沟通约束;三档 promptVersion bump

实测(本地 75 岁真实患者重算):focusCategory=prosthodontic · patientAge=75 ·
理由「…未启动活动义齿 / 种植」。308 tests passed,service + web typecheck 干净。

️ 存量:plan 变化判定只比 (scenario, subKey),signals 变化不升版本 →
   已有 plan 不会自动拿到新字段(前端回落原行为)。生产 775 个 80+ 缺牙患者
   需回填或强制重算才生效,另行处理。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Found errors in your .gitlab-ci.yml:

  • jobs:openapi-drift config contains unknown keys: rules
You can also test your .gitlab-ci.yml in the Lint
Status Job ID Name Coverage