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>
This commit is contained in:
2026-10-03 04:08:46 -04:00
parent cba1678eaf
commit 446e26224e
41 changed files with 3665 additions and 10 deletions
+185
View File
@@ -0,0 +1,185 @@
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 条"));
});