feat: 诊断(任务书 18 第一块):诊断数学、四种提交、一次运行跑多个实验、账本与材料、flow 诊断各步

消融默认关,关掉时解释阶段一并跳过;选诊断实验用信息价值减成本。
eval/diagnosis/research.json 为第三块配置。vocab 字段说明的出口文字随提示词一起改,见下一次提交的 prompts/ 与同文件改动(本次含在内)。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 10:36:20 -04:00
parent f39f8410e7
commit 852600df02
69 changed files with 6607 additions and 214 deletions
+185
View File
@@ -0,0 +1,185 @@
import assert from "node:assert/strict";
import { appendFileSync, cpSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { test, type TestContext } from "node:test";
import { stepList } from "../src/config/index.ts";
import { fullConfig as checkedFull } from "../src/flow/frozen.ts";
import { runPaths, workspaceRepo } from "../src/flow/paths.ts";
import { groupSameExperiment, selectDiagnosisExperiments } from "../src/flow/steps/diagnosis.ts";
import { prepareSubmission, taskArtifact, taskCode } from "../src/flow/task-code.ts";
import type { Study } from "../src/flow/study.ts";
import { Ledger } from "../src/ledger/index.ts";
import { runDiagnosisRule, type DiagnosisRuleInput } from "../src/rules/index.ts";
import type { ForwardCode } from "../src/submissions/index.ts";
import { checkTaskFacts, diagnosisUnversionedNames, loadTask, placeBaselineArtifact } from "../src/tasks/index.ts";
import type { JevJudgment } from "../src/vocab/index.ts";
import { compareVersions, createRepository, saveVersion } from "../src/workspace/index.ts";
import { fullConfig } from "./config-helpers.ts";
import { tempDir } from "./ledger-helpers.ts";
import { judged, r, Writer } from "./materials-helpers.ts";
// 诊断里程序自己做的两步(任务书 18 第 1.6 节):判重合并的分组、选诊断实验;加上诊断实现、修复、核验的取材和存版本接线。判重合并的 Jev 问法由 jev 的测试管;
// 诊断运行和结算要真实 Docker,在 flow-diagnosis-docker.test.ts。预期值照条文手拼,输出用 rules 的函数当对照
// (同选格测试的写法,rules 自己有独立预期值的测试)。
const facts = checkTaskFacts(JSON.parse(readFileSync(join(import.meta.dirname, "..", "tasks", "circle_packing", "task.json"), "utf8")));
const yes = (probability: number): JevJudgment => ({ question: "diagnosisDedup", probability, threshold: 0.3, yes: probability > 0.3 }) as unknown as JevJudgment;
const item = (author: string, index: number) => ({ author, index, label: `${author}.e${index + 1}` });
test("诊断实验判重合并:判为相同的连成一组,编号 d1… 按组里排最前的实验排,组内按作者、第几个排", () => {
const items = [item("researcher-1", 0), item("researcher-1", 1), item("researcher-2", 0), item("researcher-3", 0)];
const pairs = [
{ a: "researcher-1.e1", b: "researcher-2.e1", judgment: yes(0.9) },
{ a: "researcher-1.e2", b: "researcher-3.e1", judgment: yes(0.1) },
{ a: "researcher-2.e1", b: "researcher-3.e1", judgment: yes(0.8) },
];
// researcher-1.e1、researcher-2.e1、researcher-3.e1 连成一组(A=B,B=C);researcher-1.e2 自成一个。
assert.deepEqual(groupSameExperiment(items, pairs), {
experiments: [
{ experiment: "d1", sources: [{ author: "researcher-1", index: 0 }, { author: "researcher-2", index: 0 }, { author: "researcher-3", index: 0 }] },
{ experiment: "d2", sources: [{ author: "researcher-1", index: 1 }] },
],
});
});
test("诊断实验判重合并:没有一对判相同时,每个实验各成一个", () => {
const items = [item("researcher-1", 0), item("researcher-2", 0)];
const pairs = [{ a: "researcher-1.e1", b: "researcher-2.e1", judgment: yes(0.1) }];
assert.deepEqual(groupSameExperiment(items, pairs).experiments.map((e) => e.experiment), ["d1", "d2"]);
});
/** 基线 0.96;a1、a2 各一条诊断解释,对合并后的 d1、d2 各给三个数;余额:13 次里基线用了 1 次。 */
function study(t: TestContext, ranges: Record<string, ReturnType<typeof r>[]>): { s: Study; input: DiagnosisRuleInput } {
const ledger = Ledger.open(join(tempDir(t), "ledger.db"));
t.after(() => ledger.close());
const w = new Writer(ledger);
ledger.append("code", { object: "base", version: "v-base", locations: null });
w.run("base-1", "baseline", 0, null, judged(0.96, "main"));
w.open("r1-diagnosisProposal", "diagnosisProposal", 1, ["a1", "a2"]);
const experiment = (name: string) => ({ name, spot: `${name}的位置`, body: `${name}的做法`, metrics: [] });
for (const [author, name] of [["a1", "实验甲"], ["a2", "实验乙"]] as const) {
w.sub("r1-diagnosisProposal", author, "diagnosisProposal", null, null, { explanations: [{ body: `${author}的解释` }], experiments: [experiment(name)] });
}
w.close("r1-diagnosisProposal");
w.decision("diagnosisDedup", 1, null, {
experiments: [{ experiment: "d1", sources: [{ author: "a1", index: 0 }] }, { experiment: "d2", sources: [{ author: "a2", index: 0 }] }],
});
w.open("r1-diagnosisPrediction", "diagnosisPrediction", 1, ["a1", "a2"]);
for (const author of ["a1", "a2"]) {
const mine = ranges[author]!;
w.sub("r1-diagnosisPrediction", author, "diagnosisPrediction", null, null, {
predictions: ["d1", "d2"].map((d, i) => ({ explanation: `r1-${author}-dx1`, experiment: d, range: mine[i]! })),
body: "理由",
});
}
w.close("r1-diagnosisPrediction");
const config = fullConfig();
if (config.flow.preset !== "full") throw new Error("测试配置应是完整流程");
config.flow = { ...config.flow, diagnosis: { enabled: true, perResearcher: 2, maxExperiments: 4 }, ablation: { enabled: false } };
const frozen = { config: checkedFull(config), steps: stepList(config), facts, prompts: null, image: "", seed: 1 };
const input: DiagnosisRuleInput = {
baseline: 0.96,
direction: "maximize",
lambda: 0.1,
experiments: ["d1", "d2"],
explanations: ["a1", "a2"].map((author) => ({
id: `r1-${author}-dx1`,
author,
experiments: ["d1", "d2"].map((experiment, i) => ({ experiment, range: ranges[author]![i]! })),
})),
balance: 12,
batchCap: 2,
maxExperiments: 4,
u: 0.5,
decay: 0.5,
};
return { s: { ledger, frozen } as unknown as Study, input };
}
test("选诊断实验:输入照 DiagnosisRuleInput 拼(基线、每条解释对每个实验的三个数、余额从本轮之前的运行算),规则取步骤清单,种子 null", (t) => {
// 两人对 d1 预测相反(一个说涨到 1.3,一个说跌到 0.9),对 d2 都说涨不了多少。
const { s, input } = study(t, {
a1: [r(1.2, 1.3, 1.4), r(0.95, 0.97, 0.99)],
a2: [r(0.8, 0.9, 1.0), r(0.95, 0.97, 0.99)],
});
selectDiagnosisExperiments(s, 1);
const record = s.ledger.records().find((x) => x.kind === "decision" && x.content.decision === "selectDiagnosis")!;
const { rule: used, ...output } = runDiagnosisRule("valueMinusCost", input);
assert.deepEqual(record.content, { decision: "selectDiagnosis", round: 1, rule: "valueMinusCost", ruleVersion: used.version, params: output.params, seed: null, input, output });
// 分歧大的实验信息价值为正:本轮做诊断,d1 在选中的组合里。
assert.equal(output.diagnose, true);
assert.ok(output.chosen.includes("d1"));
});
/** 诊断实现的代码接线:真实裸仓库里存了基线;工作目录是基线副本加一处改动,再放进只读的原始产物。 */
function codeStudy(t: TestContext): { s: Study; work: string; baseline: string } {
const dir = tempDir(t);
const ledger = Ledger.open(join(dir, "ledger.db"));
t.after(() => ledger.close());
const task = loadTask(join(import.meta.dirname, "..", "tasks", "circle_packing"));
const repo = workspaceRepo(dir);
createRepository(repo);
const names = diagnosisUnversionedNames(task);
const saved = saveVersion(repo, task.baselineDir, names);
if (!saved.saved) throw new Error("基线存不成版本");
const w = new Writer(ledger);
ledger.append("code", { object: "base", version: saved.version, locations: null });
w.run("base-1", "baseline", 0, null, judged(0.96, "main"));
const baseRun = runPaths(dir, "base-1").run;
mkdirSync(baseRun, { recursive: true });
writeFileSync(join(baseRun, "solution.json"), "{}");
w.open("r1-diagnosisProposal", "diagnosisProposal", 1, ["a1"]);
const metric = { id: "note", meaning: "记录", unit: "无", direction: "maximize" as const, howMeasured: "数一数", missing: "recordMissing" as const };
w.sub("r1-diagnosisProposal", "a1", "diagnosisProposal", null, null, { explanations: [{ body: "解释" }], experiments: [{ name: "实验甲", spot: "第一步", body: "换个做法", metrics: [metric] }] });
w.close("r1-diagnosisProposal");
w.decision("diagnosisDedup", 1, null, { experiments: [{ experiment: "d1", sources: [{ author: "a1", index: 0 }] }] });
w.decision("selectDiagnosis", 1, null, { diagnose: true, chosen: ["d1"] });
const work = join(dir, "work");
cpSync(task.baselineDir, work, { recursive: true });
appendFileSync(join(work, "initial.py"), "\n\ndef run_experiment(experiment_id, baseline_dir):\n return [], [], 0.0\n");
placeBaselineArtifact(task, baseRun, work);
const frozen = { config: checkedFull(fullConfig()), steps: stepList(fullConfig()), facts: task.facts, prompts: null, image: "", seed: 1 };
return { s: { dir, task, ledger, frozen } as unknown as Study, work, baseline: saved.version };
}
const dxTask = (kind: "diagnosisImplementation" | "diagnosisRepair" | "diagnosisVerification") =>
({ actor: "x", kind, object: "r1-diagnosis", stageId: `r1-${kind}-1` });
test("诊断实现的接线:存版本时跳过只读的原始产物目录;Jev 对照选中实验的原文和全名指标;工作目录放进原始产物", (t) => {
const { s, work, baseline } = codeStudy(t);
const w = new Writer(s.ledger);
w.open("r1-diagnosisImplementation-1", "diagnosisImplementation", 1, ["x"]);
const content = { locations: [{ experiment: "d1", locations: ["initial.py:90-92"] }], concerns: "无", body: "无" };
const { known, code } = prepareSubmission(s, dxTask("diagnosisImplementation"), content, 0, work);
const saved = (known as { code: ForwardCode }).code;
if (!saved.saved) throw new Error("应存成版本");
assert.deepEqual(saved.protectedFiles, []);
assert.ok(saved.hunks.length > 0);
assert.ok(saved.hunks.every((h) => h.path === "initial.py"));
assert.match(saved.protocol.body, /### d1 实验甲\n\n动哪一处:第一步\n\n换个做法/);
assert.deepEqual(saved.protocol.metrics.map((m) => m.id), ["d1.note"]);
assert.deepEqual(code, { object: "r1-diagnosis", version: code!.version, locations: content.locations });
// 工作目录里放着的原始产物没有进版本:当前基线到诊断程序的改动里只有 initial.py。
const changes = compareVersions(workspaceRepo(s.dir), baseline, code!.version);
assert.deepEqual(changes.files.map((f) => f.path), ["initial.py"]);
});
test("诊断实现、修复、核验取材:实现从当前基线出发,修复从上次提交的诊断程序出发,核验和修复的差异都是当前基线到诊断程序;原始产物只给实现和修复", (t) => {
const { s, work, baseline } = codeStudy(t);
const w = new Writer(s.ledger);
const content = { locations: [{ experiment: "d1", locations: ["initial.py:90-92"] }], concerns: "无", body: "无" };
w.open("r1-diagnosisImplementation-1", "diagnosisImplementation", 1, ["x"]);
w.open("r1-diagnosisRepair-1", "diagnosisImplementation", 1, ["x"]);
w.open("r1-diagnosisVerification-1", "diagnosisVerification", 1, ["v"]);
assert.deepEqual(taskCode(s, dxTask("diagnosisImplementation")), { changes: null, startVersion: baseline });
const { code } = prepareSubmission(s, dxTask("diagnosisImplementation"), content, 0, work);
s.ledger.append("code", code!);
const changes = compareVersions(workspaceRepo(s.dir), baseline, code!.version);
assert.deepEqual(taskCode(s, dxTask("diagnosisRepair")), { changes, startVersion: code!.version });
assert.deepEqual(taskCode(s, dxTask("diagnosisVerification")), { changes, startVersion: null });
const baseRun = runPaths(s.dir, "base-1").run;
assert.equal(taskArtifact(s, dxTask("diagnosisImplementation")), baseRun);
assert.equal(taskArtifact(s, dxTask("diagnosisRepair")), baseRun);
assert.equal(taskArtifact(s, dxTask("diagnosisVerification")), null);
});