import assert from "node:assert/strict"; import { test } from "node:test"; import { ablationFinding, inseparableGroups, judgeExplanations, listCells, settleAblation, sharedSettings, supportOf, synergyLabel, totalGain, type AblationCell, type AblationExplanation, } from "../src/rules/index.ts"; // 预期值来源写在每条测试旁边。 function close(actual: number | null, expected: number, tol = 1e-12): void { assert.ok(actual !== null && Math.abs(actual - expected) <= tol, `实际 ${actual},预期 ${expected}`); } test("6.6 示例:基线 1.00、完整 1.50,关 p1 得 1.45、关 p2 得 1.10:撑 0.10 与 0.80,i = −0.10,各管各", () => { // 来源:方法.md 6.6 示例原文。 assert.equal(totalGain(1, 1.5, "maximize"), 0.5); const s1 = supportOf(1, 1.5, 1.45, "maximize"); const s2 = supportOf(1, 1.5, 1.1, "maximize"); close(s1, 0.1); close(s2, 0.8); const l = synergyLabel([s1, s2]); close(l.interaction, -0.1); assert.equal(l.label, "independent"); }); test("6.6 越小越好时撑几成按方向折算", () => { // 手算:基线 2、完整 1(越小越好,G = 1),关掉 p 得 1.5:撑 = (1.5 − 1)/1 = 0.5。 assert.equal(supportOf(2, 1, 1.5, "minimize"), 0.5); }); test("6.6 G ≤ 0 时直接报错", () => { // 来源:方法.md 6.6"遇到 G ≤ 0 时直接报错"。 assert.throws(() => totalGain(1.5, 1.5, "maximize"), /6\.6/); assert.throws(() => totalGain(1.5, 1.4, "maximize"), /G = .*≤ 0/); assert.throws(() => totalGain(1, 1.2, "minimize"), /6\.6/); assert.throws(() => supportOf(1, 1, 0.5, "maximize"), /6\.6/); }); test("6.6 配合关系标签按表从上往下判", () => { // 来源:方法.md 6.6 的表;数字手取。 assert.equal(synergyLabel([-0.1, 0.5]).label, "offBetter"); assert.equal(synergyLabel([-0.1, null]).label, "offBetter"); // 撑为负先于"不全" assert.equal(synergyLabel([0.5, null]).label, "incomplete"); assert.equal(synergyLabel([0.5, null]).interaction, null); assert.equal(synergyLabel([0.95, 0.91]).label, "indispensable"); // 0.9 是严格大于:[0.9, 0.95] 不是缺一不可;i = 0.85 > 0.2,互补。 assert.equal(synergyLabel([0.9, 0.95]).label, "complementary"); assert.equal(synergyLabel([0.75, 0.5]).label, "complementary"); // i = 0.25 assert.equal(synergyLabel([0.25, 0.5]).label, "substitute"); // i = −0.25 assert.equal(synergyLabel([0.5, 0.5]).label, "independent"); // i = 0 assert.equal(synergyLabel([0.5, 0.5, 0.25]).supportSum, 1.25); }); test("列格:每个因子一格,按因子编号数字序", () => { assert.deepEqual(listCells(["p10", "p2", "p1"]), ["p1", "p2", "p10"]); }); const r = (q1: number, q5: number, q9: number) => ({ q1, q5, q9 }); function explanation(id: string, author: string, cells: Record): AblationExplanation { return { id, author, cells: Object.entries(cells).map(([factor, [a, b, c]]) => ({ factor, range: r(a, b, c) })) }; } test("6.6 五种结局里只有有效分数进入标签的求和", () => { // 手算:基线 1、完整 1.5。p1 有效 1.45(撑 0.1);p2 证据不足、p3 没测、p4 跑不起来、p5 一起关(有分数 1.1)。 // 只有 p1 有有效格,其余因子没有 → 不全。若把 p5 的 1.1 当成撑 0.8 算进去,会错贴成别的标签。 const exps = [explanation("e1", "A", { p1: [1.4, 1.45, 1.5], p2: [1, 1.2, 1.4], p3: [1, 1.2, 1.4], p4: [1, 1.2, 1.4], p5: [1, 1.2, 1.4] })]; const cells: AblationCell[] = [ { factor: "p1", outcome: { kind: "valid", score: 1.45 } }, { factor: "p2", outcome: { kind: "insufficient" } }, { factor: "p3", outcome: { kind: "untested" } }, { factor: "p4", outcome: { kind: "cannotRun" } }, { factor: "p5", outcome: { kind: "joint", off: ["p5", "p1"], score: 1.1 } }, ]; const s = settleAblation({ baseline: 1, full: 1.5, direction: "maximize", epsilon: 0.01, knownBest: null, lambda: 0.1, factors: ["p1", "p2", "p3", "p4", "p5"], cells, explanations: exps, }); assert.equal(s.label.label, "incomplete"); close(s.cells[0]!.support, 0.1); assert.deepEqual( s.cells.slice(1).map((c) => [c.outcome, c.support, c.worldBits, c.surprise]), [ ["insufficient", null, null, null], ["untested", null, null, null], ["cannotRun", null, null, null], ["joint", null, null, null], ], ); assert.equal(typeof s.cells[0]!.worldBits, "number"); // 只有 p1 判确认 / 否定。 assert.deepEqual(s.judgments, [{ id: "e1", verdicts: [{ factor: "p1", verdict: "confirmed" }] }]); // 两个因子,p1 有效、p2 一起关有分数:仍是"不全",不是 0.1 + 0.8 的"各管各"。 const two = settleAblation({ baseline: 1, full: 1.5, direction: "maximize", epsilon: 0.01, knownBest: null, lambda: 0.1, factors: ["p1", "p2"], cells: [ { factor: "p1", outcome: { kind: "valid", score: 1.45 } }, { factor: "p2", outcome: { kind: "joint", off: ["p1", "p2"], score: 1.1 } }, ], explanations: [explanation("e1", "A", { p1: [1.4, 1.45, 1.5], p2: [1, 1.2, 1.4] })], }); assert.equal(two.label.label, "incomplete"); }); test("6.6 一起关是对称的:关 p 连带 q 与关 q 连带 p 是同一种设置,后者沿用前者", () => { const s = sharedSettings([ { factor: "p2", off: ["p2", "p1"] }, { factor: "p3", off: ["p3"] }, { factor: "p1", off: ["p1", "p2"] }, ]); assert.deepEqual(s, [ { factor: "p1", key: "p1+p2", sameAs: null }, { factor: "p2", key: "p1+p2", sameAs: "p1" }, { factor: "p3", key: "p3", sameAs: null }, ]); // 不同集合不沿用。 assert.deepEqual( sharedSettings([ { factor: "p1", off: ["p1", "p2"] }, { factor: "p2", off: ["p2", "p3"] }, ]).map((x) => x.sameAs), [null, null], ); }); test("6.6 确认与否定:落在 [q1, q9] 内(含两端)确认,外面否定;只判有效格", () => { const exps = [ explanation("e1", "A", { p1: [1.45, 1.5, 1.55], p2: [1, 1.1, 1.2] }), // p1 实测 1.45 正好在 q1 上 explanation("e2", "B", { p1: [1.3, 1.4, 1.45], p2: [1.2, 1.3, 1.4] }), // p1 实测正好在 q9 上;p2 1.1 在 q1 下 ]; const cells: AblationCell[] = [ { factor: "p2", outcome: { kind: "valid", score: 1.1 } }, { factor: "p1", outcome: { kind: "valid", score: 1.45 } }, { factor: "p3", outcome: { kind: "untested" } }, ]; assert.deepEqual(judgeExplanations(exps, cells), [ { id: "e1", verdicts: [ { factor: "p1", verdict: "confirmed" }, { factor: "p2", verdict: "confirmed" }, ], }, { id: "e2", verdicts: [ { factor: "p1", verdict: "confirmed" }, { factor: "p2", verdict: "denied" }, ], }, ]); }); test("6.6 现有测法分不开:所有有效格上确认否定序列完全相同的两条以上解释归一组;没有有效格时不标", () => { const j = (id: string, vs: Array<"confirmed" | "denied">) => ({ id, verdicts: vs.map((verdict, i) => ({ factor: `p${i + 1}`, verdict })), }); assert.deepEqual( inseparableGroups([ j("e1", ["confirmed", "denied"]), j("e2", ["denied", "denied"]), j("e3", ["confirmed", "denied"]), j("e4", ["denied", "denied"]), j("e5", ["confirmed", "confirmed"]), ]), [ ["e1", "e3"], ["e2", "e4"], ], ); assert.deepEqual(inseparableGroups([j("e1", []), j("e2", [])]), []); assert.deepEqual(inseparableGroups([j("e1", ["confirmed"]), j("e2", ["denied"])]), []); }); test("4.3 ⑰ 消融中发现:比完整代码严格高出采纳线才过线;取分数最好的,同分取编号小的;一起关也算", () => { // 手算:完整 1,ε = 0.25,没有已知最好,采纳线 0.25。 const base = { full: 1, knownBest: null, epsilon: 0.25, direction: "maximize" as const }; // 正好 1.25 不过线。 assert.equal(ablationFinding({ ...base, cells: [{ factor: "p1", outcome: { kind: "valid", score: 1.25 } }] }).chosen, null); // p1 有效 1.5,p2 一起关 1.75:取 p2。 const f = ablationFinding({ ...base, cells: [ { factor: "p1", outcome: { kind: "valid", score: 1.5 } }, { factor: "p2", outcome: { kind: "joint", off: ["p1", "p2"], score: 1.75 } }, { factor: "p3", outcome: { kind: "insufficient" } }, ], }); assert.equal(f.line, 0.25); assert.deepEqual(f.passed, [ { factor: "p1", score: 1.5, improvement: 0.5 }, { factor: "p2", score: 1.75, improvement: 0.75 }, ]); assert.equal(f.chosen, "p2"); // 同分取因子编号小的(p2 < p10)。 const tie = ablationFinding({ ...base, cells: [ { factor: "p10", outcome: { kind: "valid", score: 1.5 } }, { factor: "p2", outcome: { kind: "valid", score: 1.5 } }, ], }); assert.equal(tie.chosen, "p2"); // 一起关但没有分数的不算。 assert.equal( ablationFinding({ ...base, cells: [{ factor: "p1", outcome: { kind: "joint", off: ["p1", "p2"], score: null } }] }).chosen, null, ); // 采纳线按 B = f(完整) 算:K* = 2,完整 1.5,(2 − 1.5)/4 = 0.125。 assert.equal(ablationFinding({ ...base, knownBest: 2, full: 1.5, cells: [] }).line, 0.125); }); test("同样的输入算两次,结算结果逐位相同", () => { const input = { baseline: 2.5, full: 2.6, direction: "maximize" as const, epsilon: 0.01, knownBest: 2.635983, lambda: 0.1, factors: ["p1", "p2"], cells: [ { factor: "p1", outcome: { kind: "valid" as const, score: 2.57 } }, { factor: "p2", outcome: { kind: "valid" as const, score: 2.52 } }, ], explanations: [ explanation("e1", "A", { p1: [2.55, 2.57, 2.59], p2: [2.5, 2.53, 2.56] }), explanation("e2", "B", { p1: [2.52, 2.55, 2.6], p2: [2.54, 2.56, 2.58] }), ], }; assert.deepEqual(settleAblation(input), settleAblation(input)); }); test("4.3 ⑬ 消融结算按因子清单列格:漏掉的因子直接报错,不当成不全或缺一不可", () => { // 失败场景(第二轮审阅):被采纳改法有 p1、p2 两个因子,只传 p1 一格。手算:基线 1、完整 2,p1 关掉得 1.05,撑 0.95。 // 若只按传进来的格贴标签,唯一的撑 0.95 > 0.9 会错贴成"缺一不可"。 const input = { baseline: 1, full: 2, direction: "maximize" as const, epsilon: 0.01, knownBest: null, lambda: 0.1, factors: ["p1", "p2"], cells: [{ factor: "p1", outcome: { kind: "valid" as const, score: 1.05 } }], explanations: [explanation("e1", "A", { p1: [1, 1.05, 1.1], p2: [1.4, 1.5, 1.6] })], }; assert.throws(() => settleAblation(input), /4\.3 ⑬.*p2 没有对应的格/); // 清单外的格也报错。 assert.throws( () => settleAblation({ ...input, factors: ["p1"], cells: [...input.cells, { factor: "p2", outcome: { kind: "untested" as const } }], }), /格 p2 不在/, ); // 两格都给齐时照常结算:撑 0.95 与 0.5(关 p2 得 1.5),i = 0.45 > 0.2,互补。 const ok = settleAblation({ ...input, cells: [...input.cells, { factor: "p2", outcome: { kind: "valid" as const, score: 1.5 } }] }); assert.equal(ok.label.label, "complementary"); });