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>
162 lines
5.6 KiB
TypeScript
162 lines
5.6 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { test } from "node:test";
|
||
import {
|
||
ablationCellLimit,
|
||
assignEntries,
|
||
decisionSeed,
|
||
forwardBatchSize,
|
||
runBalance,
|
||
seededRandom,
|
||
seededSample,
|
||
type RunState,
|
||
} from "../src/rules/index.ts";
|
||
|
||
// 预期值来源写在每条测试旁边。
|
||
|
||
test("第七部分扣次示例:运行次数 13、每批上限 2,逐行余额、每轮 k 与消融上限", () => {
|
||
// 来源:方法.md 第七部分示例表。每一行先按当时余额算出 k 或消融上限,再把这些运行记成确认启动。
|
||
const total = 13;
|
||
const cap = 2;
|
||
const runs: RunState[] = [];
|
||
const spend = (n: number): number => {
|
||
for (let i = 0; i < n; i++) runs.push("started");
|
||
return runBalance(total, runs).balance;
|
||
};
|
||
assert.equal(spend(1), 12); // 基线
|
||
assert.equal(forwardBatchSize(cap, 12), 2);
|
||
assert.equal(spend(2), 10); // 第 1 轮正向
|
||
assert.equal(ablationCellLimit(10), 9);
|
||
assert.equal(spend(2), 8); // 第 1 轮消融选中 2 格
|
||
assert.equal(forwardBatchSize(cap, 8), 2);
|
||
assert.equal(spend(2), 6); // 第 2 轮正向
|
||
assert.equal(forwardBatchSize(cap, 6), 2);
|
||
assert.equal(spend(2), 4); // 第 3 轮正向
|
||
assert.equal(ablationCellLimit(4), 3);
|
||
assert.equal(spend(0), 4); // 第 3 轮消融一格都不测
|
||
assert.equal(forwardBatchSize(cap, 4), 2);
|
||
assert.equal(spend(2), 2); // 第 4 轮正向
|
||
assert.equal(forwardBatchSize(cap, 2), 2);
|
||
assert.equal(spend(2), 0); // 第 5 轮正向,余额用完
|
||
assert.equal(runBalance(total, runs).charged, 13);
|
||
});
|
||
|
||
test("第七部分:确认启动的扣,确认没启动的退还,已预留还没结果的先占着名额", () => {
|
||
assert.deepEqual(runBalance(5, ["started", "reserved", "notStarted", "started"]), {
|
||
charged: 2,
|
||
reserved: 1,
|
||
balance: 2,
|
||
});
|
||
assert.deepEqual(runBalance(3, []), { charged: 0, reserved: 0, balance: 3 });
|
||
});
|
||
|
||
test("第七部分:余额 0、1、2 时的正向批量与消融上限", () => {
|
||
// 手算:k = min(2, 余额);消融上限 = 余额 − 1,余额为 0 或 1 时是 0。
|
||
assert.deepEqual([0, 1, 2].map((b) => forwardBatchSize(2, b)), [0, 1, 2]);
|
||
assert.deepEqual([0, 1, 2].map(ablationCellLimit), [0, 0, 1]);
|
||
});
|
||
|
||
test("4.2 入口分配第 1、2 条:只有一个人选就给他;几人同选时作者优先", () => {
|
||
// 手算:e2 被 r1、r2 同时选,作者 r2 在其中 → r2;e3 只有 r3 选 → r3;第 2 轮 r1 选 e1 → r1,第 2 志愿。
|
||
assert.deepEqual(
|
||
assignEntries({
|
||
researchers: [
|
||
{ id: "r1", choices: ["e2", "e1"] },
|
||
{ id: "r2", choices: ["e2", "e3"] },
|
||
{ id: "r3", choices: ["e3", "e1"] },
|
||
],
|
||
entries: [
|
||
{ id: "e1", author: "r1" },
|
||
{ id: "e2", author: "r2" },
|
||
{ id: "e3", author: "r3" },
|
||
{ id: "e4", author: "r1" },
|
||
],
|
||
}),
|
||
[
|
||
{ researcher: "r1", entry: "e1", rank: 2 },
|
||
{ researcher: "r2", entry: "e2", rank: 1 },
|
||
{ researcher: "r3", entry: "e3", rank: 1 },
|
||
],
|
||
);
|
||
});
|
||
|
||
test("4.2 入口分配第 2、3 条:作者不在其中给编号最小的;志愿用完的人按编号拿剩下编号最小的入口", () => {
|
||
// 手算:e3(作者 r3)被 r1、r2 同选,r3 不在其中 → r1。r3 选 e1 → r3。
|
||
// r2 志愿用完;剩下 e2、e4 → r2 拿 e2,rank 为 null。
|
||
assert.deepEqual(
|
||
assignEntries({
|
||
researchers: [
|
||
{ id: "r1", choices: ["e3"] },
|
||
{ id: "r2", choices: ["e3"] },
|
||
{ id: "r3", choices: ["e1"] },
|
||
],
|
||
entries: [
|
||
{ id: "e1", author: "r1" },
|
||
{ id: "e2", author: "r2" },
|
||
{ id: "e3", author: "r3" },
|
||
{ id: "e4", author: "r1" },
|
||
],
|
||
}),
|
||
[
|
||
{ researcher: "r1", entry: "e3", rank: 1 },
|
||
{ researcher: "r2", entry: "e2", rank: null },
|
||
{ researcher: "r3", entry: "e1", rank: 1 },
|
||
],
|
||
);
|
||
// 两个人都落空:按编号各拿剩下的编号最小的。
|
||
assert.deepEqual(
|
||
assignEntries({
|
||
researchers: [
|
||
{ id: "r1", choices: ["e1"] },
|
||
{ id: "r2", choices: ["e1"] },
|
||
{ id: "r3", choices: ["e1"] },
|
||
],
|
||
entries: [
|
||
{ id: "e1", author: "r9" },
|
||
{ id: "e2", author: "r9" },
|
||
{ id: "e3", author: "r9" },
|
||
],
|
||
}).map((a) => [a.researcher, a.entry, a.rank]),
|
||
[
|
||
["r1", "e1", 1],
|
||
["r2", "e2", null],
|
||
["r3", "e3", null],
|
||
],
|
||
);
|
||
});
|
||
|
||
test("4.2 N = 1 时直接分第一志愿", () => {
|
||
assert.deepEqual(
|
||
assignEntries({
|
||
researchers: [{ id: "r1", choices: ["e3", "e1"] }],
|
||
entries: [
|
||
{ id: "e1", author: "r1" },
|
||
{ id: "e2", author: "r1" },
|
||
{ id: "e3", author: "r1" },
|
||
],
|
||
}),
|
||
[{ researcher: "r1", entry: "e3", rank: 1 }],
|
||
);
|
||
});
|
||
|
||
test("第九部分:种子 = hash({ studyId, round, version })", () => {
|
||
// 预期哈希用命令行 printf '%s' '{"round":1,"studyId":"s1","version":1}' | sha256sum 算出。
|
||
assert.equal(
|
||
decisionSeed({ studyId: "s1", round: 1, version: 1 }),
|
||
"0fcf4530be51b098065a8f4a8774afc2c0be8f3a6de9b3b670bd26396d2c18ea",
|
||
);
|
||
});
|
||
|
||
test("第九部分:同一个种子永远得到同一串数;抽样只取决于种子和编号集合", () => {
|
||
const a = seededRandom("seed");
|
||
const b = seededRandom("seed");
|
||
const xs = Array.from({ length: 50 }, () => a.uniformInt(7));
|
||
assert.deepEqual(
|
||
Array.from({ length: 50 }, () => b.uniformInt(7)),
|
||
xs,
|
||
);
|
||
assert.ok(xs.every((x) => Number.isInteger(x) && x >= 0 && x < 7));
|
||
assert.deepEqual(seededSample(["c", "a", "b", "d"], 2, "k"), seededSample(["d", "b", "a", "c"], 2, "k"));
|
||
assert.deepEqual(seededSample(["c", "a", "b"], 3, "k"), ["a", "b", "c"]);
|
||
assert.deepEqual(seededSample(["c", "a", "b"], 0, "k"), []);
|
||
});
|