Commit 34e9c1f2 by luoqi

test(recall): 修孤儿引用 — likelihood bonus 已删,改锁 v3 优先级因子

recall-suppression.spec 引用 7eb8dbed 三源标定重构中删除的 computeLikelihoodBonus,
套件加载失败(main 同样存在)。原 GAP2"considering 不加权"在 v3 由设计保证
(scorer 不再消费 execution outcome);该块改测 v3 确定性因子:置信度夹逼 [0.5,1]、
新鲜度过窗线性衰减至 0.4 地板、三维加权(急迫0.4/价值0.3/意愿0.3)、
K08/K03 牙数分档、信任封顶 10、总分 ≤100。snoozedUntil 部分 API 未变,原测保留。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parent 943b0922
......@@ -4,7 +4,12 @@ import {
BREAKER_SUPPRESS_DAYS,
} from '@pac/types';
import { resolveSnoozedUntil } from '../src/modules/plan/recall-suppression';
import { computeLikelihoodBonus } from '../src/modules/plan/engine/priority-scorer';
import {
computeConfidenceFactor,
computeFreshness,
calcPriority,
type PriorityInput,
} from '../src/modules/plan/engine/priority-scorer';
/**
* 召回闭环:熔断 / 拒绝 / 回访冷静期(snoozedUntil)。
......@@ -82,19 +87,98 @@ describe('召回抑制窗 snoozedUntil 计算', () => {
});
});
describe('优先级:considering 不再加权(GAP2)', () => {
test('考虑中不应把患者顶到列表最前(冷静期内 + 不加权)', () => {
expect(computeLikelihoodBonus(3, [{ outcome: 'considering' }])).toBe(0);
/**
* 优先级 v3.0 三维模型(7eb8dbe 重构后)。
* 原 GAP2"considering 不加权":v3 scorer 不再消费 execution outcome(likelihood bonus 已删),
* 由设计保证 —— 这里改锁 v3 的确定性因子(置信度 / 新鲜度 / 三维加权)。
*/
describe('priority-scorer v3 | 确定性因子', () => {
test('置信度因子:直用来源置信度,夹在 [0.5, 1](防过罚)', () => {
expect(computeConfidenceFactor(1)).toBe(1); // 医生诊断
expect(computeConfidenceFactor(0.8)).toBe(0.8); // 建议
expect(computeConfidenceFactor(0.2)).toBe(0.5); // 下限地板
expect(computeConfidenceFactor(1.5)).toBe(1); // 上限
});
test('新鲜度:黄金窗内满权重,过窗线性衰减,2×窗见地板 0.4', () => {
expect(computeFreshness(0, 180)).toBe(1.0);
expect(computeFreshness(180, 180)).toBe(1.0); // 窗上界仍满
expect(computeFreshness(270, 180)).toBeCloseTo(0.7); // 过窗一半
expect(computeFreshness(360, 180)).toBeCloseTo(0.4); // 2×窗
expect(computeFreshness(3600, 180)).toBeCloseTo(0.4); // 地板不再降
});
const baseInput: PriorityInput = {
urgencyLevel: 'high',
daysSince: 0,
windowDays: 180,
primaryCode: 'K02',
toothCount: 1,
rfmSegment: null,
consultIntentMatch: false,
lifecycleStage: null,
hasTreatmentHistory: false,
isReferralChampion: false,
recentVisitGood: false,
confidence: 1,
};
test('三维加权:急迫×0.4 + 价值×0.3 + 意愿×0.3(缺省 rfm=5/意向=2/信任=4)', () => {
const r = calcPriority(baseInput);
// urgency high=7 → 2.8;K02 价值 2 → 0.6;意愿 5*0.375+2*0.375+4*0.25=3.625 → 1.0875
expect(r.breakdown.urgency).toBe(7);
expect(r.breakdown.value).toBe(2);
expect(r.breakdown.willingness).toBe(3.63);
expect(r.breakdown.base).toBe(4.49);
expect(r.score).toBe(45); // round(4.4875 × 10)
});
test('真实正向进展(成功转化 / 改约)仍 +4', () => {
expect(computeLikelihoodBonus(3, [{ outcome: 'success_appointed' }])).toBe(4);
expect(computeLikelihoodBonus(3, [{ outcome: 'rescheduled' }])).toBe(4);
test('种植(K08)价值按牙数分档:单颗8 / 多颗9 / 半口+10', () => {
expect(calcPriority({ ...baseInput, primaryCode: 'K08', toothCount: 1 }).breakdown.value).toBe(8);
expect(calcPriority({ ...baseInput, primaryCode: 'K08', toothCount: 3 }).breakdown.value).toBe(9);
expect(calcPriority({ ...baseInput, primaryCode: 'K08', toothCount: 6 }).breakdown.value).toBe(10);
});
test('无近期正向执行 → 仅 riskBonus', () => {
expect(computeLikelihoodBonus(3, [{ outcome: 'no_answer' }])).toBe(0); // risk=3 → riskBonus 0
expect(computeLikelihoodBonus(0, [])).toBe(6); // risk=0 → (3-0)*2=6
test('修复(K03)≥3颗提档:5 → 6;未映射病种价值兜底 3', () => {
expect(calcPriority({ ...baseInput, primaryCode: 'K03', toothCount: 2 }).breakdown.value).toBe(5);
expect(calcPriority({ ...baseInput, primaryCode: 'K03', toothCount: 3 }).breakdown.value).toBe(6);
expect(calcPriority({ ...baseInput, primaryCode: 'K99', toothCount: 1 }).breakdown.value).toBe(3);
});
test('信任基础:lifecycle 底分 + 治疗史/转介/近1年履约各+1,封顶 10', () => {
const r = calcPriority({
...baseInput,
lifecycleStage: 'mature', // 9
hasTreatmentHistory: true,
isReferralChampion: true,
recentVisitGood: true,
});
expect(r.breakdown.trustBase).toBe(10); // 9+3 → 封顶
});
test('老诊断 + 低置信来源:新鲜度与置信度因子相乘压低总分', () => {
const r = calcPriority({ ...baseInput, daysSince: 360, confidence: 0.3 });
expect(r.breakdown.freshness).toBe(0.4);
expect(r.breakdown.confidenceFactor).toBe(0.5);
expect(r.score).toBe(9); // round(4.4875 × 0.4 × 0.5 × 10)
});
test('urgency 缺失记 0;满配输入分数不破 100', () => {
expect(calcPriority({ ...baseInput, urgencyLevel: null }).breakdown.urgency).toBe(0);
const max = calcPriority({
...baseInput,
urgencyLevel: 'urgent',
primaryCode: 'K08',
toothCount: 8,
rfmSegment: 'important_value',
consultIntentMatch: true,
lifecycleStage: 'mature',
hasTreatmentHistory: true,
isReferralChampion: true,
recentVisitGood: true,
});
expect(max.score).toBeLessThanOrEqual(100);
expect(max.score).toBeGreaterThan(90);
});
});
......
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