Commit 38199a65 by luoqi

fix(web): 召回对账 oracle 同步'检查文本无修复指征'分支 — 与后端 gap 一致

后端 gap (a''') 用 exam_findings'缺牙间隙关闭/无修复间隙'排除该牙;前端对账 oracle
镜像同口径(词典 NO_RESTORATION_GAP_EXAM_PATTERNS),否则审计视图报假分歧。
判 ineligible(非修复),理由'检查所见缺牙间隙已关闭'。web build 过。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parent 3af7499d
......@@ -27,6 +27,7 @@ import {
DiagnosisTreatmentMap,
APPT_COMPLAINT_TO_CATEGORY,
isRestorationIneligibleDxName,
NO_RESTORATION_GAP_EXAM_PATTERNS,
resolverCategoriesFor,
STRUCTURAL_DX_CODE_LIST,
treatmentCategoryNameZh,
......@@ -140,6 +141,19 @@ function complaintTextsFor(cats: readonly string[]): string[] {
/**
* 独立 oracle 主入口:对一个患者的全部 fact 跑一遍,产出每颗牙(+ 全口泳道)的召回判定。
*/
/// 解析 exam_findings(字符串或 array;同 emr-soap-view.parseJsonArray)
function parseExamFindings(raw: unknown): Array<{ toothPosition?: string; message?: string }> {
if (Array.isArray(raw)) return raw as Array<{ toothPosition?: string; message?: string }>;
if (typeof raw !== 'string' || !raw.trim() || raw === 'null') return [];
try {
const p = JSON.parse(raw);
return Array.isArray(p) ? p : [];
} catch {
return [];
}
}
export function runRecallOracle(facts: AdaptedFact[], now: Date): OracleVerdict[] {
const ms = now.getTime();
const daysAgoMs = (n: number) => ms - n * 86400000;
......@@ -160,6 +174,10 @@ export function runRecallOracle(facts: AdaptedFact[], now: Date): OracleVerdict[
const maxDxByTooth = new Map<string, number>();
// 每颗牙的"最晚真实建议"时间戳 —— 用于"诊断 vs 建议冲突,以建议(医生决定)为准"。
const maxRecByTooth = new Map<string, number>();
// 每颗牙的"无修复指征"时间戳 —— 检查所见(exam_findings)写"缺牙间隙关闭/无修复间隙"。
// 与后端 gap (a''') 分支同口径(词典 NO_RESTORATION_GAP_EXAM_PATTERNS,需"缺失/缺牙"共现)。
const noRestorByTooth = new Map<string, number>();
const noRestorRe = new RegExp(NO_RESTORATION_GAP_EXAM_PATTERNS.join('|'));
for (const f of facts) {
const c = (f.content ?? {}) as Record<string, unknown>;
......@@ -195,6 +213,24 @@ export function runRecallOracle(facts: AdaptedFact[], now: Date): OracleVerdict[
const vt = new Date(f.occurredAt).getTime();
if (vt > daysAgoMs(POST_VISIT_COOLDOWN_DAYS)) recentVisit = true;
}
// 检查所见:缺牙 message 含"间隙关闭/无修复间隙" → 该牙无修复指征(同后端 gap a''')
if (f.type === 'emr_record') {
const iso = f.occurredAt ?? f.plannedFor;
const ts = iso ? new Date(iso).getTime() : null;
if (ts != null) {
for (const seg of parseExamFindings((f.content ?? {}).exam_findings)) {
const msg = String(seg.message ?? '');
if (!/缺[牙失]/.test(msg) || !noRestorRe.test(msg)) continue;
for (const t of String(seg.toothPosition ?? '')
.split(';')
.map((x) => toothBase(x.trim()))
.filter(isValidTooth)) {
const cur = noRestorByTooth.get(t);
if (cur == null || ts > cur) noRestorByTooth.set(t, ts);
}
}
}
}
}
// ⭐ 同牙位最新诊断 —— 收每颗牙的【最晚真实诊断】时间(任意真实码,不止召回码)。
// 用于"后续诊断取代旧诊断":某牙存在比信号更晚的真实诊断 → 旧信号对该牙失效。
......@@ -270,6 +306,13 @@ export function runRecallOracle(facts: AdaptedFact[], now: Date): OracleVerdict[
// ④' 废用牙
kind = 'ineligible';
detail = `「${nameZh}」非修复对象(该拔除/观察)`;
} else if (
toothFilter != null &&
(noRestorByTooth.get(toothFilter) ?? -1) >= sig.t
) {
// (a''') 检查所见:缺牙间隙已关闭 / 无修复间隙 → 无修复指征
kind = 'ineligible';
detail = `检查所见缺牙间隙已关闭,无修复指征`;
} else {
// 解决者:resolverCats 家族里任一治疗。
// wholeMouth → 任意时间 if excludeIfEverTreated,否则 afterDx;
......
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