Files
InfoRSI/tests/rules-bits.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

167 lines
6.4 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 {
authorVsMerged,
bitWidth,
cellIndex,
coverage,
disagreement,
placement,
surprise,
trackRecord,
worldBits,
type Mixture,
} from "../src/rules/index.ts";
// 预期值来源写在每条测试旁边。比特的例子取 ε = 25(Δ = 0.25),格边界在二进制里精确。
function close(actual: number, expected: number, tol = 1e-12): void {
assert.ok(Math.abs(actual - expected) <= tol, `实际 ${actual},预期 ${expected}`);
}
/** 很窄的范围 (y − 0.01, y, y + 0.01),连尾巴 [y − 0.02, y + 0.02]。 */
const narrow = (y: number) => ({ q1: y - 0.01, q5: y, q9: y + 0.01 });
const uniform = (lo: number, hi: number): Mixture => [
{
weight: 1,
knots: [
[lo, 0],
[hi, 1],
],
},
];
test("6.9 格宽 Δ = ε/100,跟随 ε", () => {
assert.equal(bitWidth(0.01), 0.01 / 100);
assert.equal(bitWidth(25), 0.25);
});
test("6.9 世界比特:从 b 起切;[0,1] 均匀切 4 格是 2 比特,错开 1/8 是 2.25 比特;整个落在一格里是 0", () => {
// 手算:[0,1] 均匀、b = 0、Δ = 0.25:四格各 1/4,H = 2。
close(worldBits(uniform(0, 1), 0, 0.25), 2);
// [0.125, 1.125]:五格 1/8、1/4、1/4、1/4、1/8,H = 2 × (1/8 × 3) + 3 × (1/4 × 2) = 0.75 + 1.5 = 2.25。
close(worldBits(uniform(0.125, 1.125), 0, 0.25), 2.25);
// 同一分布、b = 0.125:切口对齐,四格各 1/4,又是 2 比特。说明格从 b 起切。
close(worldBits(uniform(0.125, 1.125), 0.125, 0.25), 2);
// [0.3, 0.31] 整个在 [0.25, 0.5) 这一格里:把握 1,0 比特。
assert.equal(worldBits(uniform(0.3, 0.31), 0, 0.25), 0);
});
test("6.9 惊讶 = −log₂ p;正好在格边界上的实测归右边那格;事前把握为 0 时给明确状态", () => {
// 手算:[0,1] 均匀,实测 0.3 落在 [0.25, 0.5),p = 1/4,惊讶 2 比特。
assert.deepEqual(surprise(uniform(0, 1), 0, 0.25, 0.3), { cell: 1, prior: 0.25, bits: { status: "measured", value: 2 } });
assert.equal(surprise(uniform(0, 1), 0, 0.25, 0.25).cell, 1);
assert.deepEqual(surprise(uniform(0, 1), 0, 0.25, 2), {
cell: 8,
prior: 0,
bits: { status: "unmeasured", reason: "事前把握为 0,惊讶算不出" },
});
});
test("6.9 找实测所在格用和切格同一套边界:b + jΔ 正好落在第 j 格", () => {
// b = 2.5、Δ = 1e-4:(b + 3Δ − b)/Δ 在浮点里可能是 2.999…,按边界核对后仍是 3。
for (let j = -5; j <= 50; j++) assert.equal(cellIndex(2.5, 1e-4, 2.5 + j * 1e-4), j);
for (let j = -5; j <= 50; j++) assert.equal(cellIndex(2.635983, 1e-4, 2.635983 + j * 1e-4), j);
});
test("6.9 分歧量:大家的范围完全一样时为 0;两个窄范围各在一格时为 1 比特", () => {
const r = { q1: 0.2, q5: 0.5, q9: 0.9 };
close(
disagreement(
[
{ id: "A", author: "A", range: r },
{ id: "B", author: "B", range: r },
],
0,
0.25,
),
0,
);
// 手算:A 交 (0.09, 0.1, 0.11),连尾巴 [0.08, 0.12],整个在 [0, 0.25) 里;B 交 (0.59, 0.6, 0.61),整个在 [0.5, 0.75) 里。
// 各自 0 比特;平均后两格各一半,1 比特。
close(
disagreement(
[
{ id: "A", author: "A", range: narrow(0.1) },
{ id: "B", author: "B", range: narrow(0.6) },
],
0,
0.25,
),
1,
);
});
test("6.9 落点四段与覆盖率:边界归属;没有样本时覆盖率测不到", () => {
const r = { q1: 1, q5: 2, q9: 3 };
assert.equal(placement(r, 0.5), "belowQ1");
assert.equal(placement(r, 1), "q1ToQ5");
assert.equal(placement(r, 2), "q5ToQ9");
assert.equal(placement(r, 3), "q5ToQ9");
assert.equal(placement(r, 3.5), "aboveQ9");
const c = coverage([0.5, 1, 2, 3, 3.5].map((value) => ({ range: r, value })));
assert.deepEqual(c.counts, { belowQ1: 1, q1ToQ5: 1, q5ToQ9: 2, aboveQ9: 1 });
assert.equal(c.covered, 3);
assert.deepEqual(c.rate, { status: "measured", value: 0.6 });
assert.deepEqual(coverage([]).rate, { status: "unmeasured", reason: "没有样本" });
});
test("6.10 战绩:中位数偏高偏低、穿上下沿、包住,按人、按正向与消融分开", () => {
const r = { q1: 1, q5: 2, q9: 3 };
const t = trackRecord([
{ researcher: "B", phase: "forward", range: r, value: 2.5 },
{ researcher: "A", phase: "ablation", range: r, value: 2 },
{ researcher: "A", phase: "forward", range: r, value: 0.5 },
{ researcher: "A", phase: "forward", range: r, value: 3.5 },
{ researcher: "A", phase: "forward", range: r, value: 2.5 },
]);
assert.deepEqual(
t.map((x) => [x.researcher, x.phase]),
[
["A", "forward"],
["A", "ablation"],
["B", "forward"],
],
);
// A 正向:0.5 → 中位数偏高、穿下沿;3.5 → 偏低、穿上沿;2.5 → 偏低、包住。
assert.deepEqual(t[0]!.tally, {
total: 3,
medianHigh: 1,
medianLow: 2,
aboveTop: 1,
belowBottom: 1,
covered: 1,
placements: { belowQ1: 1, q1ToQ5: 0, q5ToQ9: 1, aboveQ9: 1 },
});
// A 消融:实测正好等于 q5,不算偏高也不算偏低。
assert.equal(t[1]!.tally.medianHigh + t[1]!.tally.medianLow, 0);
});
test("6.10 作者自评 vs 大家合并:作者一半按方向比合并一半好多少,正数是作者更乐观", () => {
const item = { candidate: "c1", author: "A", authorRange: { q1: 2, q5: 2.5, q9: 3 }, mergedRange: { q1: 1, q5: 2, q9: 3 }, value: 2.25 };
assert.deepEqual(authorVsMerged([item], "maximize"), [
{ candidate: "c1", author: "A", optimism: 0.5, authorPlacement: "q1ToQ5", mergedPlacement: "q5ToQ9" },
]);
assert.equal(authorVsMerged([item], "minimize")[0]!.optimism, -0.5);
});
test("6.9 分歧量只量人与人之间:同一人几条解释先等权平均成他的折线", () => {
// 手算:A、B 各交两条解释,都是 (0.09, 0.1, 0.11) 和 (0.59, 0.6, 0.61),各自整个落在 [0, 0.25)、[0.5, 0.75) 一格里。
// 每人的折线都是"两格各一半",各 1 比特;平均后仍是两格各一半,1 比特;分歧量 = 1 − 1 = 0。
// 若把每条解释各算比特(各 0 比特)再平均,会得出 1。
close(
disagreement(
[
{ id: "e1", author: "A", range: narrow(0.1) },
{ id: "e2", author: "A", range: narrow(0.6) },
{ id: "e3", author: "B", range: narrow(0.1) },
{ id: "e4", author: "B", range: narrow(0.6) },
],
0,
0.25,
),
0,
);
});