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
+227 -13
View File
@@ -1,4 +1,4 @@
import { copyFileSync, cpSync, existsSync, readFileSync, writeFileSync } from "node:fs";
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { Type, type Static } from "typebox";
import { Value } from "typebox/value";
@@ -13,7 +13,16 @@ import type {
} from "../vocab/index.ts";
import type { TaskFacts } from "./facts.ts";
import { measured, readMetricsFile, unmeasured } from "./metrics-file.ts";
import type { LightCheckOutcome, Task, TaskImplementation } from "./types.ts";
import type {
DiagnosisEnding,
DiagnosisExperiment,
DiagnosisExperimentJudged,
DiagnosisJudged,
DiagnosisJudgingStep,
LightCheckOutcome,
Task,
TaskImplementation,
} from "./types.ts";
// 填圆:在单位正方形里放 26 个圆,最大化半径之和。任务说明见 tasks/circle_packing/TASK.md。
// 凡是要执行任务代码的事(正式运行、校验器、轻量检查)都只交出运行定义,由调用方交给 runner 在任务镜像里跑;
@@ -22,6 +31,7 @@ import type { LightCheckOutcome, Task, TaskImplementation } from "./types.ts";
const CIRCLES = 26;
const ENTRY = "initial.py";
const HARNESS = "harness.py";
const DIAGNOSIS_HARNESS = "harness-diagnosis.py";
const VALIDATOR = "validate.py";
const CHECK_SCRIPT = "check-candidate.py";
const METRICS_SPEC = "metrics-spec.json";
@@ -29,6 +39,12 @@ const METRICS = "metrics.json";
const SOLUTION = "solution.json";
const VERDICT = "verdict.json";
const CHECK_RESULT = "check.json";
/** 诊断运行:原始产物目录、每个实验的答案目录、harness 写的每实验用时报告(路径都相对运行目录)。 */
const BASELINE_ARTIFACT = "baseline-artifact";
const ANSWERS = "answers";
const DIAGNOSIS_REPORT = "diagnosis.json";
/** 判分目录里每个实验写的校验器退出码文件(同一个目录下还有这个实验的 solution.json、verdict.json)。 */
const EXIT_FILE = "exit";
/** 轻量检查目录里放候选代码的子目录;检查脚本放在它的上一层,免得扫到自己。 */
const CHECK_CANDIDATE = "candidate";
/** 运行目录挂进容器的位置。 */
@@ -277,16 +293,16 @@ function readVerdict(judgeDir: string, exitCode: number): ValidatorVerdict {
return verdict;
}
function judge(
_task: Task,
runDir: string,
judgeDir: string,
formal: ProcessResult,
validator: ProcessResult,
hypothesisMetrics: HypothesisMetric[],
): JudgedResult {
const run = started(formal);
const verdict = readVerdict(judgeDir, requireExit("校验器", validator, [0, 1]));
/** 校验器判定读成的结论:算不算数、理由、最终分数、校验器给的辅助指标、合法化明细。 */
interface Scored {
valid: boolean;
reason: string;
score: Measured;
fromValidator: Record<string, Measured>;
extra: Json;
}
function scoreVerdict(verdict: ValidatorVerdict): Scored {
const legal = verdict.legalization;
let deduction: Measured;
let score: Measured;
@@ -328,7 +344,199 @@ function judge(
deduction,
},
};
return assemble(runDir, run, hypothesisMetrics, { valid: verdict.valid, reason }, score, fromValidator, extra);
return { valid: verdict.valid, reason, score, fromValidator, extra };
}
function judge(
_task: Task,
runDir: string,
judgeDir: string,
formal: ProcessResult,
validator: ProcessResult,
hypothesisMetrics: HypothesisMetric[],
): JudgedResult {
const run = started(formal);
const scored = scoreVerdict(readVerdict(judgeDir, requireExit("校验器", validator, [0, 1])));
return assemble(runDir, run, hypothesisMetrics, { valid: scored.valid, reason: scored.reason }, scored.score, scored.fromValidator, scored.extra);
}
// ---------------------------------------------------------------------------
// 诊断运行(方法.md 4.3"诊断"一段):一个容器、一次登记、一次扣次,里面按顺序跑几个实验。
// 全部是新增的一套函数,用自己的 harness(harness-diagnosis.py),不碰上面正式运行的任何一步。
// ---------------------------------------------------------------------------
/** 诊断程序存版本时跳过的名字:正式运行那份加上原始产物目录(它是宿主放进工作目录的,不是实验员的代码)。 */
function diagnosisUnversionedNames(facts: TaskFacts): string[] {
return [...facts.unversionedNames, BASELINE_ARTIFACT];
}
/**
* 把产出当前基线的那次正式运行的原始产物(solution.json)放进 dir 下的 baseline-artifact/,设成只读。
* 实验员的工作目录和诊断运行的运行目录都用它:实验员能看到实验要接着算的那份答案,运行时实验读的是同一份。
* 只读靠文件权限,防的是实验顺手改了它,不是安全边界。
*/
function placeBaselineArtifact(_task: Task, baselineRunDir: string, dir: string): void {
const target = join(dir, BASELINE_ARTIFACT);
mkdirSync(target);
const file = join(target, SOLUTION);
copyFileSync(join(baselineRunDir, SOLUTION), file);
chmodSync(file, 0o444);
}
function prepareDiagnosisRun(task: Task, programDir: string, baselineRunDir: string, runDir: string, experiments: DiagnosisExperiment[]): void {
cpSync(programDir, runDir, { recursive: true });
// 两份 harness 以宿主的为准;指标说明是全部实验登记的指标合在一起,每个实验只写自己的那几条。
copyFileSync(join(task.dir, HARNESS), join(runDir, HARNESS));
copyFileSync(join(task.dir, DIAGNOSIS_HARNESS), join(runDir, DIAGNOSIS_HARNESS));
writeFileSync(join(runDir, METRICS_SPEC), `${JSON.stringify(experiments.flatMap((e) => e.metrics), null, 2)}\n`);
placeBaselineArtifact(task, baselineRunDir, runDir);
mkdirSync(join(runDir, ANSWERS));
}
/** 总时限、核数照任务规定,实验按顺序跑,每个实验分到总时限 ÷ 实验数(由 harness 执行)。 */
function diagnosisRun(task: Task, imageDigest: string, seed: string, experimentIds: string[]): RunDefinition {
const eachSeconds = task.facts.run.wallSeconds / experimentIds.length;
const command = [
"python",
"-B",
`${WORKDIR}/${DIAGNOSIS_HARNESS}`,
"--program",
ENTRY,
"--baseline",
BASELINE_ARTIFACT,
"--answers",
ANSWERS,
"--seconds-each",
String(eachSeconds),
"--report",
DIAGNOSIS_REPORT,
"--experiments",
...experimentIds,
];
return definition(task, imageDigest, command, task.facts.run.wallSeconds, { INFORSI_SEED: seed });
}
/** 一个实验在运行目录里的结局:harness 报告里的一项,或者整次运行先结束、没轮到它。 */
type ExperimentOutcome = { ending: "finished" | "crashed" | "timeout"; wallSeconds: number } | { ending: "notRun"; reason: string };
/** harness 写的报告(我们自己的程序,按约定的格式直接读)。 */
interface ReportEntry {
id: string;
status: "finished" | "crashed" | "timeout";
wall_seconds: number;
}
function experimentOutcomes(
task: Task,
runDir: string,
process: Exclude<ProcessResult, { ending: "notStarted" }>,
experiments: DiagnosisExperiment[],
): ExperimentOutcome[] {
const path = join(runDir, DIAGNOSIS_REPORT);
const report = existsSync(path) ? (readJson(path) as ReportEntry[]) : [];
return experiments.map((e) => {
const entry = report.find((r) => r.id === e.id);
if (entry !== undefined) return { ending: entry.status, wallSeconds: entry.wall_seconds };
const failure = processFailure(task, process);
if (failure === null) throw new Error(`诊断运行正常退出,但 ${DIAGNOSIS_REPORT} 里没有实验 ${e.id}`);
return { ending: "notRun", reason: `没轮到:整次诊断运行${failure}` };
});
}
/** 没有答案可判的实验:不算数,辅助指标和假设专用指标都记测不到并附原因。 */
function unjudgedExperiment(
id: string,
ending: Exclude<DiagnosisEnding, "finished">,
wall: Measured,
reason: string,
hypothesisMetrics: HypothesisMetric[],
): DiagnosisExperimentJudged {
const metrics = {
...Object.fromEntries(FROM_VALIDATOR.map((m) => [m, unmeasured(`没有跑校验器:${reason}`)])),
...Object.fromEntries(hypothesisMetrics.map((m) => [m.id, unmeasured(`没有结果:${reason}`)])),
};
return { id, ending, wallSeconds: wall, verdict: "invalid", verdictReason: reason, score: unmeasured(reason), metrics, extra: { legalization: null } };
}
function judgedExperiments(
task: Task,
runDir: string,
judgeDir: string,
experiments: DiagnosisExperiment[],
outcomes: ExperimentOutcome[],
): DiagnosisJudged {
const eachSeconds = task.facts.run.wallSeconds / experiments.length;
return {
experiments: experiments.map((e, i): DiagnosisExperimentJudged => {
const outcome = outcomes[i]!;
if (outcome.ending === "notRun") return unjudgedExperiment(e.id, "notRun", unmeasured(outcome.reason), outcome.reason, e.metrics);
const wall = measured(outcome.wallSeconds);
if (outcome.ending === "timeout") {
return unjudgedExperiment(e.id, "timeout", wall, `超过分到的时间 ${eachSeconds} 秒,被结束`, e.metrics);
}
if (outcome.ending === "crashed") {
return unjudgedExperiment(e.id, "crashed", wall, `实验的进程以非零退出码结束(报错见这次运行的 stderr.log)`, e.metrics);
}
const dir = join(judgeDir, e.id);
const exitCode = Number(readFileSync(join(dir, EXIT_FILE), "utf8").trim());
const scored = scoreVerdict(readVerdict(dir, exitCode));
const hypothesis = readMetricsFile(join(runDir, ANSWERS, e.id, METRICS), `${ANSWERS}/${e.id}/${METRICS}`, e.metrics).hypothesis;
return {
id: e.id,
ending: "finished",
wallSeconds: wall,
verdict: scored.valid ? "valid" : "invalid",
verdictReason: scored.reason,
score: scored.score,
metrics: { ...scored.fromValidator, ...hypothesis },
extra: scored.extra,
};
}),
};
}
/** 逐个实验判分。校验器一个容器、按顺序对每个有答案的实验各跑一次,退出码各存一个文件。 */
const VALIDATE_EACH = `for id in "$@"; do
python -B ${VALIDATOR} --circles ${CIRCLES} --solution "$id/${SOLUTION}" --atol ${TOLERANCE} --legalize --verdict "$id/${VERDICT}"
echo $? > "$id/${EXIT_FILE}"
done`;
function prepareDiagnosisJudging(
task: Task,
runDir: string,
judgeDir: string,
imageDigest: string,
process: ProcessResult,
experiments: DiagnosisExperiment[],
): DiagnosisJudgingStep {
const run = started(process);
const outcomes = experimentOutcomes(task, runDir, run, experiments);
const finished = experiments.filter((_, i) => outcomes[i]!.ending === "finished");
if (finished.length === 0) return { status: "judged", result: judgedExperiments(task, runDir, judgeDir, experiments, outcomes) };
for (const e of finished) {
mkdirSync(join(judgeDir, e.id));
copyFileSync(join(runDir, ANSWERS, e.id, SOLUTION), join(judgeDir, e.id, SOLUTION));
}
copyFileSync(join(task.dir, VALIDATOR), join(judgeDir, VALIDATOR));
const command = ["sh", "-c", VALIDATE_EACH, "inforsi-validate-each", ...finished.map((e) => e.id)];
return { status: "validate", definition: definition(task, imageDigest, command, VALIDATOR_SECONDS * finished.length, {}) };
}
function judgeDiagnosis(
task: Task,
runDir: string,
judgeDir: string,
process: ProcessResult,
validator: ProcessResult | null,
experiments: DiagnosisExperiment[],
): DiagnosisJudged {
const run = started(process);
const outcomes = experimentOutcomes(task, runDir, run, experiments);
if (outcomes.some((o) => o.ending === "finished")) {
if (validator === null) throw new Error("有实验交出了答案,却没有校验器的运行结果");
requireExit("校验器", validator, [0]);
}
return judgedExperiments(task, runDir, judgeDir, experiments, outcomes);
}
export const circlePacking: TaskImplementation = {
@@ -339,4 +547,10 @@ export const circlePacking: TaskImplementation = {
readLightCheck,
prepareJudging,
judge,
diagnosisUnversionedNames,
placeBaselineArtifact,
prepareDiagnosisRun,
diagnosisRun,
prepareDiagnosisJudging,
judgeDiagnosis,
};