feat: 任务书 11,指标名加候选前缀、消融重做接着改、判重说明补关键设定

- 假设专用指标短名不能含"."(最长 64),冻结时由 materials 的 freezeProtocol 改为全名"候选编号.短名";可引用清单用全名,已占用清单只剩任务指标和两个保留键。
- 消融格核验不通过重做时,工作目录是上一次交的这一格代码的副本,能读的版本加上它,完整代码照旧列出;消融实现提示词同步改。
- jev 判重材料说明补 keySettings 一句。
- 结构.md 第八部分 vocab、materials、jev,WORK_LOG 同步更新。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 06:40:40 -04:00
parent f623fb78a8
commit 13a7e60355
20 changed files with 156 additions and 57 deletions
+2 -1
View File
@@ -231,7 +231,8 @@ test("材料:给定带类型的输入,发出去的材料与手写预期一
const states = sent.map((s) => s.body.state);
const [dedup, freeze, , , diff] = states;
for (const state of states) assert.equal(typeof state.task, "string");
// 判重放名称、做法和关键设定:关掉时换成什么、延续哪个因子不放。
// 判重放名称、做法和关键设定:关掉时换成什么、延续哪个因子不放。材料说明里也讲了关键设定是什么(任务书 11 第三部分)。
assert.match(dedup.task, /`keySettings`: the setting values that decide its effect/);
assert.deepEqual(dedup, {
task: dedup.task,
proposal_a: { factors: [{ name: "外圈环半径缩到 0.45", method: "把 construct_packing 里外圈环半径常数 0.7 改为 0.45。", keySettings: "环半径 0.45" }] },
+4 -4
View File
@@ -31,8 +31,8 @@ const adoptInput = (baseline: number, candidates: Array<{ id: string; score: num
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"]));
w.protocol("r1-c1", protocolOf("r1-c1", P_A1, 1, "base", ["r1-a1-1"], ["c"]));
w.protocol("r1-c2", protocolOf("r1-c2", 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);
@@ -124,7 +124,7 @@ test("信念本的数按条文手算:少多少分、撑几成、世界比特
};
// 这里只要 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.protocol("r1-v1-c3", protocolOf("r1-v1-c3", 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 }]);
@@ -195,7 +195,7 @@ test("解释的现在交什么写明每人最多 M 条", (t) => {
/** 第 1 轮冻结 r1-c1(两个因子)、选中、跑出 run1Judged、采纳。 */
function roundOneAdopted(t: import("node:test").TestContext, run1Judged: ReturnType<typeof judged>): Writer {
const w = fresh(t);
w.protocol("r1-c1", protocolOf(P_A1, 1, "base", ["r1-a1-1"], ["c"]));
w.protocol("r1-c1", protocolOf("r1-c1", P_A1, 1, "base", ["r1-a1-1"], ["c"]));
const b1 = batch(1, 0.96, { "r1-c1": { a1: r(1.0, 1.1, 1.2) } });
w.decision("batch", 1, b1.input, b1.output);
w.l.append("code", { object: "r1-c1", version: "v-c1", locations: TABLE_C1 });
+11 -7
View File
@@ -12,6 +12,7 @@ import {
} from "../src/rules/index.ts";
import {
buildMaterial,
freezeProtocol,
readPrompts,
type CodeChanges,
type FrozenProtocol,
@@ -24,6 +25,7 @@ import { join } from "node:path";
// materials 测试共用:在真实账本(临时文件)里一步步写入一段研究进程。
// 研究者 a1、a2,实验员 x1,核验员 v1。第 1 轮采纳 r1-c1(两个因子),消融测 p1、p2 没测;第 2 轮采纳 r2-c1。
// 第 1 轮两条提案都登记了短名 restart-count,冻结成 r1-c1.restart-count、r1-c2.restart-count;r2-c1 引用前一个。
// 好认的数和字(7.77x 作者自评、5.55x 甲的第一次预测等)用来检查"不该看到的没出现"。
export const study: StudyFacts = {
@@ -62,10 +64,11 @@ const P_A1: Submission<"proposal"> = {
authorEstimate: SEALED_A1,
body: "甲的提案正文",
};
// 乙和甲同一轮互相看不到提案,起了同一个短名 restart-count、定义却不同(参考项目真实账本出过 5 次,任务书 11 第一部分)。
const P_A2: Submission<"proposal"> = {
factors: [factor("收缩半径修复", null)],
safeguards: [],
metrics: { defined: [], reused: [] },
metrics: { defined: [{ id: "restart-count", meaning: "修复阶段重启了几次", unit: "次", direction: "minimize", howMeasured: "修复循环每重启一次加一", missing: "recordMissing" }], reused: [] },
authorEstimate: SEALED_A2,
body: "乙的提案正文",
};
@@ -121,8 +124,9 @@ export class Writer {
}
}
export function protocolOf(p: Submission<"proposal">, round: number, baseline: string, proposals: string[], clauses: string[]): FrozenProtocol {
return { factors: p.factors, safeguards: p.safeguards, metrics: p.metrics, body: p.body, clauses, round, baseline, proposals };
/** 候选 candidate 的冻结协议:照 flow 的做法,用 freezeProtocol 写(新登记的指标改全名)。 */
export function protocolOf(candidate: string, p: Submission<"proposal">, round: number, baseline: string, proposals: string[], clauses: string[]): FrozenProtocol {
return freezeProtocol({ factors: p.factors, safeguards: p.safeguards, metrics: p.metrics, body: p.body, clauses }, candidate, round, baseline, proposals);
}
export function batch(round: number, baseline: number, ranges: Record<string, Record<string, Range>>): { input: BatchInput; output: ReturnType<typeof selectBestExpected> } {
@@ -192,8 +196,8 @@ const STEPS: Array<[string, (w: Writer, s: Record<string, number>) => void]> = [
w.sub("s1-resp", "a1", "response", "r1-c1", null, { replies: [{ critique: CRITIQUE_ID, reply: "甲答复:保留扰动" }], changes: {}, clauses: ["加多起点", "加扰动"], body: "无" });
w.sub("s1-resp", "a2", "response", "r1-c2", null, { replies: [], changes: {}, clauses: ["加修复"], body: "无" });
w.close("s1-resp");
w.protocol("r1-c1", protocolOf(P_A1, 1, "base", ["r1-a1-1"], ["加多起点", "加扰动"]));
w.protocol("r1-c2", protocolOf(P_A2, 1, "base", ["r1-a2-1"], ["加修复"]));
w.protocol("r1-c1", protocolOf("r1-c1", P_A1, 1, "base", ["r1-a1-1"], ["加多起点", "加扰动"]));
w.protocol("r1-c2", protocolOf("r1-c2", P_A2, 1, "base", ["r1-a2-1"], ["加修复"]));
}],
["secondOpen", (w) => w.open("s1-sp", "secondPrediction", 1, ["a1", "a2"])],
["second", (w) => {
@@ -273,8 +277,8 @@ const STEPS: Array<[string, (w: Writer, s: Record<string, number>) => void]> = [
w.sub("s2-resp", "a1", "response", "r2-c1", null, { replies: [], changes: {}, clauses: ["加局部搜索"], body: "无" });
w.sub("s2-resp", "a2", "response", "r2-c2", null, { replies: [], changes: {}, clauses: ["改修复", "加早停"], body: "无" });
w.close("s2-resp");
w.protocol("r2-c1", protocolOf({ ...P_A1, metrics: { defined: [], reused: ["restart-count"] }, factors: [factor("新的局部搜索", "r1-c1-p1"), factor("更多起点", null)], body: "甲的第二轮提案" }, 2, "r1-c1", ["r2-a1-1"], ["加局部搜索"]));
w.protocol("r2-c2", protocolOf({ ...P_A2, factors: [factor("更细的修复", null), factor("早停", null)], body: "乙的第二轮提案" }, 2, "r1-c1", ["r2-a2-1"], ["改修复", "加早停"]));
w.protocol("r2-c1", protocolOf("r2-c1", { ...P_A1, metrics: { defined: [], reused: ["r1-c1.restart-count"] }, factors: [factor("新的局部搜索", "r1-c1-p1"), factor("更多起点", null)], body: "甲的第二轮提案" }, 2, "r1-c1", ["r2-a1-1"], ["加局部搜索"]));
w.protocol("r2-c2", protocolOf("r2-c2", { ...P_A2, factors: [factor("更细的修复", null), factor("早停", null)], body: "乙的第二轮提案" }, 2, "r1-c1", ["r2-a2-1"], ["改修复", "加早停"]));
w.open("s2-sp", "secondPrediction", 2, ["a1", "a2"]);
const ranges = { "r2-c1": { a1: r(1.2, 1.24, 1.3), a2: r(1.19, 1.22, 1.27) }, "r2-c2": { a1: r(1.18, 1.2, 1.22), a2: r(1.17, 1.2, 1.22) } };
for (const [c, rs] of Object.entries(ranges)) for (const [a, range] of Object.entries(rs)) w.sub("s2-sp", a, "secondPrediction", c, null, { range, body: "理由" });
+39 -4
View File
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { test } from "node:test";
import { beliefBook, queryMaterial, readPrompts, type TaskMaterial } from "../src/materials/index.ts";
import { beliefBook, freezeProtocol, queryMaterial, readPrompts, type TaskMaterial } from "../src/materials/index.ts";
import { SUBMISSION_KINDS, type SubmissionKind } from "../src/vocab/index.ts";
import { tempDir } from "./ledger-helpers.ts";
import { CHANGES_C1, CRITIQUE_ID, everything, material, NONE, study, studyUpTo, TABLE_C1 } from "./materials-helpers.ts";
@@ -81,8 +81,8 @@ test("第 2 轮提案:上一轮全部收尾、上一轮实验员的提醒、
maxProposals: 2,
factorLimit: 3,
continuable: ["r1-c1-p1", "r1-c1-p2", "r1-c2-p1"],
reusableMetrics: ["restart-count"],
takenMetricIds: ["sum-of-radii", "wall-seconds", "answer-path", "safeguard-triggers", "restart-count"],
reusableMetrics: ["r1-c1.restart-count", "r1-c2.restart-count"],
takenMetricIds: ["sum-of-radii", "wall-seconds", "answer-path", "safeguard-triggers"],
});
for (const x of ["上一轮的收尾", "上一轮实验员实现时的提醒", '可以写进"延续"的因子', "已登记指标清单", "研究记录", "信念本"]) {
assert.ok(headings(m).includes(x), `应有一节"${x}"`);
@@ -157,6 +157,31 @@ test("新登记指标不能用的编号里有答案路径、保障动手次数
assert.ok(c.takenMetricIds.includes("answer-path") && c.takenMetricIds.includes("safeguard-triggers"), JSON.stringify(c.takenMetricIds));
});
test("冻结时新登记的指标改全名:同一轮两个候选登记同一个短名、定义不同,冻结后是两个不同的全名", () => {
const body = (meaning: string) => ({
factors: [{ name: "因子", method: "做法", keySettings: "无", continues: null }],
safeguards: [],
metrics: { defined: [{ id: "restart-gain", meaning, unit: "无", direction: "maximize" as const, howMeasured: "测一次", missing: "recordMissing" as const }], reused: ["r1-c1.min-gap"] },
body: "正文",
clauses: ["条款"],
});
const a = freezeProtocol(body("重启段内最好 Σr 减进入重启段时的 Σr"), "r2-c1", 2, "r1-c1", ["r2-a1-1"]);
const b = freezeProtocol(body("最终 Σr 减池阶段最好 Σr"), "r2-c2", 2, "r1-c1", ["r2-a2-1", "r2-a3-1"]);
assert.deepEqual(a, { ...body("重启段内最好 Σr 减进入重启段时的 Σr"), metrics: { defined: [{ ...body("").metrics.defined[0]!, id: "r2-c1.restart-gain", meaning: "重启段内最好 Σr 减进入重启段时的 Σr" }], reused: ["r1-c1.min-gap"] }, round: 2, baseline: "r1-c1", proposals: ["r2-a1-1"] });
assert.deepEqual([b.metrics.defined[0]!.id, b.metrics.defined[0]!.meaning, b.proposals], ["r2-c2.restart-gain", "最终 Σr 减池阶段最好 Σr", ["r2-a2-1", "r2-a3-1"]]);
});
test("下一轮:同短名的两个全名都能引用,各自对应自己的定义;引用的指标在实验员材料里是原登记的定义", (t) => {
const m = material(studyUpTo(t, "proposal2Open"), "proposal", "a2", "s2-prop", null, NONE);
const list = m.sections.find((s) => s.heading === "已登记指标清单")!.text;
assert.match(list, /- r1-c1\.restart-count(候选 r1-c1 登记,越大越好,单位 次,测不到时只记缺失):重启次数;/);
assert.match(list, /- r1-c2\.restart-count(候选 r1-c2 登记,越小越好,单位 次,测不到时只记缺失):修复阶段重启了几次;/);
const impl = material(studyUpTo(t, "implOpen2"), "implementation", "x1", "s2-impl-c1", "r2-c1", NONE);
const defs = impl.sections.find((s) => s.heading === "要实现的指标定义")!.text;
assert.ok(defs.includes('"id": "r1-c1.restart-count"') && defs.includes("重启次数"), defs);
assert.ok(!defs.includes("修复阶段重启了几次"), defs);
});
test("解释:本轮运行、提醒、要给三个数的格、产物只进查询;看不到别人的第一次预测", (t) => {
const l = studyUpTo(t, "explOpen1");
const m = material(l, "explanation", "a2", "s1-expl", null, { changes: null, artifacts: { "run-1": { "stdout.log": "日志正文" } } });
@@ -191,7 +216,8 @@ test("实现:冻结协议、指标定义、要交的位置表、起点代码
assert.deepEqual(m.readable.map((r) => r.version), ["v-base"]);
for (const x of ["任务与本轮", "基线代码", "本任务要处理的东西", "要实现的指标定义", "要交的因子位置表"]) assert.ok(headings(m).includes(x));
assert.ok(!headings(m).includes("信念本"));
has(m, "restart-count");
has(m, '"id": "r1-c1.restart-count"');
has(m, "键照抄这里的 id(全名");
lacks(m, "乙的提案正文");
lacks(m, "收缩半径修复");
lacks(m, "6.661");
@@ -269,6 +295,14 @@ test("消融实现重做:材料放上一次消融核验关错的因子和依
has(m, "- 因子 p1 应当关着,核验员看到的是照留:solve.py:12 仍在跑多起点循环");
lacks(m, "solve.py:30 照留");
assert.deepEqual(m.checklist, { kind: "ablationImplementation", cell: "r1-c1-p1", off: "p1", factors: ["p1", "p2"] });
// 重做从上一次交的这一格代码接着改(用户 2026-10-03 定):工作目录是它的副本,能读的版本里有它,也有完整代码作对照。
assert.deepEqual(m.readable.map((r) => [r.version, r.label]), [
["v-c1", "完整代码"],
["v-base", "本批基线(因子没写关掉时换成什么,就回到它在这件事上的做法)"],
["v-c1-p1", "你上一次交的这一格代码"],
]);
has(m, "你的工作目录是上一次交的这一格代码的副本,从它接着改");
lacks(m, "你的工作目录是完整代码的副本");
});
test("消融实现:这一格、因子清单、完整代码的位置表;看不到运行成绩", (t) => {
@@ -277,6 +311,7 @@ test("消融实现:这一格、因子清单、完整代码的位置表;看
assert.deepEqual(m.checklist, { kind: "ablationImplementation", cell: "r1-c1-p1", off: "p1", factors: ["p1", "p2"] });
assert.ok(headings(m).includes("完整代码(消融的起点)"));
assert.deepEqual(m.readable.map((r) => r.version), ["v-c1", "v-base"]);
has(m, "你的工作目录是完整代码的副本");
lacks(m, "1.2");
lacks(m, "甲的解释");
});
+13 -5
View File
@@ -20,12 +20,13 @@ const r = (q1: number, q5: number, q9: number) => ({ q1, q5, q9 });
const factor = (name: string, continues: string | null) => ({ name, method: `${name}的做法`, keySettings: "无", continues });
const metric = (id: string) => ({ id, meaning: "次数", unit: "次", direction: "maximize" as const, howMeasured: "数", missing: "recordMissing" as const });
const RULES = { factorLimit: 2, continuable: ["r1-c1-p1", "r1-c1-p2"], reusableMetrics: ["restart-count"], takenMetricIds: ["sum-of-radii", "wall-seconds", "answer-path", "safeguard-triggers", "restart-count"] };
// 能引用的是冻结后的全名"候选编号.短名";已占用的只有任务的指标和两个保留键(任务书 11 第一部分)。
const RULES = { factorLimit: 2, continuable: ["r1-c1-p1", "r1-c1-p2"], reusableMetrics: ["r1-c1.restart-count", "r1-c2.restart-count"], takenMetricIds: ["sum-of-radii", "wall-seconds", "answer-path", "safeguard-triggers"] };
const PROPOSAL: Submission<"proposal"> = {
factors: [factor("多起点", null), factor("扰动", "r1-c1-p2")],
safeguards: ["guard"],
metrics: { defined: [metric("kick-count")], reused: ["restart-count"] },
metrics: { defined: [metric("kick-count")], reused: ["r1-c1.restart-count"] },
authorEstimate: r(1.1, 1.2, 1.3),
body: "提案正文",
};
@@ -188,13 +189,20 @@ test("规则 2、3、4、10 对统一自己的字段查:因子超上限、保
});
test("规则 10:reused 要在可引用清单里;新登记的编号不重复,也不能和已占用的重名(含答案路径两个保留键)", async () => {
assert.match(await rejected(proposal({ ...PROPOSAL, metrics: { defined: [], reused: ["kick-count"] } }, 0)), /metrics.reused 只能写 restart-count;你写的 kick-count 不在里面/);
assert.match(await rejected(proposal({ ...PROPOSAL, metrics: { defined: [], reused: ["kick-count"] } }, 0)), /metrics.reused 只能写 r1-c1\.restart-count、r1-c2\.restart-count;你写的 kick-count 不在里面/);
const reason = await rejected(proposal({ ...PROPOSAL, metrics: { defined: [metric("a"), metric("a"), metric("wall-seconds"), metric("answer-path")], reused: [] } }, 0));
assert.match(reason, /metrics.defined 里编号 a 写了不止一次/);
assert.match(reason, /metrics.defined 的编号 wall-seconds 已被占用/);
assert.match(reason, /metrics.defined 的编号 answer-path 已被占用/);
});
test("规则 10:引用写短名而不是全名时退回,原因里列出能引用的全名;新登记的短名和已登记的全名同名不算占用", async () => {
const reason = await rejected(proposal({ ...PROPOSAL, metrics: { defined: [], reused: ["restart-count"] } }, 0));
assert.equal(reason, "这份提交没有收下,要改的地方:\n- metrics.reused 只能写 r1-c1.restart-count、r1-c2.restart-count;你写的 restart-count 不在里面");
const v = await receive(proposal({ ...PROPOSAL, metrics: { defined: [metric("restart-count")], reused: ["r1-c1.restart-count"] } }, 0));
assert.equal(v.accepted, true);
});
test("提案条数:已收下 M 条再交就退回", async () => {
assert.match(await rejected(proposal(PROPOSAL, 2)), /本阶段你已经收下 2 条提案,每人最多 2 条/);
});
@@ -301,7 +309,7 @@ test("回应拼协议:changes 只改保障时,因子、指标、正文取原
assert.deepEqual(p, {
factors: [factor("多起点", null), factor("扰动", "r1-c1-p2")],
safeguards: ["clip"],
metrics: { defined: [metric("kick-count")], reused: ["restart-count"] },
metrics: { defined: [metric("kick-count")], reused: ["r1-c1.restart-count"] },
body: "提案正文",
clauses: ["条款一"],
});
@@ -402,7 +410,7 @@ test("回应全判否:收下,交回拼好的协议和三问的判断", async
assert.deepEqual(v.accepted && v.protocol, {
factors: [factor("多起点", null), factor("扰动", "r1-c1-p2")],
safeguards: ["guard"],
metrics: { defined: [metric("kick-count")], reused: ["restart-count"] },
metrics: { defined: [metric("kick-count")], reused: ["r1-c1.restart-count"] },
body: "新正文",
clauses: ["条款一"],
});
+10
View File
@@ -344,6 +344,16 @@ test("判定表 5:缺了整次作废的假设专用指标测不到,不算数
assert.equal(value(voided.score), 0.203125);
});
test("假设专用指标用冻结后的全名\"候选编号.短名\"当键,读得到值(任务书 11 第一部分)", async (t) => {
const full: HypothesisMetric = { id: "r2-c1.restart-gain", meaning: "测试用", unit: "无", direction: "maximize", howMeasured: "测试用", missing: "invalidateRun" };
const runDir = runDirWith(t, loose());
writeFileSync(join(runDir, "metrics.json"), JSON.stringify({ "r2-c1.restart-gain": 0.125, "restart-gain": 9, "answer-path": "main", "safeguard-triggers": {} }));
const result = await judgeRun(t, "fullname", runDir, EXITED_OK, [full]);
assert.equal(result.verdict, "valid");
assert.deepEqual(Object.keys(result.metrics).filter((k) => k.includes("restart")), ["r2-c1.restart-gain"]);
assert.equal(value(result.metrics["r2-c1.restart-gain"]), 0.125);
});
test("答案路径、保障动手次数:读不到时记读不到及原因,不作废运行", async (t) => {
const missing = await judgeRun(t, "nometrics", runDirWith(t, loose()), EXITED_OK, []);
assert.equal(missing.verdict, "valid");
+10
View File
@@ -172,6 +172,16 @@ test("关掉消融时,不带 cells 的解释通过字段定义检查", () => {
assert.equal(Value.Check(SUBMISSION_SCHEMAS.explanation, submission), true);
});
test("假设专用指标的短名不能含\".\":带点的新登记不收;引用写全名照收(任务书 11 第一部分)", () => {
const proposal = (defined: string, reused: string[]) => ({ ...samples.proposal, metrics: { defined: [{ ...metrics.defined[0]!, id: defined }], reused } });
assert.equal(Value.Check(SUBMISSION_SCHEMAS.proposal, proposal("restart-gain", ["r1-c1.restart-gain"])), true);
assert.equal(Value.Check(SUBMISSION_SCHEMAS.proposal, proposal("r1-c1.restart-gain", [])), false);
assert.equal(Value.Check(SUBMISSION_SCHEMAS.proposal, proposal("restart.gain", [])), false);
// 短名最长 64 个字符,加上候选编号的全名仍在编号的 128 个字符以内。
assert.equal(Value.Check(SUBMISSION_SCHEMAS.proposal, proposal("g".repeat(64), [])), true);
assert.equal(Value.Check(SUBMISSION_SCHEMAS.proposal, proposal("g".repeat(65), [])), false);
});
// 复现:TypeBox 的 Record 对带格式的键只生成 patternProperties,不合格式的键原先完全不检查。
test("环境变量和指标取值里,不合格式的键被拒收", () => {
const run = {