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
+943
View File
@@ -0,0 +1,943 @@
{
"generator": "gen_forward_info.py",
"tolerance": {
"abs": 1e-12,
"note": "全是离散结果,必须完全相同。"
},
"scenarios": [
{
"name": "entries_rule1_unique",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"r1",
"r2",
"r3"
],
"entries": [
{
"id": "e1",
"author": "r1"
},
{
"id": "e2",
"author": "r1"
},
{
"id": "e3",
"author": "r2"
},
{
"id": "e4",
"author": "r2"
},
{
"id": "e5",
"author": "r3"
},
{
"id": "e6",
"author": "r3"
}
],
"preferences": {
"r1": [
"e1",
"e3",
"e5"
],
"r2": [
"e3",
"e1",
"e5"
],
"r3": [
"e6",
"e1",
"e2"
]
}
},
"expected": {
"assignments": {
"r1": {
"entry": "e1",
"choice": 1
},
"r2": {
"entry": "e3",
"choice": 1
},
"r3": {
"entry": "e6",
"choice": 1
}
}
},
"note": "规则 1:第 1 志愿互不相同,各得第 1 志愿。"
},
{
"name": "entries_rule2_author_wins",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"r1",
"r2",
"r3"
],
"entries": [
{
"id": "e1",
"author": "r1"
},
{
"id": "e2",
"author": "r1"
},
{
"id": "e3",
"author": "r2"
},
{
"id": "e4",
"author": "r2"
},
{
"id": "e5",
"author": "r3"
},
{
"id": "e6",
"author": "r3"
}
],
"preferences": {
"r1": [
"e3",
"e1",
"e2"
],
"r2": [
"e3",
"e4",
"e1"
],
"r3": [
"e3",
"e5",
"e1"
]
}
},
"expected": {
"assignments": {
"r1": {
"entry": "e1",
"choice": 2
},
"r2": {
"entry": "e3",
"choice": 1
},
"r3": {
"entry": "e5",
"choice": 2
}
}
},
"note": "规则 2:三人都选 e3,作者 r2 在其中,归 r2;r1、r3 进入第 2 轮,各得 e1、e5(第 2 志愿)。"
},
{
"name": "entries_rule2_smallest_index",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"r1",
"r2",
"r3"
],
"entries": [
{
"id": "e1",
"author": "r1"
},
{
"id": "e2",
"author": "r1"
},
{
"id": "e3",
"author": "r2"
},
{
"id": "e4",
"author": "r2"
},
{
"id": "e5",
"author": "r3"
},
{
"id": "e6",
"author": "r3"
}
],
"preferences": {
"r1": [
"e5",
"e2",
"e3"
],
"r2": [
"e1",
"e4",
"e5"
],
"r3": [
"e1",
"e4",
"e2"
]
}
},
"expected": {
"assignments": {
"r1": {
"entry": "e5",
"choice": 1
},
"r2": {
"entry": "e1",
"choice": 1
},
"r3": {
"entry": "e4",
"choice": 2
}
}
},
"note": "规则 2:r2、r3 同选 e1,作者 r1 不在其中,归编号小的 r2;第 2 轮 r3 选 e4 得到。"
},
{
"name": "entries_identical_prefs",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"r1",
"r2",
"r3"
],
"entries": [
{
"id": "e1",
"author": "r1"
},
{
"id": "e2",
"author": "r1"
},
{
"id": "e3",
"author": "r2"
},
{
"id": "e4",
"author": "r2"
},
{
"id": "e5",
"author": "r3"
},
{
"id": "e6",
"author": "r3"
}
],
"preferences": {
"r1": [
"e1",
"e2",
"e3"
],
"r2": [
"e1",
"e2",
"e3"
],
"r3": [
"e1",
"e2",
"e3"
]
}
},
"expected": {
"assignments": {
"r1": {
"entry": "e1",
"choice": 1
},
"r2": {
"entry": "e2",
"choice": 2
},
"r3": {
"entry": "e3",
"choice": 3
}
}
},
"note": "三人志愿完全相同:第 1 轮 e1 归作者 r1;第 2 轮 r2、r3 选 e2,作者 r1 已分到不在其中,归 r2;第 3 轮 r3 选 e3 得到。"
},
{
"name": "entries_rule3_exhausted",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"r1",
"r2",
"r3"
],
"entries": [
{
"id": "e1",
"author": "r1"
},
{
"id": "e2",
"author": "r1"
},
{
"id": "e3",
"author": "r2"
},
{
"id": "e4",
"author": "r2"
},
{
"id": "e5",
"author": "r3"
},
{
"id": "e6",
"author": "r3"
}
],
"preferences": {
"r1": [
"e3",
"e5"
],
"r2": [
"e3",
"e5"
],
"r3": [
"e3",
"e5"
]
}
},
"expected": {
"assignments": {
"r1": {
"entry": "e1",
"choice": null
},
"r2": {
"entry": "e3",
"choice": 1
},
"r3": {
"entry": "e5",
"choice": 2
}
}
},
"note": "志愿用完:第 1 轮 e3 归作者 r2;第 2 轮 r1、r3 选 e5,作者 r3 在其中归 r3;r1 志愿用完,按规则 3 拿剩下编号最小的 e1,choice 为 null。"
},
{
"name": "entries_N1",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"r1"
],
"entries": [
{
"id": "e1",
"author": "r1"
},
{
"id": "e2",
"author": "r1"
}
],
"preferences": {
"r1": [
"e2",
"e1"
]
}
},
"expected": {
"assignments": {
"r1": {
"entry": "e2",
"choice": 1
}
}
},
"note": "N=1:直接给第一志愿。"
},
{
"name": "entries_natural_rule2",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"researcher-10",
"researcher-3",
"researcher-2"
],
"entries": [
{
"id": "e10",
"author": "researcher-2"
},
{
"id": "e1",
"author": "researcher-3"
},
{
"id": "e2",
"author": "researcher-3"
}
],
"preferences": {
"researcher-10": [
"e1"
],
"researcher-2": [
"e1"
],
"researcher-3": [
"e10"
]
}
},
"expected": {
"assignments": {
"researcher-2": {
"entry": "e1",
"choice": 1
},
"researcher-3": {
"entry": "e10",
"choice": 1
},
"researcher-10": {
"entry": "e2",
"choice": null
}
}
},
"note": "规则 2:researcher-10 与 researcher-2 同选 e1,作者 researcher-3 不在其中,归编号小的 researcher-2(按字符串序会错给 researcher-10);researcher-3 独选 e10;researcher-10 志愿用完,按规则 3 拿剩下的 e2。"
},
{
"name": "entries_natural_rule3",
"section": "4.2 分配规则;约定 38",
"params": {},
"input": {
"researchers": [
"researcher-10",
"researcher-3",
"researcher-2"
],
"entries": [
{
"id": "e10",
"author": "researcher-2"
},
{
"id": "e1",
"author": "researcher-3"
},
{
"id": "e2",
"author": "researcher-3"
}
],
"preferences": {
"researcher-10": [
"e1"
],
"researcher-2": [
"e1"
],
"researcher-3": [
"e1"
]
}
},
"expected": {
"assignments": {
"researcher-2": {
"entry": "e2",
"choice": null
},
"researcher-3": {
"entry": "e1",
"choice": 1
},
"researcher-10": {
"entry": "e10",
"choice": null
}
}
},
"note": "三人同选 e1,作者 researcher-3 在其中归他;researcher-2、researcher-10 志愿用完,按编号从小到大拿剩下编号最小的入口:researcher-2 拿 e2,researcher-10 拿 e10(按字符串序会把 e10 排在 e2 前面)。"
},
{
"name": "balance_part7_table",
"section": "第七部分示例;约定 35–36",
"params": {
"totalRuns": 13,
"batchCap": 2
},
"input": {
"steps": [
{
"step": "基线",
"added": [
"started"
]
},
{
"step": "第 1 轮正向",
"added": [
"started",
"started"
]
},
{
"step": "第 1 轮消融",
"added": [
"started",
"started"
]
},
{
"step": "第 2 轮正向",
"added": [
"started",
"started"
]
},
{
"step": "第 3 轮正向",
"added": [
"started",
"started"
]
},
{
"step": "第 3 轮消融",
"added": []
},
{
"step": "第 4 轮正向",
"added": [
"started",
"started"
]
},
{
"step": "第 5 轮正向",
"added": [
"started",
"started"
]
}
]
},
"expected": {
"afterEachStep": [
{
"step": "基线",
"added": [
"started"
],
"charged": 1,
"occupied": 1,
"balance": 12,
"forwardK": 2,
"ablationMax": 11
},
{
"step": "第 1 轮正向",
"added": [
"started",
"started"
],
"charged": 3,
"occupied": 3,
"balance": 10,
"forwardK": 2,
"ablationMax": 9
},
{
"step": "第 1 轮消融",
"added": [
"started",
"started"
],
"charged": 5,
"occupied": 5,
"balance": 8,
"forwardK": 2,
"ablationMax": 7
},
{
"step": "第 2 轮正向",
"added": [
"started",
"started"
],
"charged": 7,
"occupied": 7,
"balance": 6,
"forwardK": 2,
"ablationMax": 5
},
{
"step": "第 3 轮正向",
"added": [
"started",
"started"
],
"charged": 9,
"occupied": 9,
"balance": 4,
"forwardK": 2,
"ablationMax": 3
},
{
"step": "第 3 轮消融",
"added": [],
"charged": 9,
"occupied": 9,
"balance": 4,
"forwardK": 2,
"ablationMax": 3
},
{
"step": "第 4 轮正向",
"added": [
"started",
"started"
],
"charged": 11,
"occupied": 11,
"balance": 2,
"forwardK": 2,
"ablationMax": 1
},
{
"step": "第 5 轮正向",
"added": [
"started",
"started"
],
"charged": 13,
"occupied": 13,
"balance": 0,
"forwardK": 0,
"ablationMax": 0
}
]
},
"note": "每一步追加的运行记录状态,累计后算余额。余额序列 12,10,8,6,4,4,2,0。forwardK = min(每批上限, 余额);ablationMax = 余额−1(余额 ≤ 1 时 0)。"
},
{
"name": "balance_mixed_states",
"section": "第七部分;约定 35–36",
"params": {
"totalRuns": 6,
"batchCap": 2
},
"input": {
"runs": [
"started",
"reserved",
"notStarted",
"started",
"reserved"
]
},
"expected": {
"charged": 2,
"occupied": 4,
"balance": 2,
"forwardK": 2,
"ablationMax": 1
},
"note": "started 扣次;reserved(含状态未知)占着名额;notStarted 退还。已扣 2,占用 4,余额 2。"
},
{
"name": "balance_0",
"section": "第七部分;约定 36",
"params": {
"totalRuns": 3,
"batchCap": 2
},
"input": {
"runs": [
"started",
"started",
"started"
]
},
"expected": {
"charged": 3,
"occupied": 3,
"balance": 0,
"forwardK": 0,
"ablationMax": 0
},
"note": "余额 0、1、2 时的正向批量与消融上限。"
},
{
"name": "balance_1",
"section": "第七部分;约定 36",
"params": {
"totalRuns": 3,
"batchCap": 2
},
"input": {
"runs": [
"started",
"started"
]
},
"expected": {
"charged": 2,
"occupied": 2,
"balance": 1,
"forwardK": 1,
"ablationMax": 0
},
"note": "余额 0、1、2 时的正向批量与消融上限。"
},
{
"name": "balance_2",
"section": "第七部分;约定 36",
"params": {
"totalRuns": 3,
"batchCap": 2
},
"input": {
"runs": [
"started"
]
},
"expected": {
"charged": 1,
"occupied": 1,
"balance": 2,
"forwardK": 2,
"ablationMax": 1
},
"note": "余额 0、1、2 时的正向批量与消融上限。"
},
{
"name": "seed_study-a_r1",
"section": "约定 39–40",
"params": {},
"input": {
"studyId": "study-a",
"round": 1,
"version": 1,
"candidates": [
"c1",
"c2",
"c3",
"c4",
"c5"
],
"batchSize": 2,
"cells": [
"p1",
"p2",
"p3",
"p4"
],
"cellCount": 2,
"candidatesMany": [
"c1",
"c2",
"c3",
"c4",
"c5",
"c6",
"c7",
"c8",
"c9",
"c10",
"c11",
"c12"
],
"cellsMany": [
"p1",
"p2",
"p3",
"p4",
"p5",
"p6",
"p7",
"p8",
"p9",
"p10",
"p11",
"p12"
],
"manyCount": 4
},
"expected": {
"canonical": "{\"round\":1,\"studyId\":\"study-a\",\"version\":1}",
"seed": "39855ea9f581fcad6cb7710ac02151da468f2300ef72fefbbdabb3e191f62ff0",
"first6BytesHex": [
"d2d2a16d557a",
"c85c9f2d75ac",
"61392fbb6bbb"
],
"uniformInt10First5": [
2,
0,
7,
0,
2
],
"batchSample": [
"c2",
"c3"
],
"cellSample": [
"p3",
"p4"
],
"batchSampleMany": [
"c5",
"c7",
"c9",
"c10"
],
"cellSampleMany": [
"p5",
"p7",
"p9",
"p10"
]
},
"note": "种子 = sha256(规范 JSON {round, studyId, version})。uniformInt:sha256(`${seed}:${counter}`) 前 6 字节大端,counter 从 0 起;uniformInt10First5 是同一个生成器连续 5 次。batchSample、cellSample 各自用一个从 counter=0 开始的新生成器,部分 Fisher–Yates 后取前 size 个再排序。候选、因子编号都按自然序(c2 < c10、p2 < p10);*Many 两项用 12 个编号核对这一点。"
},
{
"name": "seed_15-real-1_r3",
"section": "约定 39–40",
"params": {},
"input": {
"studyId": "15-real-1",
"round": 3,
"version": 1,
"candidates": [
"c1",
"c2",
"c3",
"c4",
"c5"
],
"batchSize": 2,
"cells": [
"p1",
"p2",
"p3",
"p4"
],
"cellCount": 2,
"candidatesMany": [
"c1",
"c2",
"c3",
"c4",
"c5",
"c6",
"c7",
"c8",
"c9",
"c10",
"c11",
"c12"
],
"cellsMany": [
"p1",
"p2",
"p3",
"p4",
"p5",
"p6",
"p7",
"p8",
"p9",
"p10",
"p11",
"p12"
],
"manyCount": 4
},
"expected": {
"canonical": "{\"round\":3,\"studyId\":\"15-real-1\",\"version\":1}",
"seed": "cb9cc4dbd939af39de4852c42806f3471e90bd0e16427ced09087345586506ff",
"first6BytesHex": [
"f5b64e27d768",
"0b0216f8610e",
"b48a7659e2b5"
],
"uniformInt10First5": [
8,
4,
7,
7,
6
],
"batchSample": [
"c1",
"c4"
],
"cellSample": [
"p1",
"p3"
],
"batchSampleMany": [
"c6",
"c9",
"c10",
"c11"
],
"cellSampleMany": [
"p6",
"p9",
"p10",
"p11"
]
},
"note": "种子 = sha256(规范 JSON {round, studyId, version})。uniformInt:sha256(`${seed}:${counter}`) 前 6 字节大端,counter 从 0 起;uniformInt10First5 是同一个生成器连续 5 次。batchSample、cellSample 各自用一个从 counter=0 开始的新生成器,部分 Fisher–Yates 后取前 size 个再排序。候选、因子编号都按自然序(c2 < c10、p2 < p10);*Many 两项用 12 个编号核对这一点。"
}
]
}