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
+384
View File
@@ -0,0 +1,384 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
ablationFinding,
ablationModel,
chooseBet,
evaluateCombos,
gridSize,
inseparableGroups,
informationValue,
informationValueExact,
informationValueGrid,
judgeExplanations,
measuredGrid,
OPEN_RESERVE,
pickByNet,
topCombo,
selectAll,
selectByValue,
supportOf,
synergyLabel,
totalGain,
type AblationCell,
type AblationModel,
type AblationModelInput,
type CellOutcome,
type CellRuleInput,
type SynergyLabel,
type UntestedReason,
} from "../src/rules/index.ts";
import { close, closeArray, closeGrid, loadFixture, range, scenarioPicker } from "./rules-fixtures-helpers.ts";
// ablation.json:方法.md 6.6–6.8、4.3 ⑰ 与约定 17–30,对照独立 Python 参考实现算出的预期值。
// 容差见 ablation.json tolerance:精确量 1e-12;网格值 1e-12 + 1e-9·|值|;离散结果完全相同;
// 网格对一维精确按 tolerance.gridVsExact。
const fixture = loadFixture("ablation.json");
const pick = scenarioPicker(fixture);
const TOL = 1e-12;
const SEED = "fixture-seed";
/** Python 的标签、结论、原因名 → rules 的代号。 */
const LABEL: Record<string, SynergyLabel> = {
negative: "offBetter",
incomplete: "incomplete",
indispensable: "indispensable",
complementary: "complementary",
substitute: "substitute",
independent: "independent",
};
const VERDICT: Record<string, string> = { confirmed: "confirmed", refuted: "denied" };
const REASON: Record<string, UntestedReason> = { insufficientBudget: "budget", notWorthCost: "value" };
const BET: Record<string, string> = { bet: "bet", noBetNegativeSupport: "offBetter", noBetNonPositiveGain: "notPositive" };
const componentName = (n: string): string => (n === "reserve" ? OPEN_RESERVE : n);
function modelInput(name: string): AblationModelInput & { params: Record<string, number> } {
const m = fixture.models![name]!;
const i = m.input;
return {
baseline: i.B,
full: i.full,
direction: i.direction,
knownBest: i.knownBest,
epsilon: m.params.eps!,
lambda: m.params.lambda!,
factors: i.cells,
explanations: i.explanations.map((e: any) => ({
id: e.id,
author: e.author,
cells: Object.entries(e.cells).map(([factor, r]) => ({ factor, range: range(r as any) })),
})),
params: m.params,
};
}
/** Python 的一格 {factor, kind, value?} → rules 的 AblationCell。一起关的 off 只用于沿用(sharedSettings),这里用到的函数不读它。 */
function cellOf(factor: string, o: { kind: string; value?: number }): AblationCell {
const outcome: CellOutcome =
o.kind === "valid"
? { kind: "valid", score: o.value! }
: o.kind === "joint"
? { kind: "joint", off: [factor], score: o.value ?? null }
: { kind: o.kind as "insufficient" | "untested" | "cannotRun" };
return { factor, outcome };
}
const MODEL_CACHE = new Map<string, AblationModel>();
function modelOf(name: string): AblationModel {
if (!MODEL_CACHE.has(name)) MODEL_CACHE.set(name, ablationModel(modelInput(name)));
return MODEL_CACHE.get(name)!;
}
// ---------------------------------------------------------------- 撑几成、标签(6.6)
for (const name of [
"label_method_example_independent",
"label_calc15_substitute",
"label_r1_negative",
"label_incomplete_with_other_outcomes",
"label_negative_beats_incomplete",
"label_indispensable",
"label_complementary",
"label_boundary_exact_0.9",
"label_minimize_substitute",
"label_G_not_positive",
"label_G_negative_minimize",
]) {
test(`ablation.json ${name}`, () => {
const s = pick.get(name);
const { B, full, direction, cells } = s.input;
if (s.expected.error !== undefined) {
assert.equal(s.expected.error, "totalGainNotPositive");
assert.throws(() => totalGain(B, full, direction), /G = /);
return;
}
close(totalGain(B, full, direction), s.expected.G, TOL, "G");
const supports = cells.map((c: any) => (c.kind === "valid" ? supportOf(B, full, c.value, direction) : null));
cells.forEach((c: any, i: number) => {
const e = s.expected.supports[c.factor];
if (e === null) assert.equal(supports[i], null, `${c.factor} 没有撑几成`);
else close(supports[i]!, e, TOL, `${c.factor} 撑几成`);
});
const got = synergyLabel(supports);
assert.equal(got.label, LABEL[s.expected.label], "标签");
// Python 在"关掉反而更好""不全"两种标签下不给 i;rules 在"不全"时给 null,在"关掉反而更好"且每格都有效时照给数。
if (s.expected.interaction !== null) close(got.interaction!, s.expected.interaction, TOL, "相对交互量 i");
if (got.label === "incomplete") assert.equal(got.interaction, null);
});
}
// ---------------------------------------------------------------- 确认与否定、分不开(6.6)
for (const name of ["confirm_r1_real", "confirm_calc15", "confirm_endpoint_inclusive"]) {
test(`ablation.json ${name}`, () => {
const s = pick.get(name);
const input = modelInput(s.input.model);
const cells = Object.entries(s.input.valid as Record<string, number>).map(([f, v]) => cellOf(f, { kind: "valid", value: v }));
const judgments = judgeExplanations(input.explanations, cells);
assert.deepEqual(
Object.fromEntries(judgments.map((j) => [j.id, Object.fromEntries(j.verdicts.map((v) => [v.factor, v.verdict]))])),
Object.fromEntries(
Object.entries(s.expected.verdicts as Record<string, Record<string, string>>).map(([id, row]) => [
id,
Object.fromEntries(Object.entries(row).map(([f, v]) => [f, VERDICT[v]])),
]),
),
);
assert.deepEqual(inseparableGroups(judgments), s.expected.inseparableGroups);
});
}
// ---------------------------------------------------------------- 消融中发现(4.3 ⑰)
for (const name of ["discovery_r1real", "discovery_calc15", "discovery_gainNotPositive"]) {
test(`ablation.json ${name}`, () => {
const s = pick.get(name);
const input = modelInput(s.input.model);
const got = ablationFinding({
full: input.full,
knownBest: input.knownBest,
epsilon: s.params.eps,
direction: input.direction,
cells: s.input.cells.map((c: any) => cellOf(c.factor, c)),
});
close(got.line, s.expected.line, TOL, "采纳线");
assert.deepEqual(got.passed.map((p) => p.factor), s.expected.overLine, "过线的格");
assert.equal(got.chosen, s.expected.newBaseline, "新基线");
});
}
// ---------------------------------------------------------------- 成分、先验、信息价值(6.7)
function gridVsExactTol(k: number): number {
// tolerance.gridVsExact:k ≤ 3 时 5e-4·|精确值|,k = 4 时 3e-3·|精确值|。
return k <= 3 ? 5e-4 : 3e-3;
}
for (const name of ["calc15", "r1real", "minimize", "fourCells", "negativeG0"]) {
test(`ablation.json voi_${name}`, () => {
const s = pick.get(`voi_${name}`);
const e = s.expected;
const model = modelOf(name);
const idx = (f: string): number => model.factors.indexOf(f);
close(model.gain, e.G, TOL, "G");
close(model.space, e.h, TOL, "h");
assert.deepEqual(model.components, e.componentNames.map(componentName), "成分名单");
closeArray(model.weights, e.weights, TOL, "起始权重");
for (const [f, ends] of Object.entries(e.reserve as Record<string, number[]>)) {
const k = model.rawKnots[idx(f)]!.at(-1)!;
closeArray([k[0]![0], k.at(-1)![0]], ends, TOL, `${f} 的开放预留`);
}
e.componentNames.forEach((n: string, j: number) => {
for (const [f, v] of Object.entries(e.componentExpectedPayoff[n] as Record<string, number>)) {
close(model.space * model.expected[idx(f)]![j]!, v, TOL, `成分 ${n} 在 ${f} 的期望收益`);
}
});
for (const [f, v] of Object.entries(e.priorExpectedPayoff as Record<string, number>)) {
close(model.space * model.prior[idx(f)]!, v, TOL, `${f} 的事前期望收益`);
}
close(evaluateCombos(model, { balance: 0, batchCap: 2, u: 0.5, decay: 0.5 }).g0, e.g0, TOL, "ĝ₀");
const all = model.factors.map((_, c) => c);
const exact = informationValueExact(model);
close(exact, e.voiFullExact, TOL, "全测 VOI(一维精确)");
close(informationValue(model, all), e.voiFullExact, TOL, "全测 VOI(informationValue 走精确)");
// 网格:先核对每格折点数、小段数与 Python 一致(差一个 ulp 会多出一段),再比 VOI。
const checkGrid = (S: string[], info: any, what: string): void => {
assert.equal(gridSize(S.length), info.m, `${what}:m`);
for (const f of S) {
// 折点在分数原值上取(measuredGrid),两边都按浮点原式算、只合并完全相等的值。
const brk = new Set(model.rawKnots[idx(f)]!.flatMap((k) => k.map(([x]) => x))).size;
assert.equal(brk, info.cells[f].breakpoints, `${what} ${f}:折点数`);
assert.equal(measuredGrid(model, idx(f), S.length).mids.length, info.cells[f].subsegments, `${what} ${f}:小段数`);
}
};
for (const row of e.partial) {
const S = row.cells as string[];
checkGrid(S, row.grid, `测 ${S.join(",")}`);
const v = informationValueGrid(model, S.map(idx));
closeGrid(v, row.voiGrid, `测 ${S.join(",")} 的网格 VOI`);
closeGrid(informationValue(model, S.map(idx)), row.voiGrid, `测 ${S.join(",")} 的 informationValue`);
if (row.exactK1VOI !== undefined) {
// 只测一格的精确参考值:rules 没有这个算法,只核对网格与它的差在 gridVsExact 的 k ≤ 3 界内。
close(v, row.exactK1VOI, gridVsExactTol(1) * Math.abs(row.exactK1VOI), `测 ${S[0]} 的网格对精确`);
}
}
checkGrid(model.factors, e.gridFullInfo, "全测走网格");
const gridFull = informationValueGrid(model, all);
closeGrid(gridFull, e.gridFullVOI, "全测走网格的 VOI");
close(gridFull, exact, gridVsExactTol(all.length) * Math.abs(exact), "全测:网格对一维精确");
// tolerance.gridVsExact:每格 s ≥ 1 时网格值不超过精确值(Jensen);fourCells 有 s = 0 的格,不适用。
if (name !== "fourCells") assert.ok(gridFull <= exact + 1e-12, `全测网格 ${gridFull} 不应超过精确 ${exact}`);
});
}
// ---------------------------------------------------------------- 价值、成本、选格(6.7)
for (const name of ["calc15", "r1real", "minimize", "negativeG0"]) {
test(`ablation.json select_${name}`, () => {
const s = pick.get(`select_${name}`);
const { params, ...input } = modelInput(name);
const model = modelOf(name);
for (const e of s.expected.byBalance) {
const tag = `余额 ${e.balance}`;
const ruleInput: CellRuleInput = {
...input,
balance: e.balance,
batchCap: params.batchCap!,
u: params.u!,
decay: params.r!,
seed: SEED,
};
const { g0, combos } = evaluateCombos(model, ruleInput);
close(g0, s.expected.g0, TOL, `${tag} ĝ₀`);
if (name === "negativeG0") {
// ĝ₀ < 0:原始值照记,成本按 max(0, ĝ₀) = 0 算(6.7"价值减成本"、约定 28)。
assert.ok(g0 < 0, `${tag} ĝ₀ 应为负`);
assert.ok(combos.length > 0 && combos.every((c) => c.cost === 0 && c.net === c.value), `${tag} 成本为 0、净值 = 价值`);
assert.equal(selectByValue(ruleInput).g0, g0, `${tag} 选格记录里的 ĝ₀ 是原始负值`);
}
assert.deepEqual(combos.map((c) => c.factors), e.combos.map((c: any) => c.combo), `${tag} 列出的组合`);
combos.forEach((c, i) => {
const r = e.combos[i];
const what = `${tag} {${c.factors.join(",")}}`;
// 全测用一维精确(精确量容差),部分格用网格(网格容差)。
const cmp = c.factors.length === model.factors.length ? (a: number, b: number, w: string) => close(a, b, TOL, w) : closeGrid;
cmp(c.voi, r.voi, `${what} VOI`);
assert.equal(c.rHat, r.Rhat, `${what} R̂`);
close(c.mPrime, r.mPrime, TOL, `${what} m′`);
cmp(c.value, r.value, `${what} 价值`);
close(c.cost, r.cost, TOL, `${what} 成本`);
closeGrid(c.net, r.net, `${what} 净值`);
});
const d = selectByValue(ruleInput);
assert.deepEqual(d.chosen, e.selected, `${tag} 选中的格`);
closeGrid(d.chosenNet, e.selectedNet, `${tag} 选中组合的净值`);
for (const cell of d.cells) {
const u = e.unselected[cell.factor];
if (cell.tested) {
assert.equal(u, undefined, `${tag} ${cell.factor} 应选中`);
continue;
}
assert.equal(cell.reason, REASON[u.reason], `${tag} ${cell.factor} 没测的原因`);
if (u.bestCombo === null) assert.equal(cell.bestCombo, null, `${tag} ${cell.factor} 没有可选组合`);
else {
assert.deepEqual(cell.bestCombo!.factors, u.bestCombo, `${tag} ${cell.factor} 含它的最好组合`);
closeGrid(cell.bestCombo!.net, u.bestNet, `${tag} ${cell.factor} 含它的最好组合的净值`);
}
}
const a = selectAll(ruleInput);
assert.deepEqual(a.chosen, e.fullRule.selected, `${tag} 全测规则选中的格`);
assert.deepEqual(
a.cells.filter((c) => !c.tested).map((c) => [c.factor, c.reason]),
e.fullRule.insufficientBudget.map((f: string) => [f, "budget"]),
`${tag} 全测规则放不下的格`,
);
}
});
}
// 纯选格:给定净值表,核对 1e-12 容差与平局(6.7"选格"第 2 条、约定 29):pickByNet 选组合,topCombo 挑
// "含这一格的最好组合"。没选中的原因在 selectByValue 里由模型算出的净值表决定,这里覆盖不到。
for (const name of [
"select_pure_net_at_tolerance",
"select_pure_just_above_tolerance",
"select_pure_tie_chain",
"select_pure_budget_reason",
"select_pure_tolerance_relay",
"select_pure_filter_before_max",
]) {
test(`ablation.json ${name}`, () => {
const s = pick.get(name);
const cells = s.input.cells as string[];
const limit = Math.min(cells.length, s.input.balance - 1);
// 组合按格数从少到多、同格数按因子编号字典序(约定 29),只列格数 ≤ 余额 − 1 的。
const combos: Array<{ factors: string[]; net: number }> = [];
for (let n = 1; n <= limit; n++) {
const walk = (start: number, acc: string[]): void => {
if (acc.length === n) {
combos.push({ factors: acc, net: s.input.nets[acc.join(",")] });
return;
}
for (let i = start; i < cells.length; i++) walk(i + 1, [...acc, cells[i]!]);
};
walk(0, []);
}
const chosen = pickByNet(combos);
assert.deepEqual(chosen, s.expected.selected, "选中的格");
const net = chosen.length === 0 ? 0 : s.input.nets[chosen.join(",")];
assert.equal(net, s.expected.selectedNet, "选中组合的净值");
// 没选中的格附"含这一格的净值最大组合",按同一条规则挑(topCombo)。
for (const [f, u] of Object.entries(s.expected.unselected as Record<string, any>)) {
assert.ok(!chosen.includes(f));
const best = topCombo(combos.filter((c) => c.factors.includes(f)));
if (u.bestCombo === null) assert.equal(best, null, `${f} 没有可选组合`);
else {
assert.deepEqual(best!.factors, u.bestCombo, `${f} 含它的最好组合`);
assert.equal(best!.net, u.bestNet, `${f} 含它的最好组合的净值`);
}
}
});
}
// ---------------------------------------------------------------- 押的因子(6.8)
for (const name of [
"bet_calc15_all_valid",
"bet_calc15_p2_insufficient",
"bet_calc15_joint_ignored",
"bet_r1_negative_support",
"bet_gain_not_positive",
"bet_weights_not_updatable",
"bet_minimize",
]) {
test(`ablation.json ${name}`, () => {
const s = pick.get(name);
const { params: _, ...input } = modelInput(s.input.model);
const e = s.expected;
const got = chooseBet({
...input,
cells: Object.entries(s.input.outcomes as Record<string, any>).map(([f, o]) => cellOf(f, o)),
});
assert.equal(!got.weightsKept, e.weightsUpdated, "权重是否更新");
const names = Object.keys(e.weights);
assert.deepEqual(modelOf(s.input.model).components, names.map(componentName), "成分名单");
closeArray(got.weights, names.map((n) => e.weights[n]), TOL, "更新后的权重");
assert.deepEqual(got.gains.map((g) => g.factor), Object.keys(e.payoffs), "因子");
got.gains.forEach((g) => close(g.gain, e.payoffs[g.factor], TOL, `${g.factor} 方向收益`));
assert.equal(got.factor, e.picked, "押的因子");
close(got.predictedGain, e.predictedGain, TOL, "预测收益");
assert.equal(got.decision, BET[e.status], "押不押");
assert.deepEqual(got.offBetter.map((o) => o.factor), e.negativeFactors.map((n: any) => n.factor), "关掉反而更好的格");
got.offBetter.forEach((o, i) => close(o.better, e.negativeFactors[i].offBetterBy, TOL, `${o.factor} 关掉后好多少`));
});
}
test("ablation.json 每个场景都已覆盖(value_cost_pure 除外,见下)", () => {
// value_cost_pure 直接给 ĝ₀ 和 VOI 算 R̂、m′、价值、成本;rules 只在 evaluateCombos 里从模型算这些,
// 没有接受 (ĝ₀, VOI) 的对外函数,无法映射。R̂、m′、价值、成本已在 select_* 场景里逐组合核对,
// ĝ₀ < 0 时成本为 0 由 select_negativeG0 核对。
pick.get("value_cost_pure");
assert.deepEqual(pick.unused(), []);
});