Commit c9c1e778 by luoqi

refactor(sync): assembler 字典分层 — 临床通用层抽 _shared/dict,宿主怪癖留本地

多宿主字典治理:enum/keyword 映射词分两类 —— 临床通用知识(慢性牙髓炎→K04、
开髓→pulp_extirpation)拷贝必漂移(实证:jvs-dw 自己的 treatment_planned 字典
已是 treatment_actual 的陈旧拷贝);宿主怪癖(价格表项名/标点残留/用词判例)共用
会互相污染。→ 新增 dict_includes 机制:loader 在 zod 校验前解析合并,引擎零改动。

- dict-include.ts:include 解析 + 合并语义(enum 本地覆盖/keyword 本地优先/
  strip 去重并集/_default 禁入共享层 strict 校验)
- data/_shared/dict/ 七份共享字典:diagnosis-kcode(187 词+9 keyword 规则)、
  treat-stages(45)、treatment-category-core(177=actual∩planned 干净交集)、
  category keyword 按 kind 分 actual/planned 两个语义变体、image-modality(9)、
  recommendation-code(27+11)
- jvs-dw 五份 yaml 改用 include,本地只留判例(牙齿缺少→K08、拆线→surgical、
  单据类 modality、价格表项名、标点残留死条目)

生产零变化证明(jvs-dw 每日增量无感):20/20 assembler 新旧解析结果 deep-equal
(vs git HEAD);tsc 0 err;测试与 HEAD 基线完全一致(存量 3 挂套件与本次无关);
reparse dry-run 真实 loader 全链冒烟通过。

