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>
This commit is contained in:
2026-10-02 01:50:21 -04:00
parent bfb96ba7d7
commit 60c21ae346
37 changed files with 14067 additions and 3 deletions
+286
View File
@@ -0,0 +1,286 @@
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");
});