import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { test } from "node:test"; import { Ledger } from "../src/ledger/index.ts"; import { beliefBook, queryMaterial } from "../src/materials/index.ts"; import { adopt, selectBestExpected, settleAblation, type BatchInput, type SettleInput } from "../src/rules/index.ts"; import { tempDir } from "./ledger-helpers.ts"; import { batch, judged, material, NONE, P_A1, P_A2, protocolOf, r, study, studyUpTo, TABLE_C1, Writer } from "./materials-helpers.ts"; // 信念本的留池、离池,按条文手算的数字,第零轮记录在第 1 轮起公开。 function fresh(t: import("node:test").TestContext): Writer { const l = Ledger.open(join(tempDir(t), "ledger.db")); t.after(() => l.close()); const w = new Writer(l); w.l.append("code", { object: "base", version: "v-base", locations: null }); w.run("run-0", "baseline", 0, null, judged(0.96, "main")); return w; } const adoptInput = (baseline: number, candidates: Array<{ id: string; score: number }>) => ({ baseline, knownBest: 2.635983, epsilon: 0.01, direction: "maximize" as const, candidates, }); /** 第 1 轮冻结 r1-c1、r1-c2,选中 r1-c1、跑了没采纳:r1-c2 留到第 2 轮的候选池。 */ function roundOneNoAdoption(t: import("node:test").TestContext, before: (w: Writer) => void): Writer { const w = fresh(t); before(w); w.protocol("r1-c1", protocolOf(P_A1, 1, "base", ["r1-a1-1"], [], ["c"])); w.protocol("r1-c2", protocolOf(P_A2, 1, "base", ["r1-a2-1"], [], ["c"])); const b1 = batch(1, 0.96, { "r1-c1": { a1: r(1.0, 1.1, 1.2) }, "r1-c2": { a1: r(0.95, 0.97, 0.99) } }); assert.deepEqual(b1.output.chosen, ["r1-c1"]); w.decision("batch", 1, b1.input, b1.output); w.l.append("code", { object: "r1-c1", version: "v-c1", locations: TABLE_C1 }); w.run("run-1", "forward", 1, "r1-c1", judged(0.965, "main")); const a1 = adoptInput(0.96, [{ id: "r1-c1", score: 0.965 }]); w.decision("adopt", 1, a1, adopt(a1)); return w; } test("留池:第 1 轮没选中、第 2 轮入选跑了没采纳的候选,第 3 轮不再算留池(复现审阅意见 1)", (t) => { const w = roundOneNoAdoption(t, () => undefined); w.open("s2-prop", "proposal", 2, ["a1"]); // 第 2 轮提案阶段(还没有本轮判重合并):r1-c2 没选中、基线没变,留在候选池。 assert.ok(beliefBook(w.l, study, 2).includes("r1-c2:没选中,留在本轮候选池;之前的合并范围不附")); const b2 = batch(2, 0.96, { "r1-c2": { a1: r(0.95, 0.97, 0.99) } }); w.decision("batch", 2, b2.input, b2.output); w.l.append("code", { object: "r1-c2", version: "v-c2", locations: { factorLocations: [{ factor: "p1", locations: ["x.py"] }], safeguardLocations: [], unassigned: [] } }); w.run("run-2", "forward", 2, "r1-c2", judged(0.962, "main")); const a2 = adoptInput(0.96, [{ id: "r1-c2", score: 0.962 }]); w.decision("adopt", 2, a2, adopt(a2)); w.open("s3-prop", "proposal", 3, ["a1"]); const book = beliefBook(w.l, study, 3); assert.ok(!book.includes("留在本轮候选池"), "r1-c2 第 2 轮已经入选,不再留池"); assert.match(book, /- 第 1 轮选批[^\n]*\n(?:[^\n]*\n)*? - r1-c2:合并范围 /); assert.ok(book.includes("离开候选池的旧候选**:没有")); }); test("导出时(round 为 null)不藏任何候选的合并范围(复现复审意见 1)", (t) => { const w = roundOneNoAdoption(t, () => undefined); w.open("s2-prop", "proposal", 2, ["a1"]); const book = beliefBook(w.l, study, null); assert.ok(!book.includes("留在本轮候选池")); assert.match(book, / - r1-c2:合并范围 /); }); test("第二次预测:留池候选按 carried 判,不取它上一轮的第一次预测(复现复审意见 2)", (t) => { const w = roundOneNoAdoption(t, (w) => { w.open("s1-prop", "proposal", 1, ["a1"]); w.sub("s1-prop", "a1", "proposal", null, "r1-a1-1", P_A1); w.sub("s1-prop", "a1", "proposal", null, "r1-a1-2", P_A2); w.close("s1-prop"); w.decision("dedup", 1, null, { fresh: [{ candidate: "r1-c1", proposals: ["r1-a1-1"] }, { candidate: "r1-c2", proposals: ["r1-a1-2"] }], carried: [] }); w.open("s1-fp", "firstPrediction", 1, ["a1"]); w.sub("s1-fp", "a1", "firstPrediction", "r1-c2", null, { range: r(4.441, 4.442, 4.443), body: "上一轮的第一次" }); w.close("s1-fp"); }); w.open("s2-prop", "proposal", 2, ["a1"]); w.close("s2-prop"); w.decision("dedup", 2, null, { fresh: [], carried: [{ candidate: "r1-c2", proposals: [] }] }); w.open("s2-sp", "secondPrediction", 2, ["a1"]); const m = material(w.l, "secondPrediction", "a1", "s2-sp", "r1-c2", NONE); assert.ok(m.firstMessage.includes("(这是留池候选,本轮没有第一次预测)")); assert.ok(!m.firstMessage.includes("4.442")); }); test("离池:基线变了、从没入选的旧候选列在离开候选池里,不再写留池", (t) => { const book = beliefBook(studyUpTo(t, "proposal2Open"), study, 2); assert.ok(book.includes("离开候选池的旧候选**:r1-c2")); assert.ok(!book.includes("留在本轮候选池")); }); test("信念本的数按条文手算:少多少分、撑几成、世界比特、惊讶、战绩次数、标签", (t) => { // 正向的三个候选、各人范围、本批基线 b 和 c3 的实测取自 tests/fixtures/rules/info.json 的 bits_forward_r1_real, // 世界比特、惊讶的预期值是那里不看实现算出的独立值。 const fx = JSON.parse(readFileSync(join(import.meta.dirname, "fixtures", "rules", "info.json"), "utf8")).scenarios[0]; const b: number = fx.input.b; const full: number = fx.input.actual["r1-v1-c3"]; const w = fresh(t); const input: BatchInput = { candidates: Object.entries(fx.input.candidates as Record>).map( ([id, ranges]) => ({ id, ranges }), ), baseline: b, direction: "maximize", epsilon: 0.01, lambda: 0.1, batchCap: 1, balance: 9, seed: "seed", }; // 这里只要 c3 入选并跑过:选批记录的其余内容照规则算出,chosen 写成 c3。 w.decision("batch", 1, input, { ...selectBestExpected(input), chosen: ["r1-v1-c3"] }); w.protocol("r1-v1-c3", protocolOf(P_A1, 1, "base", ["r1-a1-1"], [], ["c"])); w.l.append("code", { object: "r1-v1-c3", version: "v-c3", locations: TABLE_C1 }); w.run("run-1", "forward", 1, "r1-v1-c3", judged(full, "main")); const a = adoptInput(b, [{ id: "r1-v1-c3", score: full }]); w.decision("adopt", 1, a, adopt(a)); w.run("run-2", "ablation", 1, "r1-v1-c3-p1", judged(1.3, "main")); w.run("run-3", "ablation", 1, "r1-v1-c3-p2", judged(1.45, "main")); const settle: SettleInput = { baseline: b, full, direction: "maximize", epsilon: 0.01, knownBest: 2.635983, lambda: 0.1, factors: ["p1", "p2"], cells: [ { factor: "p1", outcome: { kind: "valid", score: 1.3 } }, { factor: "p2", outcome: { kind: "valid", score: 1.45 } }, ], explanations: [ { id: "r1-researcher-1-x1", author: "researcher-1", cells: [{ factor: "p1", range: r(1.25, 1.35, 1.45) }, { factor: "p2", range: r(1.5, 1.55, 1.6) }] }, { id: "r1-researcher-2-x1", author: "researcher-2", cells: [{ factor: "p1", range: r(1.1, 1.2, 1.28) }, { factor: "p2", range: r(1.4, 1.44, 1.5) }] }, ], }; w.l.append("settlement", { round: 1, result: { candidate: "r1-v1-c3", input: settle, output: settleAblation(settle) } as never }); w.open("s2-prop", "proposal", 2, ["researcher-1"]); const book = beliefBook(w.l, study, 2); // 世界比特 13.018692667257758、惊讶 18.609640474436972(fixture),写 4 位有效数字。 assert.ok(book.includes("世界比特 13.02,惊讶 18.61 比特"), "正向 c3 的世界比特与惊讶"); // G = 1.552176048093269 − 0.9597642169962064 = 0.5924118311;关 p1:少 0.252176,撑 0.4257 → 43%;关 p2:少 0.102176,撑 0.1725 → 17%。 assert.ok(book.includes("关掉后 1.3,少 0.252176 分,撑 43%")); assert.ok(book.includes("关掉后 1.45,少 0.102176 分,撑 17%")); // Σ撑 = 0.598,相对交互量 −0.402 < −0.2:替代(方法.md 6.6)。 assert.ok(book.includes("替代:几个因子在做同一件事")); // 正向:三人对 c3 的一成都在 2.1 以上,实测 1.552 低于一成:中位数偏高、穿过下沿,没包住。 for (const who of ["researcher-1", "researcher-2", "researcher-3"]) { assert.ok(book.includes(`- ${who},正向 1 次:中位数偏高 1、偏低 0;穿过上沿 0、下沿 1;包住 0`), who); } // 消融:researcher-1 p1 1.3 落在 1.25–1.35(包住,中位数偏高)、p2 1.45 < 1.5(穿下沿,偏高); // researcher-2 p1 1.3 > 1.28(穿上沿,偏低)、p2 1.45 在 1.44–1.5(包住,偏低)。 assert.ok(book.includes("- researcher-1,消融 2 次:中位数偏高 2、偏低 0;穿过上沿 0、下沿 1;包住 1")); assert.ok(book.includes("- researcher-2,消融 2 次:中位数偏高 0、偏低 2;穿过上沿 1、下沿 0;包住 1")); }); test("第零轮的入口、讨论、志愿、分配在第 1 轮起的研究记录里查得到(方法.md 4.2)", (t) => { const m = material(studyUpTo(t, "proposalOpen"), "proposal", "a2", "s1-prop", null, NONE); assert.ok(queryMaterial(m, "e-a1-1")!.includes("已被整份替换,作废")); assert.ok(queryMaterial(m, "e-a2-1")!.includes("乙一起点")); assert.ok(queryMaterial(m, "s0-d1.a1")!.includes("甲的讨论")); assert.ok(queryMaterial(m, "s0-claim.a2")!.includes("e-a2-1")); assert.ok(queryMaterial(m, "entry-assignment")!.includes("e-a1-3")); }); test("第 2 轮解释的研究记录里查得到第 1 轮和第零轮的已公开记录", (t) => { const m = material(studyUpTo(t, "explOpen2"), "explanation", "a1", "s2-expl", null, { changes: null, artifacts: {} }); const list = m.sections.find((s) => s.heading === "研究记录")!.text; const ids = ["run-1", "run-2", "run-3", "r1-c1", "r1-a1-1", "r1-a2-1", "r1-a1-1.a2.1", "r1-a1-x1", "r1-a2-x1", "s1-wrap.a1", "s0-d1.a1", "s0-claim.a2", "entry-assignment"]; for (const id of ids) { assert.ok(list.includes(`- ${id}:`), `研究记录清单里应有 ${id}`); assert.notEqual(queryMaterial(m, id), null, id); } }); test("解释的现在交什么写明每人最多 M 条", (t) => { const m = material(studyUpTo(t, "explOpen1"), "explanation", "a1", "s1-expl", null, { changes: null, artifacts: {} }); assert.ok(m.firstMessage.slice(m.firstMessage.lastIndexOf("# 现在交什么")).includes("最多 2 条")); });