已知遗留(有意不修,行为保真):planned keyword 裸「二期」误吞正畸(共享文件已标
TODO);planned 残差中文标点死条目待清理 —— 均属行为变更须评估+reparse,另行提交。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parent f7cdf226
# ═══════════════════════════════════════════════════════════
# 共享字典 · 影像类型名 → PAC modality 闭集(pa/bw/pano/cbct/intraoral_photo/scan/other)
# 【临床通用层】真影像类型名(FRIDAY std_check_class 与 jvs-dw check_name 实测同名)。
# 宿主单据/化验类名目(牙周大表/血常规/术前清单…→other)与 _default 留宿主 yaml。
# 源出处:jvs-dw image.yaml。
# ═══════════════════════════════════════════════════════════
enum_mapping:
modality:
"口内像照片": intraoral_photo
"面相照": intraoral_photo
"内窥镜": intraoral_photo
"小牙片": pa
"全景片/头颅正侧位片截图": pano
"全景片/侧位片/其他X线影像": pano
"CT影像截图": cbct
"口腔扫描": scan
"其他类型资料(非X影像)": other
# ═══════════════════════════════════════════════════════════
# 共享字典 · 医生建议文本 → PAC 推荐码(IMPLANT_RECOMMENDED 等)
# 【临床通用层】"建议种植/建议拔除"等标准表达(enum 精确 + keyword 含词兜底)。
# 宿主标点变体等痕迹词与 _default 留宿主 yaml。源出处:jvs-dw recommendation.yaml。
# ═══════════════════════════════════════════════════════════
enum_mapping:
code:
# ── 种植 ──
建议种植: IMPLANT_RECOMMENDED
建议种植修复: IMPLANT_RECOMMENDED
推荐种植: IMPLANT_RECOMMENDED
建议种植治疗: IMPLANT_RECOMMENDED
建议种植牙: IMPLANT_RECOMMENDED
# ── 戴冠 / 修复 ──
建议戴冠: CROWN_RECOMMENDED
建议冠修复: CROWN_RECOMMENDED
建议全冠: CROWN_RECOMMENDED
建议桩核冠: CROWN_RECOMMENDED
建议做冠: CROWN_RECOMMENDED
推荐冠修复: CROWN_RECOMMENDED
# ── 充填 ──
建议充填: FILLING_RECOMMENDED
建议树脂充填: FILLING_RECOMMENDED
建议补牙: FILLING_RECOMMENDED
# ── 牙周基础 ──
建议洁牙: SRP_RECOMMENDED
建议龈上洁治: SRP_RECOMMENDED
建议牙周治疗: SRP_RECOMMENDED
建议牙周基础治疗: SRP_RECOMMENDED
# ── 拔除 ──
建议拔除: EXTRACTION_RECOMMENDED
建议拔牙: EXTRACTION_RECOMMENDED
推荐拔除: EXTRACTION_RECOMMENDED
# ── 复查 ──
建议年度复查: ANNUAL_REVIEW_RECOMMENDED
建议定期复查: ANNUAL_REVIEW_RECOMMENDED
建议复查: ANNUAL_REVIEW_RECOMMENDED
# ── 正畸咨询 ──
建议正畸咨询: ORTHO_CONSULT_RECOMMENDED
建议正畸: ORTHO_CONSULT_RECOMMENDED
建议矫正: ORTHO_CONSULT_RECOMMENDED
# ─────────────────────────────────────────────────────────
# 含词兜底 —— 精确 enum 未命中时,按序含词匹配 → 召回码。
# 治本长尾:"建议X"变体太多(建议充填治疗/建议根管治疗后冠修复/建议拔除后种植…),
# 精确白名单永远漏(谢玉兰 44 "建议充填治疗" 漏配 → null → 44 漏召)。引擎按 canonicalKey 通用。
# 顺序=优先级:高价值/主操作在前(种植>冠、根管>冠、拔除独立)。错配最坏=_default(现状),无回归。
keyword_mapping:
code:
- { value: IMPLANT_RECOMMENDED, any: [种植, 植体] }
- { value: RCT_RECOMMENDED, any: [根管, RCT, 牙髓] }
- { value: EXTRACTION_RECOMMENDED, any: [拔除, 拔牙] }
- { value: CROWN_RECOMMENDED, any: [, , 桩核] }
- { value: FILLING_RECOMMENDED, any: [充填, 补牙, 树脂] }
- { value: HARD_TISSUE_REPAIR_RECOMMENDED, any: [楔状, 牙体, 嵌体, 缺损] }
- { value: SRP_RECOMMENDED, any: [洁治, 洗牙, 牙周, 龈下, 刮治] }
- { value: ORTHO_CONSULT_RECOMMENDED, any: [正畸, 矫正, 矫治] }
- { value: GUM_TREATMENT_RECOMMENDED, any: [牙龈, 牙槽嵴, 系带] }
- { value: JAW_CYST_REMOVAL_RECOMMENDED, any: [囊肿] }
- { value: ERUPTION_INTERVENTION_RECOMMENDED, any: [萌出, 乳牙滞留, 多生牙] }
# ═══════════════════════════════════════════════════════════
# 共享字典 · 治疗 stage 词 → PAC step enum(开髓→pulp_extirpation 等)
# 【临床通用层】牙科标准术式步骤词。宿主经 dict_includes 引入;_default 宿主本地声明。
# 治理:改动影响所有引入宿主 → reparse;宿主怪癖词留宿主 yaml(本地覆盖共享)。
# 源出处:jvs-dw treatment_actual.yaml(与 treatment_planned 完全相同,抽取消除同宿主重复)。
# ═══════════════════════════════════════════════════════════
enum_mapping:
treatStages:
开髓: pulp_extirpation
拔髓: pulp_extirpation
根备: canal_preparation
根管预备: canal_preparation
根充: canal_filling
根管充填: canal_filling
# 牙髓切断术 / 活髓保存(pulpotomy 类 — VPT 终末术式)
# 临床等价:活髓切断 / 部分活髓切断 / 部分牙髓切断 / 冠髓切断 / 干髓 / 盖髓 / 牙髓血运重建
活髓切断: pulpotomy
部分活髓切断: pulpotomy
部分牙髓切断: pulpotomy
冠髓切断: pulpotomy
干髓术: pulpotomy
干髓治疗: pulpotomy
盖髓术: pulpotomy
间接盖髓: pulpotomy
直接盖髓: pulpotomy
牙髓血运重建: pulpotomy
活髓保存: pulpotomy
# 种植
植入: implant_placement
种植体植入: implant_placement
种植一期: implant_placement
种植I期: implant_placement # 罗马数字版(host 实测)
简单种植: implant_placement
复杂种植: implant_placement
即刻种植: implant_placement
延期种植: implant_placement
基台: abutment_placement
种植二期: abutment_placement
种植II期: abutment_placement # 罗马数字版(host 实测)
上部修复: crown_placement
种植冠修复: crown_placement
种植戴牙: crown_placement
种植三期: crown_placement
# 牙周
龈上洁治: supragingival_scaling
龈下刮治: subgingival_scaling
刮治: subgingival_scaling
牙周维护: periodontal_maintenance
# 修复
充填: composite_filling
嵌体: inlay
冠修复: crown_restoration
桩核: post_core
# 正畸
粘附件: bracket_placement
托槽: bracket_placement
矫治器: bracket_placement
保持器: retainer
加力: ortho_adjustment
# 长尾兜底:host stage 词字典外 → 空(不进 PAC,treat_stages 数组减一元素)
# ═══════════════════════════════════════════════════════════
# 共享字典 · 治疗 category 关键词兜底 —— 【actual(本次已做)语义变体】
# 与 planned 变体(treatment-category-planned-rules.yaml)是有意语义分歧:
# actual 剥"建议/推荐/考虑/若"从句(那些=本次没做);planned 本就以建议措辞,只剥纯条件词。
# 规则顺序=优先级(首个命中即用);宿主本地规则(若有)自动排在本文件规则之前。
# 源出处:jvs-dw treatment_actual.yaml(含裁决顺序的全部推敲注释)。
# ═══════════════════════════════════════════════════════════
# 关键词分类兜底 —— 精确 enum_mapping 未命中时,按序含词匹配 → 赋 category。
# 治本长尾:treatName 2万+ distinct 精确字典覆盖不全,以前全靠 _default:'' 丢(→ 误召)。
# 引擎算法(切段 / 含词 / 按序裁决 / 剥离从句)host 无关;此处规则词 = 本 host 术式特征(host 差异)。
# 只覆盖【真治疗】10 大类;流程/无操作(观察/复诊/无治疗)不在此 → 落 _default 丢弃
# (review 噪音已由 transforms.route_by_pattern 上游分流,这里不重复)。
# 顺序 = 优先级:主操作/高价值在前 → 关键词赢复合(种植>冠、根管>冠、操作词>人群词)。
# ⚠️ 安全:只接管精确未命中的长尾,精确命中零影响;错配最坏退化=_default(现状),无回归。
keyword_mapping:
category:
# ⚠️ "二期" 必须限定到「种植二期」:裸"二期"会误吞"二期隐形矫正"(正畸)等 → 误判 implant。
# 种植语境的二期都带"种植"(种植二期);ortho 的"二期隐形矫正/二期矫正"靠下面 orthodontic 兜住。
- { value: implant, any: [种植, 即拔即种, 植体, 种植二期] }
- { value: endodontic, any: [根管, RCT, 牙髓, 开髓, 根备, 根充, 盖髓, 摘髓, 根尖] }
- { value: orthodontic, any: [正畸, 矫治, 矫正, 托槽, 保持器, 粘附件, 隐适美, 隐形矫, 扩弓, 错颌, 错合] }
- { value: cosmetic, any: [贴面, 漂白, 美白] }
# 修复/备牙/取模:收 C.10 治疗痕迹(修复后/修复术后/全瓷桥修复后/备牙后/重新取模后)。
# 排在 implant/endo/ortho/cosmetic 之后 → 种植修复→implant、根管…修复→endo 仍被前面抢走。
# "嵌体修复"等 restorative 已由上方精确 enum 先命中,不受裸"修复"影响。
- { value: prosthodontic, any: [, , 义齿, 修复体, 修复, 备牙, 取模, 桩核, 桩冠, 戴牙, 全瓷, 烤瓷, 重新粘接] }
# ⭐ 明确充填动作(充填/去腐/备洞/嵌体/垫底/补牙)压过 preventive:
# "去腐,备洞,...树脂充填,窝沟封闭"(dispose 散文)是补牙(restorative),不能因含"窝沟封闭"
# 被误判 preventive → 非 resolver → 误召(马思煦@46)。注:"开髓去腐"等根管语境已被上方 endodontic 先收。
- { value: restorative, any: [充填, 补牙, 去腐, 备洞, 嵌体, 垫底] }
# 纯封闭(无充填动作)→ preventive:玻璃离子常作封闭剂材料("玻璃离子窝沟封闭"≠充填)
- { value: preventive, any: [窝沟封闭, 封闭剂, 点隙封闭] }
# 材料弱词:放 preventive 后 —— "玻璃离子窝沟封闭" 已被上面 preventive 收;裸"树脂/玻璃离子"→ restorative
- { value: restorative, any: [树脂, 玻璃离子] }
# 牙周去裸"洁牙"(避开自由文本"清洁牙面"误命中);洁牙/全口洁牙等精确词由上方 enum_mapping 兜
- { value: periodontic, any: [洁治, 洗牙, 龈上, 龈下, 刮治, 牙周, 喷砂, 细洁] }
- { value: preventive, any: [涂氟, 防龋, OHI, 口腔卫生宣教] }
- { value: surgical, any: [拔除, 拔牙, 切开, 翻瓣, 切除, 系带, 脓肿, 囊肿, 植骨] }
- { value: pediatric, any: [乳牙, 儿童, 年轻恒牙] }
# actual 语义:这些词开头的从句 = 本次没做(条件/未来/建议)→ 切段丢弃后再匹配。
# 例 "充填,必要时根管治疗" → 丢"必要时根管治疗" → "充填" → restorative(不误判成 endodontic)。
keyword_strip_clauses: [必要时, 如需, 择期, 建议, 推荐, 考虑, ] # 若…=条件从句(若牙面脱矿,建议充填),非本次实际治疗
# ═══════════════════════════════════════════════════════════
# 共享字典 · 治疗名 → PAC category(11 类闭集)· 精确匹配临床核心
#
# 【临床通用层】只收"干净"的标准术式名(无标点残留/价格表项名/长句/宿主判例)。
# 取自 jvs-dw treatment_actual ∩ treatment_planned 同值交集再过滤宿主痕迹(2026-07)。
# 宿主价格表项名/标点变体/判例(拆线→surgical、全景片→preventive 等)留各宿主 yaml。
# _default 宿主本地声明。keyword 兜底规则见 treatment-category-{actual,planned}-rules.yaml。
# ═══════════════════════════════════════════════════════════
enum_mapping:
category:
# ── 牙周(periodontic)
龈上洁治: periodontic
龈上洁治术: periodontic
全口龈上洁治: periodontic
全口洁治: periodontic
全口洁牙: periodontic
全口超声洁治: periodontic
全口超声波洁治: periodontic
全口龈上超声洁治: periodontic
定期全口洁治: periodontic
定期洁牙: periodontic
洁牙: periodontic
洁治: periodontic
洗牙: periodontic
"洁治 OHI": periodontic
龈下细洁: periodontic
龈上超声洁治: periodontic
牙周基础治疗: periodontic
牙周系统治疗: periodontic
牙周治疗: periodontic
牙周维护: periodontic
牙周刮治: periodontic
牙周刮治术: periodontic
龈下刮治: periodontic
龈下刮治术: periodontic
根面平整术: periodontic
PMTC: periodontic
牙周序列治疗: periodontic
系统性牙周治疗: periodontic
# ── 充填修复(restorative)
充填: restorative
充填治疗: restorative
充填术: restorative
树脂充填: restorative
光固化树脂充填: restorative
非美学区树脂充填: restorative
美学区简单树脂充填: restorative
重新充填: restorative
预防性充填: restorative
树脂充填术: restorative
嵌体修复: restorative
嵌体: restorative
戴嵌体: restorative
龋齿充填: restorative
玻璃离子充填: restorative
玻璃离子充填术: restorative
进口玻璃离子充填: restorative
玻璃离子暂封: restorative
玻璃离子预充填: restorative
充填或嵌体修复: restorative
高嵌体修复: restorative
预防性树脂充填: restorative
# ── 牙髓/根管(endodontic)
根管治疗: endodontic
根管再治疗: endodontic
根管预备: endodontic
根管充填: endodontic
根充: endodontic
RCT: endodontic
牙髓治疗: endodontic
根尖诱导成形术: endodontic
拔髓: endodontic
干髓术: endodontic
盖髓术: endodontic
间接盖髓: endodontic
直接盖髓: endodontic
MTA盖髓: endodontic
冲洗上药: endodontic
根管治疗后冠修复: endodontic
活髓切断术: endodontic
部分活髓切断术: endodontic
部分牙髓切断术: endodontic
活髓切断: endodontic
乳牙活髓切断术: endodontic
乳牙活髓保存术: endodontic
冠髓切断术: endodontic
干髓治疗: endodontic
直接盖髓术: endodontic
间接盖髓术: endodontic
开髓引流: endodontic
开髓失活: endodontic
开髓开放: endodontic
开髓封药: endodontic
开髓封失活剂: endodontic
牙髓失活: endodontic
封失活剂: endodontic
放失活剂: endodontic
放置失活剂: endodontic
失活: endodontic
乳牙牙髓治疗: endodontic
髓腔消毒: endodontic
髓腔封药: endodontic
髓腔预备: endodontic
髓腔换药: endodontic
保髓治疗: endodontic
牙髓保留: endodontic
根管治疗术: endodontic
复杂根管治疗: endodontic
# ── 种植(implant)
种植: implant
种植手术: implant
种植体植入: implant
种植修复: implant
种植戴牙: implant
种植一期: implant
种植二期: implant
种植三期: implant
种植耗材: implant
种植基台: implant
种植取模: implant
种植拆线: implant
即刻种植术: implant
拔除后种植修复: implant
延期种植术: implant
拔除后种植: implant
# ── 修复(冠/桥/义齿/桩核)(prosthodontic)
冠修复: prosthodontic
全冠修复: prosthodontic
牙冠修复: prosthodontic
重新冠修复: prosthodontic
戴冠: prosthodontic
粘冠: prosthodontic
拆冠: prosthodontic
戴牙: prosthodontic
桩核: prosthodontic
桩核冠: prosthodontic
固定修复: prosthodontic
活动修复: prosthodontic
重新修复: prosthodontic
修复: prosthodontic
预成冠修复: prosthodontic
牙体预备: prosthodontic
种植上部修复: prosthodontic
种植体上部修复: prosthodontic
桩冠修复: prosthodontic
# ── 外科(拔除)(surgical)
拔除: surgical
拔除术: surgical
拔牙: surgical
牙拔除术: surgical
简单牙拔除术: surgical
松动恒牙拔除术: surgical
松动乳牙拔除术: surgical
择期拔除: surgical
阻生齿拔除术: surgical
阻生牙拔除: surgical
残根拔除术: surgical
# ── 正畸(orthodontic)
正畸: orthodontic
正畸治疗: orthodontic
隐形正畸: orthodontic
固定正畸: orthodontic
早期矫治: orthodontic
保持器: orthodontic
正畸保持器: orthodontic
正畸保持: orthodontic
正畸矫治器: orthodontic
正畸辅助治疗: orthodontic
正畸加力: orthodontic
加力: orthodontic
粘附件: orthodontic
粘接附件: orthodontic
重粘托槽: orthodontic
更换新矫治器: orthodontic
粘接上半口矫治器: orthodontic
粘接全口附件: orthodontic
精调粘接附件: orthodontic
发放新矫治器: orthodontic
精细调整: orthodontic
发放矫治器: orthodontic
# ── 预防(preventive)
涂氟: preventive
全口涂氟: preventive
窝沟封闭: preventive
窝沟封闭术: preventive
OHI: preventive
口腔卫生宣教: preventive
脱敏: preventive
抛光: preventive
# ── 美学(cosmetic)
美白: cosmetic
冷光美白: cosmetic
牙齿美白: cosmetic
牙位图美白: cosmetic
贴面: cosmetic
贴面修复: cosmetic
# ═══════════════════════════════════════════════════════════
# 共享字典 · 治疗 category 关键词兜底 —— 【planned(未来计划)语义变体】
# 与 actual 变体是有意语义分歧:planned 文本本就以"建议/拟/推荐"措辞 → 不剥这些词,
# 只剥纯条件从句(必要时/如需/择期)。
# ⚠️ TODO(继承自 jvs-dw 陈旧拷贝,行为保真原样搬入):implant 规则里裸「二期」会误吞
# "二期隐形矫正"(actual 变体已改「种植二期」)。修复属行为变更,须评估 + reparse,另行提交。
# 源出处:jvs-dw treatment_planned.yaml。
# ═══════════════════════════════════════════════════════════
keyword_mapping:
category:
- { value: implant, any: [种植, 即拔即种, 植体, 二期] }
- { value: endodontic, any: [根管, RCT, 牙髓, 开髓, 根备, 根充, 盖髓, 摘髓] }
- { value: orthodontic, any: [正畸, 矫治, 矫正, 托槽, 保持器, 粘附件, 隐适美, 扩弓] }
- { value: cosmetic, any: [贴面, 漂白, 美白] }
- { value: prosthodontic, any: [, , 义齿, 修复体, 桩核, 桩冠, 戴牙, 全瓷, 烤瓷, 重新粘接] }
- { value: restorative, any: [充填, 补牙, 树脂, 玻璃离子, 嵌体, 垫底] }
- { value: periodontic, any: [洁治, 洗牙, 洁牙, 龈上, 龈下, 刮治, 牙周, 喷砂, 细洁] }
- { value: preventive, any: [涂氟, 窝沟封闭, 封闭, 防龋, OHI, 口腔卫生宣教] }
- { value: surgical, any: [拔除, 拔牙, 切开, 翻瓣, 切除, 系带, 脓肿, 囊肿] }
- { value: pediatric, any: [乳牙, 儿童, 年轻恒牙] }
# planned 语义:计划本就以"建议/拟/推荐"措辞 → 不剥,只剥纯条件词(必要时/如需/择期)。
# 例 "建议全口洁治后,必要时正畸治疗" → 丢"必要时正畸治疗" → "建议全口洁治后" → periodontic。
keyword_strip_clauses: [必要时, 如需, 择期]
......@@ -25,19 +25,14 @@ field_mapping:
doctorId: user_id # 接诊医生(从 emr 父级继承,影像拍摄/解读医生)
doctorName: doctor_name
# ── 字典分层(2026-07 重构):真影像类型在 _shared/dict;单据/化验类名目留本地 ──
dict_includes:
- ../../_shared/dict/image-modality.yaml
enum_mapping:
# host check_name → PAC modality 闭集(pa/bw/pano/cbct/intraoral_photo/scan/other)
# 真影像归对应类型;化验/表单/记录类(血常规/牙周大表/登记表…)无影像语义,归 other
# host check_name → PAC modality:化验/表单/记录类(无影像语义)归 other(本 host 名目)
modality:
"口内像照片": intraoral_photo
"面相照": intraoral_photo
"内窥镜": intraoral_photo
"小牙片": pa
"全景片/头颅正侧位片截图": pano
"全景片/侧位片/其他X线影像": pano
"CT影像截图": cbct
"口腔扫描": scan
"其他类型资料(非X影像)": other
"牙周大表": other
"血常规": other
"拔牙术前清单": other
......
......@@ -29,68 +29,14 @@ field_mapping:
# treat_name 自由文本,enum_mapping cover 高频"建议 XX"白名单
# 长尾 _default: '' parser 跳过(不入 recommendation_record fact)
enum_mapping:
code:
# ── 种植 ──
建议种植: IMPLANT_RECOMMENDED
建议种植修复: IMPLANT_RECOMMENDED
推荐种植: IMPLANT_RECOMMENDED
建议种植治疗: IMPLANT_RECOMMENDED
建议种植牙: IMPLANT_RECOMMENDED
# ── 戴冠 / 修复 ──
建议戴冠: CROWN_RECOMMENDED
建议冠修复: CROWN_RECOMMENDED
建议全冠: CROWN_RECOMMENDED
建议桩核冠: CROWN_RECOMMENDED
建议做冠: CROWN_RECOMMENDED
推荐冠修复: CROWN_RECOMMENDED
# ── 字典分层(2026-07 重构):标准"建议X"词典在 _shared/dict;本地留 host 标点变体 ──
dict_includes:
- ../../_shared/dict/recommendation-code.yaml
# ── 充填 ──
建议充填: FILLING_RECOMMENDED
enum_mapping:
code:
# host 数据尾标点残留(normalize 前旧形态,保真保留)
"建议充填。": FILLING_RECOMMENDED
建议树脂充填: FILLING_RECOMMENDED
建议补牙: FILLING_RECOMMENDED
# ── 牙周基础 ──
建议洁牙: SRP_RECOMMENDED
建议龈上洁治: SRP_RECOMMENDED
建议牙周治疗: SRP_RECOMMENDED
建议牙周基础治疗: SRP_RECOMMENDED
# ── 拔除 ──
建议拔除: EXTRACTION_RECOMMENDED
"建议拔除。": EXTRACTION_RECOMMENDED
建议拔牙: EXTRACTION_RECOMMENDED
推荐拔除: EXTRACTION_RECOMMENDED
# ── 复查 ──
建议年度复查: ANNUAL_REVIEW_RECOMMENDED
建议定期复查: ANNUAL_REVIEW_RECOMMENDED
建议复查: ANNUAL_REVIEW_RECOMMENDED
# ── 正畸咨询 ──
建议正畸咨询: ORTHO_CONSULT_RECOMMENDED
建议正畸: ORTHO_CONSULT_RECOMMENDED
建议矫正: ORTHO_CONSULT_RECOMMENDED
_default: "" # 未识别"建议X" → 交下面 keyword_mapping 含词兜底;再不中 → 丢弃
# ─────────────────────────────────────────────────────────
# 含词兜底 —— 精确 enum 未命中时,按序含词匹配 → 召回码。
# 治本长尾:"建议X"变体太多(建议充填治疗/建议根管治疗后冠修复/建议拔除后种植…),
# 精确白名单永远漏(谢玉兰 44 "建议充填治疗" 漏配 → null → 44 漏召)。引擎按 canonicalKey 通用。
# 顺序=优先级:高价值/主操作在前(种植>冠、根管>冠、拔除独立)。错配最坏=_default(现状),无回归。
keyword_mapping:
code:
- { value: IMPLANT_RECOMMENDED, any: [种植, 植体] }
- { value: RCT_RECOMMENDED, any: [根管, RCT, 牙髓] }
- { value: EXTRACTION_RECOMMENDED, any: [拔除, 拔牙] }
- { value: CROWN_RECOMMENDED, any: [, , 桩核] }
- { value: FILLING_RECOMMENDED, any: [充填, 补牙, 树脂] }
- { value: HARD_TISSUE_REPAIR_RECOMMENDED, any: [楔状, 牙体, 嵌体, 缺损] }
- { value: SRP_RECOMMENDED, any: [洁治, 洗牙, 牙周, 龈下, 刮治] }
- { value: ORTHO_CONSULT_RECOMMENDED, any: [正畸, 矫正, 矫治] }
- { value: GUM_TREATMENT_RECOMMENDED, any: [牙龈, 牙槽嵴, 系带] }
- { value: JAW_CYST_REMOVAL_RECOMMENDED, any: [囊肿] }
- { value: ERUPTION_INTERVENTION_RECOMMENDED, any: [萌出, 乳牙滞留, 多生牙] }
_default: "" # 未识别"建议X" → 共享 keyword 含词兜底;再不中 → 丢弃
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as yaml from 'js-yaml';
import { z } from 'zod';
/**
* 字典 include —— assembler yaml 的 `dict_includes:` 解析与合并。
*
* 动机(多宿主字典分层):enum/keyword 映射词分两类 ——
* ① 临床通用知识(慢性牙髓炎→K04、开髓→pulp_extirpation):跟宿主无关,拷贝会漂移
* (实证:jvs-dw 自己的 treatment_planned 字典已是 treatment_actual 的陈旧拷贝);
* ② 宿主怪癖(价格表项名、标点残留、host 用词判例):共用会互相污染。
* → ① 抽到 data/_shared/dict/*.yaml,宿主 yaml 用 `dict_includes:` 引入;② 留宿主本地。
*
* 用法(assembler yaml 顶层):
* dict_includes:
* - ../../_shared/dict/diagnosis-kcode.yaml # 路径相对本 yaml 文件
*
* 合并语义(⚠️ 三条规则,变更需同步 pac-docs data-ingestion 文档):
* - enum_mapping :同字段逐 key 合并;include 按声明序先铺,本地条目**覆盖**同 key
* (宿主可用本地 `词: ""` 墓碑禁用共享词条 —— "" 与 _default 空值同义)。
* - keyword_mapping:同字段规则数组拼接,**本地规则在前**(引擎首个命中即用 → 宿主优先),
* include 按声明序殿后兜底。
* - keyword_strip_clauses:去重并集,本地在前(引擎对全列表做 startsWith,序无语义)。
* - `_default` 约定留宿主本地声明(共享文件禁止携带,schema 强校验拦截)。
*
* 解析发生在 zod 校验【之前】(loadAllAssemblers),合并产物 = 现行 AssemblerConfig 形状
* → 引擎(assembler-engine)与 schema 零改动;不写 dict_includes 的 yaml 行为分毫不变。
*/
const EnumMapSchema = z.record(z.string(), z.record(z.string(), z.string()));
const KeywordRuleSchema = z.object({
value: z.string(),
any: z.array(z.string().min(1)).min(1),
none: z.array(z.string().min(1)).optional(),
});
const KeywordMapSchema = z.record(z.string(), z.array(KeywordRuleSchema));
/// 共享字典文件形状:只允许三个映射段(strict:打错字段名/夹带 field_mapping 等 fail-fast)。
const DictIncludeFileSchema = z
.object({
enum_mapping: EnumMapSchema.optional(),
keyword_mapping: KeywordMapSchema.optional(),
keyword_strip_clauses: z.array(z.string().min(1)).optional(),
})
.strict()
.superRefine((file, ctx) => {
// _default 是宿主决策(丢弃 or 空码兜底),共享层禁止携带 —— 防止共享文件悄悄改宿主兜底行为
for (const [field, m] of Object.entries(file.enum_mapping ?? {})) {
if (Object.prototype.hasOwnProperty.call(m, '_default')) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `共享字典不得含 _default(enum_mapping.${field});_default 由宿主 yaml 本地声明`,
});
}
}
});
export type DictIncludeFile = z.infer<typeof DictIncludeFileSchema>;
interface RawAssemblerYaml {
dict_includes?: unknown;
enum_mapping?: Record<string, Record<string, string>>;
keyword_mapping?: Record<string, unknown[]>;
keyword_strip_clauses?: string[];
[k: string]: unknown;
}
/**
* 解析 yaml 原始对象里的 dict_includes 并合并,返回可直接过 AssemblerConfigSchema 的对象。
* 无 dict_includes → 原样返回(零开销,老 yaml 完全不走新逻辑)。
* @param raw yaml.load 的原始产物
* @param yamlPath 本 yaml 的绝对路径(include 相对路径的锚点;也用于报错定位)
*/
export function resolveDictIncludes(raw: unknown, yamlPath: string): unknown {
if (raw === null || typeof raw !== 'object') return raw;
const cfg = raw as RawAssemblerYaml;
if (cfg.dict_includes === undefined) return raw;
const incPaths = z.array(z.string().min(1)).min(1).safeParse(cfg.dict_includes);
if (!incPaths.success) {
throw new Error(`${yamlPath}: dict_includes 须为非空字符串数组`);
}
// 1. 逐个加载 include 文件(相对本 yaml;strict 校验)
const includes: DictIncludeFile[] = incPaths.data.map((rel) => {
const abs = path.resolve(path.dirname(yamlPath), rel);
if (!fs.existsSync(abs)) {
throw new Error(`${yamlPath}: dict include 不存在: ${rel}${abs}`);
}
const parsed = yaml.load(fs.readFileSync(abs, 'utf-8'));
const r = DictIncludeFileSchema.safeParse(parsed);
if (!r.success) {
throw new Error(
`共享字典 ${rel} 校验失败(由 ${path.basename(yamlPath)} 引入):\n` +
JSON.stringify(r.error.issues, null, 2),
);
}
return r.data;
});
// 2. enum_mapping:include 按序先铺(后者覆盖前者),本地最后覆盖
const mergedEnum: Record<string, Record<string, string>> = {};
for (const inc of includes) {
for (const [field, m] of Object.entries(inc.enum_mapping ?? {})) {
mergedEnum[field] = { ...mergedEnum[field], ...m };
}
}
for (const [field, m] of Object.entries(cfg.enum_mapping ?? {})) {
mergedEnum[field] = { ...mergedEnum[field], ...m };
}
// 3. keyword_mapping:本地规则在前,include 按序殿后
const mergedKeyword: Record<string, unknown[]> = {};
for (const [field, rules] of Object.entries(cfg.keyword_mapping ?? {})) {
mergedKeyword[field] = [...rules];
}
for (const inc of includes) {
for (const [field, rules] of Object.entries(inc.keyword_mapping ?? {})) {
mergedKeyword[field] = [...(mergedKeyword[field] ?? []), ...rules];
}
}
// 4. keyword_strip_clauses:本地在前去重并集
const strip = [
...(cfg.keyword_strip_clauses ?? []),
...includes.flatMap((i) => i.keyword_strip_clauses ?? []),
];
const mergedStrip = [...new Set(strip)];
// 5. 拼回(剥掉 dict_includes;空段不落 key,与"没写该段"的老形状一致)
const { dict_includes: _drop, ...rest } = cfg;
return {
...rest,
...(Object.keys(mergedEnum).length > 0 ? { enum_mapping: mergedEnum } : {}),
...(Object.keys(mergedKeyword).length > 0 ? { keyword_mapping: mergedKeyword } : {}),
...(mergedStrip.length > 0 ? { keyword_strip_clauses: mergedStrip } : {}),
};
}
......@@ -23,6 +23,7 @@ import {
AssemblerConfigSchema,
type AssemblerConfig,
} from '../assembler/assembler.schema';
import { resolveDictIncludes } from '../assembler/dict-include';
import {
ColdImportManifest,
ColdImportManifestSchema,
......@@ -2139,7 +2140,10 @@ export class ColdImportService {
if (!fs.existsSync(filePath)) {
throw new Error(`Assembler config not found: ${filePath}`);
}
const parsed = yaml.load(fs.readFileSync(filePath, 'utf-8'));
const parsed = resolveDictIncludes(
yaml.load(fs.readFileSync(filePath, 'utf-8')),
filePath,
);
const r = AssemblerConfigSchema.safeParse(parsed);
if (!r.success) {
throw new Error(
......
......@@ -28,6 +28,7 @@ import {
AssemblerConfigSchema,
type AssemblerConfig,
} from '../src/modules/sync/assembler/assembler.schema';
import { resolveDictIncludes } from '../src/modules/sync/assembler/dict-include';
// ─────────────────────────────────────────────
// 闸 1:FactType ↔ FactContentSchemas 完整性
......@@ -109,8 +110,10 @@ function loadAllAssemblerYamls(): YamlFileInfo[] {
if (!fs.existsSync(asmDir)) continue;
for (const f of fs.readdirSync(asmDir)) {
if (!f.endsWith('.yaml') && !f.endsWith('.yml')) continue;
const raw = fs.readFileSync(path.join(asmDir, f), 'utf-8');
const parsed = yaml.load(raw);
const yamlPath = path.join(asmDir, f);
const raw = fs.readFileSync(yamlPath, 'utf-8');
// 走生产同款加载:dict_includes(共享字典分层)在 zod 校验前解析合并
const parsed = resolveDictIncludes(yaml.load(raw), yamlPath);
const r = AssemblerConfigSchema.safeParse(parsed);
if (!r.success) {
throw new Error(
......
......@@ -12,10 +12,12 @@ import * as path from 'path';
import * as yaml from 'js-yaml';
import { classifyByKeyword } from '../src/modules/sync/assembler/assembler-engine';
import { AssemblerConfigSchema } from '../src/modules/sync/assembler/assembler.schema';
import { resolveDictIncludes } from '../src/modules/sync/assembler/dict-include';
function loadCfg(file: string) {
const p = path.join(__dirname, '..', 'data', 'jvs-dw', 'assemblers', file);
const parsed = yaml.load(fs.readFileSync(p, 'utf-8'));
// 走生产同款加载:dict_includes(共享字典分层)在 zod 校验前解析合并
const parsed = resolveDictIncludes(yaml.load(fs.readFileSync(p, 'utf-8')), p);
const r = AssemblerConfigSchema.safeParse(parsed);
if (!r.success) {
throw new Error(`${file} schema invalid:\n${JSON.stringify(r.error.issues, null, 2)}`);
......
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