852600df02
消融默认关,关掉时解释阶段一并跳过;选诊断实验用信息价值减成本。 eval/diagnosis/research.json 为第三块配置。vocab 字段说明的出口文字随提示词一起改,见下一次提交的 prompts/ 与同文件改动(本次含在内)。 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
187 lines
8.3 KiB
TypeScript
187 lines
8.3 KiB
TypeScript
import { readFileSync } from "node:fs";
|
||
import { join, resolve } from "node:path";
|
||
import type { HypothesisMetric, JudgedResult, MetricDefinition, ProcessResult, RunDefinition } from "../vocab/index.ts";
|
||
import { circlePacking } from "./circle-packing.ts";
|
||
import { checkTaskFacts, type TaskKind } from "./facts.ts";
|
||
import type {
|
||
DiagnosisExperiment,
|
||
DiagnosisJudged,
|
||
DiagnosisJudgingStep,
|
||
JudgingStep,
|
||
LightCheckOutcome,
|
||
Task,
|
||
TaskImplementation,
|
||
} from "./types.ts";
|
||
|
||
// tasks 对外的函数。任务目录的约定:task.json(任务事实)、TASK.md(给实验员的说明)、
|
||
// Dockerfile 与依赖清单(建镜像)、baseline/(基线代码),以及各任务自己的 Python 工具。
|
||
// 每个函数按 task.facts.kind 从对照表里取出那种任务的实现来做。
|
||
|
||
/** 任务种类 → 实现。加一种任务就在这里加一行。 */
|
||
const IMPLEMENTATIONS: Readonly<Record<TaskKind, TaskImplementation>> = {
|
||
circle_packing: circlePacking,
|
||
};
|
||
|
||
function implementation(task: Task): TaskImplementation {
|
||
return IMPLEMENTATIONS[task.facts.kind];
|
||
}
|
||
|
||
/** 读一个任务目录:严格检查 task.json,同时交回 Dockerfile 所在目录、基线目录和 TASK.md 全文。 */
|
||
export function loadTask(taskDir: string): Task {
|
||
const dir = resolve(taskDir);
|
||
const path = join(dir, "task.json");
|
||
let value: unknown;
|
||
try {
|
||
value = JSON.parse(readFileSync(path, "utf8"));
|
||
} catch (error) {
|
||
throw new Error(`${path} 读不出来或不是合法的 JSON:${(error as Error).message}`, { cause: error });
|
||
}
|
||
return {
|
||
facts: checkTaskFacts(value),
|
||
dir,
|
||
baselineDir: join(dir, "baseline"),
|
||
guide: readFileSync(join(dir, "TASK.md"), "utf8"),
|
||
};
|
||
}
|
||
|
||
/** 最终指标与常规辅助指标的定义。最终指标的编号和方向取自 task.json。 */
|
||
export function metricDefinitions(task: Task): MetricDefinition[] {
|
||
return implementation(task).metricDefinitions(task.facts);
|
||
}
|
||
|
||
/**
|
||
* 准备一次正式运行的运行目录:把候选代码、任务的 harness 和这次要测的假设专用指标说明
|
||
* (metrics-spec.json)放进去。runDir 由调用方建好,应为空目录。
|
||
* 消融格里传进来的指标应当已经全部改成"缺了只记缺失"(vocab 指标说明),这里不区分。
|
||
*/
|
||
export function prepareRun(task: Task, candidateDir: string, runDir: string, hypothesisMetrics: HypothesisMetric[]): void {
|
||
implementation(task).prepareRun(task, candidateDir, runDir, hypothesisMetrics);
|
||
}
|
||
|
||
/**
|
||
* 正式运行的运行定义。种子由调用方给,同一份研究的全部运行用同一个种子:
|
||
* 评测要确定,消融不重跑基线也靠这一点(方法.md 第十部分第 9 条)。
|
||
*/
|
||
export function formalRun(task: Task, imageDigest: string, seed: string): RunDefinition {
|
||
return implementation(task).formalRun(task, imageDigest, seed);
|
||
}
|
||
|
||
/**
|
||
* 轻量检查:把候选代码和检查脚本放进 checkDir(调用方建好的空目录),交回运行定义。
|
||
* 只看语法和入口函数,不执行候选代码。seconds 来自配置;次数上限不归这里管。
|
||
*/
|
||
export function lightCheck(task: Task, candidateDir: string, checkDir: string, imageDigest: string, seconds: number): RunDefinition {
|
||
return implementation(task).lightCheck(task, candidateDir, checkDir, imageDigest, seconds);
|
||
}
|
||
|
||
/** 解读轻量检查:通过,或者不通过及原因。检查脚本自己没正常跑完时报错停下。 */
|
||
export function readLightCheck(task: Task, checkDir: string, result: ProcessResult): LightCheckOutcome {
|
||
return implementation(task).readLightCheck(checkDir, result);
|
||
}
|
||
|
||
/**
|
||
* 判分第一步。正式运行没正常结束、或者没有产物时直接判不算数;否则把产物和宿主保管的校验器
|
||
* 复制进 judgeDir(调用方建好的空目录),交回校验器的运行定义。
|
||
*/
|
||
export function prepareJudging(
|
||
task: Task,
|
||
runDir: string,
|
||
judgeDir: string,
|
||
imageDigest: string,
|
||
formal: ProcessResult,
|
||
hypothesisMetrics: HypothesisMetric[],
|
||
): JudgingStep {
|
||
return implementation(task).prepareJudging(task, runDir, judgeDir, imageDigest, formal, hypothesisMetrics);
|
||
}
|
||
|
||
/**
|
||
* 判分第二步:读校验器写的判定、运行目录里候选写的指标文件,加上正式运行的结果,交回判分结果。
|
||
* 校验器自己没正常跑完、判定文件认不出、修后分数高于原分数时报错停下:这些是宿主一侧的问题。
|
||
*/
|
||
export function judge(
|
||
task: Task,
|
||
runDir: string,
|
||
judgeDir: string,
|
||
formal: ProcessResult,
|
||
validator: ProcessResult,
|
||
hypothesisMetrics: HypothesisMetric[],
|
||
): JudgedResult {
|
||
return implementation(task).judge(task, runDir, judgeDir, formal, validator, hypothesisMetrics);
|
||
}
|
||
|
||
/** 改动过的文件里,属于 task.json"不许改"名单的那些。路径都相对候选代码目录。 */
|
||
export function protectedChanges(task: Task, changedFiles: string[]): string[] {
|
||
return changedFiles.filter((file) => task.facts.protectedFiles.includes(file));
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 诊断运行(方法.md 4.3"诊断"一段):一个容器、一次登记、一次扣次,里面按顺序跑几个实验。
|
||
// 是和上面正式运行并列的另一套函数,上面的函数不认识它。各目录同样由调用方建好、传空目录进来。
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** 存诊断程序版本时跳过的名字:task.json 的清单,加上宿主放进工作目录的原始产物目录。 */
|
||
export function diagnosisUnversionedNames(task: Task): string[] {
|
||
return implementation(task).diagnosisUnversionedNames(task.facts);
|
||
}
|
||
|
||
/**
|
||
* 把产出当前基线的那次正式运行的原始产物,只读地放进 dir(实验员工作目录,调用方已用 checkoutVersion 取出基线代码)。
|
||
* baselineRunDir 是那次运行的运行目录。放在哪、叫什么,由任务定,写在 TASK.md 里给实验员看。
|
||
*/
|
||
export function placeBaselineArtifact(task: Task, baselineRunDir: string, dir: string): void {
|
||
implementation(task).placeBaselineArtifact(task, baselineRunDir, dir);
|
||
}
|
||
|
||
/**
|
||
* 准备一次诊断运行的运行目录:诊断程序、两份 harness、全部实验登记的指标说明(metrics-spec.json)、
|
||
* 只读的原始产物。runDir 由调用方建好,应为空目录;实验的先后顺序就是 experiments 的顺序。
|
||
*/
|
||
export function prepareDiagnosisRun(
|
||
task: Task,
|
||
programDir: string,
|
||
baselineRunDir: string,
|
||
runDir: string,
|
||
experiments: DiagnosisExperiment[],
|
||
): void {
|
||
implementation(task).prepareDiagnosisRun(task, programDir, baselineRunDir, runDir, experiments);
|
||
}
|
||
|
||
/**
|
||
* 诊断运行的运行定义:总时限、核数照任务规定,harness 按顺序跑 experimentIds 里的实验,
|
||
* 每个实验分到总时限 ÷ 实验数。种子同正式运行(同一份研究共用一个)。
|
||
*/
|
||
export function diagnosisRun(task: Task, imageDigest: string, seed: string, experimentIds: string[]): RunDefinition {
|
||
return implementation(task).diagnosisRun(task, imageDigest, seed, experimentIds);
|
||
}
|
||
|
||
/**
|
||
* 诊断判分第一步:每个交出答案的实验的答案复制进 judgeDir 下以实验编号命名的子目录,
|
||
* 交回校验器的运行定义(一个容器逐个实验判分);没有一个实验交出答案时直接交回判分结果。
|
||
*/
|
||
export function prepareDiagnosisJudging(
|
||
task: Task,
|
||
runDir: string,
|
||
judgeDir: string,
|
||
imageDigest: string,
|
||
process: ProcessResult,
|
||
experiments: DiagnosisExperiment[],
|
||
): DiagnosisJudgingStep {
|
||
return implementation(task).prepareDiagnosisJudging(task, runDir, judgeDir, imageDigest, process, experiments);
|
||
}
|
||
|
||
/**
|
||
* 诊断判分第二步:按实验分开交回分数、合法化、登记的指标、用时、结局。validator 是第一步交回过运行定义时跑出的结果,
|
||
* 第一步直接判完时传 null。资源用量记在整次运行的 ProcessResult 里,不在这里。
|
||
* 校验器自己没正常跑完、判定文件认不出、修后分数高于原分数时报错停下,同正式运行的 judge。
|
||
*/
|
||
export function judgeDiagnosis(
|
||
task: Task,
|
||
runDir: string,
|
||
judgeDir: string,
|
||
process: ProcessResult,
|
||
validator: ProcessResult | null,
|
||
experiments: DiagnosisExperiment[],
|
||
): DiagnosisJudged {
|
||
return implementation(task).judgeDiagnosis(task, runDir, judgeDir, process, validator, experiments);
|
||
}
|