feat: 任务书 05,runner 与 tasks 两个模块

runner 用 Docker 跑一条命令并交回结局和资源用量;tasks 读 task.json、准备运行目录、给出正式运行/校验器/轻量检查的运行定义,并从原始产物独立判分。任务目录从参考项目搬到 tasks/circle_packing/,新增 task.json;vocab 的 VERDICTS 删掉"判不了",ProcessResult 的 cancelled 改为"容器从外面被杀了"。

全新上下文审阅后的两处修复:计量 shell 以容器内 root 运行,命令经 setpriv 降权,结果写在容器自己的文件层、结束后 docker cp 取出,并加 no-new-privileges,候选无法伪造计量结果把超时记成正常退出;harness 开跑前删掉旧的 solution.json、metrics.json,旧产物不会被当成本次的值。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 13:55:47 -04:00
parent 6005fb8ec2
commit cf6bc4c844
25 changed files with 3205 additions and 6 deletions
+107
View File
@@ -0,0 +1,107 @@
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 { 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));
}