60c21ae346
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>
287 lines
11 KiB
TypeScript
287 lines
11 KiB
TypeScript
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");
|
||
});
|