Files
InfoRSI/tests/rules-ablation.test.ts
T
iomgaa 60c21ae346 feat: 研究规则 rules 模块(任务书 02)
rules 是纯计算,src/rules 共 1761 行:方法.md 第六部分的全部公式、第七部分的余额与扣次、4.2 的入口分配,以及种子与规则登记表。

预期值有三个来源:方法.md 的手算例;tests/fixtures/rules/ 下不看实现写的 Python 脚本;参考项目 calc15、calc14b 里与现行方法一致的部分。审阅做了两轮,每轮都换一个全新上下文的子 Agent,提出的问题都已修复或写明不修的理由。

用户确认的约定(原话"1–8 都同意"):入池、采纳、消融中发现都用严格不等号;成本用 max(0, ĝ₀);没测的格附"含它的最好组合"的净值;战绩在分数原值上判。三个数改为严格递增后,删掉了尖峰的全部专门处理(bfb96ba)。vocab 里 Range 的说明同步改为严格递增。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:50:21 -04:00

287 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, [number, number, number]>): 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");
});