Files
InfoRSI/tests/materials-belief.test.ts
T
iomgaa 446e26224e feat: 任务书 09,materials 模块与提示词
- src/materials:每种提交一个材料函数,生成给模型看的材料、第一条消息、对号清单、能读的代码版本;查询只在材料内按编号找;读提示词并算原始字节哈希;信念本按方法.md 6.10 现算。
- 账本里只存成 Json 的冻结协议、位置表、决定、结算的形状和编号规则由 materials 定义并导出。
- vocab 加 Checklist(对号清单)。
- prompts/:参考项目 3 份角色提示词、16 份任务说明,只做必要改动。
- TASK.md 补一句 unversionedNames 不存进代码版本。
- 方法.md 4.3 ① 改为"研究者任务"放信念本;结构.md vocab 上限 1150、补 materials 对外内容。

用户 2026-10-03 开工前的四个决定(选的都是推荐项):"甲:上限改 1150,只放对号清单";"甲:materials 定义并导出";"不放,改方法.md ① 的措辞";"按原提案挑"。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 04:08:46 -04:00

186 lines
10 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 { 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<string, Array<{ researcher: string; range: { q1: number; q5: number; q9: number } }>>).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 条"));
});