feat: flow 第 1 部分(骨架、研究开始、第零轮)与第零轮检验

按任务书 12 实现 src/flow:从账本现算下一步的纯函数、主循环、恢复入口、派模型任务和跑正式运行两个共用动作、冻结配置、基线、第零轮三个研究者阶段和入口分配,走到第 1 轮时停下。materials 导出 finalEntries、加 researcherId 和 StudyRecord。

审阅采纳两条:研究编号本机已用过时冻结前报错;docker run 没启动时写完结果就停下,不在本进程里无限起新编号。

eval/ 新增第零轮检验:起研究命令行、回放假模型、检验用 Jev 客户端、六问质量评估、16-real-1 回放、杀进程与两份同时跑。tsc 与 depcruise 也检查 eval/,新增 eval 只经 index.ts 用正式代码的规则。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 10:58:52 -04:00
parent 8491191a2b
commit 75d7cffd27
36 changed files with 2456 additions and 14 deletions
+54
View File
@@ -0,0 +1,54 @@
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import { ofKind, readLedger } from "../lib/ledger.ts";
import { replayModels, startReplay } from "../lib/replay.ts";
import { containers, launch, newEvalDir, sleep, writeConfig } from "../lib/study.ts";
// 两份研究同时跑(任务书 12 第 5.4 节):两个研究编号同时启动,都跑到基线结束,查两边各扣各的、容器不撞名。
// node eval/round-zero/concurrent.ts
// 研究者用一份空清单的回放:基线跑完、派写入口任务时模型一份不交,flow 按"交不出合格提交"停下,不花钱。
const out = newEvalDir("r0-concurrent");
const ids = ["r0-conc-a", "r0-conc-b"];
const researchers = ["researcher-1", "researcher-2", "researcher-3"];
const replay = await startReplay({ source: "空清单", models: Object.fromEntries([...researchers, "experimenter", "verifier"].map((m) => [m, []])) }, join(out, "pi-agent"), 0);
const studies = ids.map((id) => {
const dir = join(out, id);
const config = writeConfig("eval/round-zero/research.json", join(out, `${id}.research.json`), id, replayModels(researchers, "jev-latest"));
return { id, dir, run: launch(config, dir, replay.env) };
});
// 跑的时候每 100 毫秒看一次本机 inforsi- 开头的容器,记下见过的名字。
const seen = new Set<string>();
let running = true;
const watcher = (async () => {
while (running) {
for (const n of containers("inforsi-")) seen.add(n);
await sleep(100);
}
})();
const results = await Promise.all(studies.map((s) => s.run.done));
running = false;
await watcher;
await replay.close();
const report = studies.map((s, i) => {
const r = readLedger(s.dir);
const base = new Set(ofKind(r, "runPlanned").filter((p) => p.content.purpose === "baseline").map((p) => p.content.run));
return {
id: s.id,
outcome: results[i]!.outcome,
baseStarted: ofKind(r, "runStarted").filter((x) => base.has(x.content.run)).length,
baseResults: ofKind(r, "runResult").filter((x) => base.has(x.content.run)).map((x) => x.content),
containersSeen: [...seen].filter((n) => n.startsWith(`inforsi-${s.id}-`)),
};
});
const shared = report[0]!.containersSeen.filter((n) => report[1]!.containersSeen.includes(n));
const pass = report.every((s) => s.baseStarted === 1 && s.baseResults.length === 1 && s.baseResults[0]!.judged !== null) && shared.length === 0;
writeFileSync(join(out, "concurrent-report.json"), `${JSON.stringify({ pass, report, allContainersSeen: [...seen], shared }, null, 2)}\n`);
for (const s of report) {
console.log(`${s.id}:基线确认启动 ${s.baseStarted} 次;结果 ${JSON.stringify(s.baseResults.map((x) => x.judged))};见过的容器 ${s.containersSeen.join("、") || "(没看到)"};结局 ${JSON.stringify(s.outcome)}`);
}
console.log(`两边共用的容器名:${shared.join("、") || "没有"};${pass ? "通过" : "不通过"}`);
console.log(`报告:${join(out, "concurrent-report.json")}`);
+71
View File
@@ -0,0 +1,71 @@
import { copyFileSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { entryId } from "../../src/materials/index.ts";
import type { ReplayItem, ReplayScript } from "../lib/replay.ts";
// 从参考项目 16-real-1 的账本取第零轮的提交,按任务书 12 第 5.3 节转成新项目的格式,写成回放脚本。
// 原账本只读:先复制到临时目录,再以只读方式打开复制件,原目录里不写任何东西。
// node eval/round-zero/extract-16.ts
const SOURCE = "/home/iomgaa/rb-acceptance/16-real-1/board.sqlite";
export const REPLAY_FILE = "eval/round-zero/replay-16-real-1.json";
const PHASE_STAGE: Record<string, string> = { "entry-draft": "entryDraft", "entry-discuss": "discussion", "entry-claim": "claim" };
const copy = join(mkdtempSync(join(tmpdir(), "inforsi-rb16-")), "board.sqlite");
copyFileSync(SOURCE, copy);
const db = new DatabaseSync(copy, { readOnly: true });
type OldEntry = { name: string; startFrom: string; whyWorth: string };
const rows = db
.prepare(
`SELECT st.phase, st.id AS stage, s.actor, s.version, s.created_seq AS seq, s.payload
FROM submissions s JOIN stages st ON st.id = s.stage_id
WHERE st.phase IN ('entry-draft', 'entry-discuss', 'entry-claim') ORDER BY s.created_seq`,
)
.all() as Array<{ phase: string; stage: string; actor: string; version: number; seq: number; payload: string }>;
const fact = db.prepare("SELECT payload FROM facts WHERE kind = 'entry-assignment'").all() as Array<{ payload: string }>;
db.close();
if (fact.length !== 1) throw new Error(`entry-assignment 应有 1 条,实有 ${fact.length} 条`);
/** 当年入口 {name, startFrom, whyWorth} → 新项目 {name, body}。 */
const entry = (e: OldEntry) => ({ name: e.name, body: `从哪里开始看:${e.startFrom}\n\n为什么值得走:${e.whyWorth}` });
/** 当年入口编号 researcher-1-e4 → 新项目 e-researcher-1-4。 */
function newId(old: string): string {
const m = /^(researcher-\d+)-e(\d+)$/.exec(old);
if (m === null) throw new Error(`入口编号 ${old} 不是 researcher-<i>-e<n> 的写法`);
return entryId(m[1]!, Number(m[2]));
}
const models: Record<string, ReplayItem[]> = {};
for (const r of rows) {
const p = JSON.parse(r.payload) as { entries?: OldEntry[]; body?: string; choices?: string[] };
const payload =
r.phase === "entry-draft"
? { entries: p.entries!.map(entry) }
: r.phase === "entry-discuss"
? { body: p.body!, ...(p.entries === undefined ? {} : { entries: p.entries.map(entry) }) }
: { choices: p.choices!.map(newId) };
(models[r.actor] ??= []).push({ from: `${r.stage} ${PHASE_STAGE[r.phase]} seq ${r.seq} v${r.version}`, arguments: { payload } });
}
const old = JSON.parse(fact[0]!.payload) as { assignments: Array<{ researcher: string; entryId: string; rank: number | null }>; entries: Array<{ entryId: string; author: string }> };
const expected = old.assignments.map((a) => ({ researcher: a.researcher, entry: newId(a.entryId), rank: a.rank }));
const script: ReplayScript & { expected: typeof expected; expectedEntries: string[] } = {
source: `${SOURCE}(第零轮 submissions,created_seq ${rows[0]!.seq}–${rows.at(-1)!.seq})`,
models,
expected,
expectedEntries: old.entries.map((e) => newId(e.entryId)),
};
writeFileSync(REPLAY_FILE, `${JSON.stringify(script, null, 2)}\n`);
// 核对:三阶段各几份、作者、版本,以及当年的分配。
console.log(`共 ${rows.length} 份提交,序号 ${rows[0]!.seq}–${rows.at(-1)!.seq}`);
for (const phase of Object.keys(PHASE_STAGE)) {
const these = rows.filter((r) => r.phase === phase);
console.log(`${phase}:${these.length} 份;${these.map((r) => `${r.actor}@${r.seq}(v${r.version}${JSON.parse(r.payload).entries ? `,${JSON.parse(r.payload).entries.length} 个入口` : ""})`).join("、")}`);
}
console.log(`当年分配:${expected.map((a) => `${a.researcher} → ${a.entry}(第 ${a.rank} 志愿)`).join(";")}`);
console.log(`当年最终入口:${script.expectedEntries.join("、")}`);
console.log(`已写 ${REPLAY_FILE}`);
+49
View File
@@ -0,0 +1,49 @@
{
"note": "任务书 12 第 5.1 节的关键想法清单和死路清单,出处照抄。英文说明给 Jev 看。",
"key": [
{
"id": "K1",
"name": "联合优化",
"description": "Optimize the centers and radii of all 26 circles together (78 numbers) with a constrained continuous optimizer such as scipy SLSQP, with constraints that every circle stays inside the square and no two circles overlap.",
"source": "最大的一跳:OpenEvolve 2.377→2.634;AdaEvolve 2.541→2.610;参考项目 12-real-1、12-real-2 第 1 轮采纳;16-real-1 关掉它少 0.132"
},
{
"id": "K2",
"name": "扰动重优化",
"description": "Starting from the current best solution, perturb the circle centers by a moderate amount (about 0.01 to 0.06), run the joint optimization again, and keep the result only if it is better; this includes annealing-style acceptance and larger perturbations after the search stagnates.",
"source": "AlphaEvolve、ShinkaEvolve、Discovery Loop;参考项目 12-real-2 第 2 轮靠它从 2.6155 到 2.635983,15-tight-2 也到了 2.635983"
},
{
"id": "K3",
"name": "结构化起点与多起点",
"description": "Start from structured layouts such as hexagonal or staggered rows, or from many random starting layouts, run the optimization from each start, and keep the best result.",
"source": "ThetaEvolve、TTT-Discover 只靠\"错排起点 + 一次 SLSQP\"就到 2.635983;CodeEvolve 10 次重启"
},
{
"id": "K4",
"name": "固定圆心求最优半径",
"description": "Keep the circle centers fixed and solve a linear program for the radii that maximizes their sum.",
"source": "ShinkaEvolve 容差 0 那次的第一步、Discovery Loop 的种子;16-real-1 消融撑 28%"
},
{
"id": "K5",
"name": "严格合法收尾",
"description": "As a final step, shrink all radii by a tiny amount or refine the solution so that it is strictly valid under zero tolerance.",
"source": "ShinkaEvolve、ThetaEvolve 严格版;本任务容差为 0(TASK.md)"
}
],
"deadEnds": [
{
"id": "D1",
"name": "只做几何构造",
"description": "Place the circle centers by a hand-designed rule (hexagonal grid, square grid, concentric rings) and only tune the arrangement and its parameters, without using continuous optimization to move the centers.",
"source": "各家卡在 2.2–2.54;等半径六角的上限是 2.438966;OpenEvolve 第一阶段提示词要求只写构造,卡在 2.377;参考项目 9a、9b-real-2 卡在 2.517、2.541"
},
{
"id": "D2",
"name": "只优化半径",
"description": "Keep the circle centers fixed and only change how the radii are allocated, as the whole route rather than as a helper step.",
"source": "上限受布局限制,2.5414 是反复出现的平台"
}
]
}
+121
View File
@@ -0,0 +1,121 @@
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import type { LedgerRecord } from "../../src/ledger/index.ts";
import { entryDecision, finalSubmissions, ofKind, readLedger, sameAssignment, stages, type Assignment } from "../lib/ledger.ts";
import { replayModels, startReplay, type ReplayScript } from "../lib/replay.ts";
import { containers, launch, newEvalDir, sleep, waitForLedger, writeConfig } from "../lib/study.ts";
// 杀进程检验(任务书 12 第 5.4 节):在一处 SIGKILL 控制进程,再启动让它跑完第零轮,逐条查五件事。
// node eval/round-zero/kill.ts baseline|drafting|stageFull
// 模型一侧用 16-real-1 的回放(不花钱、结果确定,分配应和当年相同);基线照样用真 Docker。
// 回放每次回复前等 DELAY_MS,留出"研究者正在写""最后一份刚收下、阶段还没关"的窗口。
const DELAY_MS = 3000;
type Point = "baseline" | "drafting" | "stageFull";
const point = process.argv[2] as Point;
/** 三处杀点:看到账本里出现这种状态就杀。 */
const HITS: Record<Point, (r: LedgerRecord[]) => boolean> = {
// 基线容器正在跑:基线运行确认启动了、还没有结果。
baseline: (r) => {
const base = new Set(ofKind(r, "runPlanned").filter((p) => p.content.purpose === "baseline").map((p) => p.content.run));
const done = new Set(ofKind(r, "runResult").map((x) => x.content.run));
return ofKind(r, "runStarted").some((s) => base.has(s.content.run) && !done.has(s.content.run));
},
// 研究者 2 正在写入口:他在写入口阶段的调用开始了、还没结束。
drafting: (r) => {
const draft = new Set(stages(r).filter((s) => s.stage === "entryDraft").map((s) => s.stageId));
return openCalls(r).some((c) => c.content.actor === "researcher-2" && draft.has(c.content.stageId ?? ""));
},
// 某阶段最后一份提交刚收下、阶段还没关:名单上的人都交了,还没有关闭记录。
stageFull: (r) => stages(r).some((s) => !s.closed && s.roster.every((a) => submitted(r, s.stageId, a))),
};
if (!(point in HITS)) throw new Error(`杀点只能是 ${Object.keys(HITS).join("、")} 之一`);
function openCalls(r: LedgerRecord[]): Array<LedgerRecord<"callStarted">> {
const ended = new Set(ofKind(r, "callEnded").map((e) => e.content.call));
return ofKind(r, "callStarted").filter((c) => !ended.has(c.content.call));
}
const submitted = (r: LedgerRecord[], stageId: string, author: string) =>
ofKind(r, "submission").some((s) => s.content.stageId === stageId && s.content.author === author);
const script = JSON.parse(readFileSync("eval/round-zero/replay-16-real-1.json", "utf8")) as ReplayScript & { expected: Assignment[] };
// 研究编号也是镜像标签的一部分,Docker 镜像名只许小写,所以杀点名转成小写。
const name = `r0-kill-${point.toLowerCase()}`;
const out = newEvalDir(name);
const replay = await startReplay(script, join(out, "pi-agent"), DELAY_MS);
const config = writeConfig("eval/round-zero/research.json", join(out, "research.json"), name, replayModels(Object.keys(script.models).sort(), "jev-latest"));
const studyDir = join(out, "study");
// ---------------------------------------------------------------------------
// 第一次启动,打中就杀
// ---------------------------------------------------------------------------
const first = launch(config, studyDir, replay.env);
await waitForLedger(studyDir, first.child);
let triggered: LedgerRecord | undefined;
while (first.child.exitCode === null) {
const r = readLedger(studyDir);
if (HITS[point](r)) {
first.child.kill("SIGKILL");
triggered = r.at(-1);
break;
}
await sleep(20);
}
await first.done;
if (triggered === undefined) throw new Error(`没打中 ${point}:研究进程自己结束了,账本最后一条是 ${JSON.stringify(readLedger(studyDir).at(-1))}`);
// 进程已死,此刻的账本就是被杀时的样子。
const atKill = readLedger(studyDir);
const killSeq = atKill.at(-1)!.seq;
const killedCalls = openCalls(atKill);
const openStages = stages(atKill).filter((s) => !s.closed);
// ---------------------------------------------------------------------------
// 再启动,跑完第零轮
// ---------------------------------------------------------------------------
const second = await launch(null, studyDir, replay.env).done;
await replay.close();
const r = readLedger(studyDir);
const studyId = name;
const baseRuns = new Set(ofKind(r, "runPlanned").filter((p) => p.content.purpose === "baseline").map((p) => p.content.run));
const baseStarted = ofKind(r, "runStarted").filter((s) => baseRuns.has(s.content.run));
const leftover = containers(`inforsi-${studyId}-`);
const callEnds = killedCalls.map((c) => ({ call: c.content.call, end: ofKind(r, "callEnded").find((e) => e.content.call === c.content.call)?.content ?? null }));
const redispatch = openStages.flatMap((s) =>
s.roster.map((actor) => {
const before = submitted(atKill, s.stageId, actor);
const again = ofKind(r, "callStarted").some((c) => c.seq > killSeq && c.content.stageId === s.stageId && c.content.actor === actor);
return { stageId: s.stageId, actor, submittedBeforeKill: before, dispatchedAfterRestart: again, ok: before !== again };
}),
);
const finals = finalSubmissions(r);
const perStage = stages(r).map((s) => ({
stageId: s.stageId,
closed: s.closed,
finalPerPerson: Object.fromEntries(s.roster.map((a) => [a, finals.filter((f) => f.content.stageId === s.stageId && f.content.author === a).length])),
}));
const decision = entryDecision(r);
const assignment = decision === undefined ? null : decision.output;
const checks = {
"1 基线只扣了 1 次,Docker 里没有留下基线容器": { pass: baseStarted.length === 1 && leftover.length === 0, baseRunsPlanned: [...baseRuns], baseStarted: baseStarted.length, leftoverContainers: leftover },
"2 被杀那次调用有结束记录,结局出错、原因进程中断": {
pass: callEnds.every((c) => c.end !== null && c.end.status === "failed" && c.end.reason.includes("进程中断")),
calls: callEnds,
},
"3 没交上的人被重新派了,交过的没被重派": { pass: redispatch.every((x) => x.ok), detail: redispatch },
"4 每个阶段都关上了,每人每阶段只有一份最终收下的提交": {
pass: perStage.every((s) => s.closed && Object.values(s.finalPerPerson).every((n) => n === 1)),
detail: perStage,
},
"5 分配照常写出": { pass: assignment !== null, assignment, sameAsReference: assignment !== null && sameAssignment(assignment, script.expected) },
};
const report = { point, triggeredAt: triggered, lastRecordAtKill: atKill.at(-1), outcomeAfterRestart: second.outcome, checks };
writeFileSync(join(out, "kill-report.json"), `${JSON.stringify(report, null, 2)}\n`);
console.log(`\n杀点 ${point}:触发时账本最后一条 #${triggered.seq} ${triggered.kind};被杀时最后一条 #${killSeq} ${atKill.at(-1)!.kind} ${JSON.stringify(atKill.at(-1)!.content).slice(0, 200)}`);
console.log(`再启动后的结局:${JSON.stringify(second.outcome)}`);
for (const [k, v] of Object.entries(checks)) console.log(`${v.pass ? "通过" : "不通过"} ${k}`);
console.log(`报告:${join(out, "kill-report.json")}`);
+148
View File
@@ -0,0 +1,148 @@
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { ask, THRESHOLD_NOTE, type Answer, type Noul } from "../lib/jev.ts";
import { entriesIn, entryDecision, readLedger, stages, type Entry } from "../lib/ledger.ts";
import { ENTRY_ONLY, LEADS_TO_IDEA, SAME_KIND, STOPS_AT_DEAD_END, TASKS } from "./questions.ts";
// 第零轮质量评估(任务书 12 第 5.2 节六问)。只读账本,问 Jev,原始概率全部存进 <检验目录>/quality.json。
// node eval/round-zero/quality.ts <检验目录>(研究在 <检验目录>/study)
// "入口全体"是分配时最终有效的全部入口,"分到的入口"是分配结果里每人分到的那个。
type Idea = { id: string; name: string; description: string };
const ideas = JSON.parse(readFileSync("eval/round-zero/ideas.json", "utf8")) as { key: Idea[]; deadEnds: Idea[] };
const out = process.argv[2];
if (out === undefined) throw new Error("要写检验目录,研究在它下面的 study 里");
const records = readLedger(join(out, "study"));
const decision = entryDecision(records);
if (decision === undefined) throw new Error("账本里还没有第 0 轮的入口分配决定,第零轮没跑完");
const round0 = stages(records).filter((s) => s.round === 0);
const final = entriesIn(records, new Set(round0.map((s) => s.stageId)));
const draft = entriesIn(records, new Set(round0.filter((s) => s.stage === "entryDraft").map((s) => s.stageId)));
// 自己按编号规则算出的入口全体,要和 flow 分配时用的那份一致;对不上说明 flow 或这里有一边取错了。
const decided = decision.input.entries;
if (JSON.stringify(decided.map((e) => e.id).sort()) !== JSON.stringify(final.map((e) => e.id).sort())) {
throw new Error(`分配决定里的入口 ${decided.map((e) => e.id).join("、")} 和从提交算出的入口全体 ${final.map((e) => e.id).join("、")} 对不上`);
}
const assignment = decision.output;
const assigned = assignment.map((a) => final.find((e) => e.id === a.entry)!);
// ---------------------------------------------------------------------------
// 问 Jev:同样的材料只问一次;每次请求的材料、问题、原始概率都记下
// ---------------------------------------------------------------------------
const raw: Array<{ site: string; ids: string[]; answers: Answer[] }> = [];
const cache = new Map<string, Promise<Answer[]>>();
/** 同时最多几个请求在路上:一下发上百个容易被限流。 */
const PARALLEL = 6;
let running = 0;
const waiting: Array<() => void> = [];
async function limited<T>(fn: () => Promise<T>): Promise<T> {
if (running >= PARALLEL) await new Promise<void>((ok) => waiting.push(ok));
running++;
try {
return await fn();
} finally {
running--;
waiting.shift()?.();
}
}
const show = (e: Entry) => ({ name: e.name, body: e.body });
/** 同样的材料、同样的问题只问一次(写入口阶段没换的入口在讨论后还是同一段文字)。 */
function query(site: string, ids: string[], state: Record<string, unknown>, questions: Record<string, Noul>): Promise<Answer[]> {
const key = JSON.stringify([site, state]);
if (!cache.has(key)) {
cache.set(key, limited(() => ask(state, questions)).then((answers) => (raw.push({ site, ids, answers }), answers)));
}
return cache.get(key)!;
}
const yes = async (p: Promise<Answer[]>, question: string) => (await p).find((a) => a.question === question)!.yes;
const ideaState = (e: Entry, idea: Idea) => ({ task: TASKS.idea, entry: show(e), idea: { description: idea.description } });
const leadsTo = (e: Entry, idea: Idea) => yes(query("leads", [e.id, idea.id], ideaState(e, idea), LEADS_TO_IDEA), "leads_to_idea");
const staysIn = (e: Entry, idea: Idea) => yes(query("stays", [e.id, idea.id], ideaState(e, idea), STOPS_AT_DEAD_END), "stays_within_idea");
const entryOnly = (e: Entry, question: string) => yes(query("entry", [e.id], { task: TASKS.entry, entry: show(e) }, ENTRY_ONLY), question);
const sameKind = (a: Entry, b: Entry) =>
yes(query("pair", [a.id, b.id], { task: TASKS.pair, entry_a: show(a), entry_b: show(b) }, SAME_KIND), "same_kind_of_change");
async function coverage(list: Entry[]): Promise<Record<string, string[]>> {
const out: Record<string, string[]> = {};
for (const idea of ideas.key) {
const hits = await Promise.all(list.map((e) => leadsTo(e, idea)));
out[idea.id] = list.filter((_, i) => hits[i]).map((e) => e.id);
}
return out;
}
async function overlap(list: Entry[]): Promise<{ pairs: number; same: string[][]; ratio: number | null }> {
const pairs = list.flatMap((a, i) => list.slice(i + 1).filter((b) => b.author !== a.author).map((b) => [a, b] as const));
const hits = await Promise.all(pairs.map(([a, b]) => sameKind(a, b)));
const same = pairs.filter((_, i) => hits[i]).map(([a, b]) => [a.id, b.id]);
return { pairs: pairs.length, same, ratio: pairs.length === 0 ? null : same.length / pairs.length };
}
// ---------------------------------------------------------------------------
// 六问
// ---------------------------------------------------------------------------
const k1 = ideas.key.find((i) => i.id === "K1")!;
const covFinal = await coverage(final);
const q1 = { coverage: covFinal, pass: covFinal["K1"]!.length > 0 };
const assignedPairs = assigned.flatMap((a, i) => assigned.slice(i + 1).map((b) => [a, b] as const));
const pairHits = await Promise.all(assignedPairs.map(([a, b]) => sameKind(a, b)));
const q2 = { same: assignedPairs.filter((_, i) => pairHits[i]).map(([a, b]) => [a.id, b.id]), pass: !pairHits.some(Boolean) };
const assignedK1 = await Promise.all(assigned.map((e) => leadsTo(e, k1)));
const deadEnd = await Promise.all(
assigned.map(async (e) => {
const hits = await Promise.all(ideas.deadEnds.map((d) => staysIn(e, d)));
return ideas.deadEnds.filter((_, i) => hits[i]).map((d) => d.id);
}),
);
const q3 = {
k1: assigned.filter((_, i) => assignedK1[i]).map((e) => e.id),
deadEnds: Object.fromEntries(assigned.map((e, i) => [e.id, deadEnd[i]])),
pass: assignedK1.some(Boolean) && deadEnd.every((d) => d.length === 0),
};
const perEntry = async (list: Entry[], question: string) => (await Promise.all(list.map((e) => entryOnly(e, question))));
const changesCode = await perEntry(assigned, "changes_code");
const q4 = { notCodeRoute: assigned.filter((_, i) => !changesCode[i]).map((e) => e.id), pass: changesCode.every(Boolean) };
const writesAnswer = async (list: Entry[]) => {
const [code, score] = [await perEntry(list, "names_code_change"), await perEntry(list, "states_expected_score")];
return list.filter((_, i) => code[i] || score[i]).map((e) => e.id);
};
const q5 = { assignedWritingAnswer: await writesAnswer(assigned), allWritingAnswer: await writesAnswer(final) };
const q5pass = q5.assignedWritingAnswer.length === 0;
const q6 = {
beforeDiscussion: { overlap: await overlap(draft), coverage: await coverage(draft) },
afterDiscussion: { overlap: await overlap(final), coverage: covFinal },
};
const report = {
study: join(out, "study"),
thresholdNote: THRESHOLD_NOTE,
entries: final.map((e) => ({ id: e.id, author: e.author, name: e.name })),
draftEntries: draft.map((e) => ({ id: e.id, author: e.author, name: e.name })),
assignment: assignment.map((a, i) => ({ ...a, name: assigned[i]!.name })),
results: { q1, q2, q3, q4, q5: { ...q5, pass: q5pass }, q6 },
passed: { q1: q1.pass, q2: q2.pass, q3: q3.pass, q4: q4.pass, q5: q5pass },
raw,
};
writeFileSync(join(out, "quality.json"), `${JSON.stringify(report, null, 2)}\n`);
const mark = (b: boolean) => (b ? "通过" : "不通过");
const ratio = (o: { pairs: number; same: string[][]; ratio: number | null }) => `${o.same.length}/${o.pairs}${o.ratio === null ? "" : `(${(o.ratio * 100).toFixed(0)}%)`}`;
const count = (c: Record<string, string[]>) => Object.entries(c).map(([k, v]) => `${k}:${v.length}`).join(" ");
console.log(`(${THRESHOLD_NOTE})`);
console.log(`入口全体:${final.map((e) => `${e.id} ${e.name}`).join("\n ")}`);
console.log(`分到的入口:${report.assignment.map((a) => `${a.researcher} → ${a.entry}(第 ${a.rank} 志愿)${a.name}`).join("\n ")}`);
console.log(`1 方向对不对:${mark(q1.pass)};覆盖 ${count(covFinal)}`);
console.log(`2 分没分开:${mark(q2.pass)};判"是"的对 ${JSON.stringify(q2.same)}`);
console.log(`3 好方向分出去没有:${mark(q3.pass)};指向 K1 的 ${JSON.stringify(q3.k1)},停在死路的 ${JSON.stringify(q3.deadEnds)}`);
console.log(`4 改代码的路线:${mark(q4.pass)};不是的 ${JSON.stringify(q4.notCodeRoute)}`);
console.log(`5 有没有写答案:${mark(q5pass)};分到的里写了的 ${JSON.stringify(q5.assignedWritingAnswer)},入口全体里写了的 ${JSON.stringify(q5.allWritingAnswer)}`);
console.log(`6 讨论有没有用:讨论前重合 ${ratio(q6.beforeDiscussion.overlap)}、覆盖 ${count(q6.beforeDiscussion.coverage)};讨论后重合 ${ratio(q6.afterDiscussion.overlap)}、覆盖 ${count(covFinal)}`);
console.log(`原始概率:${join(out, "quality.json")}`);
+176
View File
@@ -0,0 +1,176 @@
import type { Noul } from "../lib/jev.ts";
// 第零轮质量评估问 Jev 的问题(任务书 12 第 5.2 节"问 Jev 的规矩"):英文、一问只问一件事、
// 带材料说明、判"是"判"否"的说明和反例;写法照 src/jev/questions.ts。这些是检验自己的问题,不进 src/jev。
// 任务书里两处一句话里有两个条件(第 3 问"D1 或 D2"、第 5 问"写出改成什么,或者预计分数"),按"一问一事"拆成两问,
// 在 quality.ts 里用"任一判是"合起来。
const TASK =
"Round zero of a study that improves a program for the circle packing task: place 26 non-overlapping circles inside the unit square " +
"so that the sum of their radii is as large as possible (no tolerance: any overlap or circle outside the square is invalid). " +
"The baseline program puts the circles on fixed rings around the center and then shrinks radii pair by pair to remove overlaps; " +
"it scores about 0.96 and the best known score is about 2.636. " +
"In round zero each researcher writes entries: starting points for a line of work that one researcher will follow in the next rounds. " +
"An entry has a `name` and a `body` (in Chinese) saying where to start looking and why the route is worth walking. " +
"Entries are meant to name a starting point only, not the finished code change or an expected score.";
export const TASKS = {
idea: `${TASK} \`idea\` is a technique from the literature on this task, described in English.`,
entry: TASK,
pair: `${TASK} \`entry_a\` and \`entry_b\` were written by two different researchers.`,
};
/** 第 1 问:按这个入口走,会直接走到这个想法吗(idea 是 K1–K5 之一)。 */
export const LEADS_TO_IDEA: Record<string, Noul> = {
leads_to_idea: {
type: "noul",
instructions: {
question: "Will a researcher who follows `entry` arrive directly at the technique described in `idea.description`?",
focus:
"Read `entry.body` for what the researcher will look at first and what they intend to try or measure. " +
"Answer yes if the entry names this technique, or if this technique is the natural first code change on the route the entry points to. " +
"The entry may describe the technique in other words, in Chinese. Sharing the goal of a higher score, or reading the same function, is not enough.",
},
criteria: {
true: {
what: "The entry names this technique, or the route it points to leads straight to this technique as its first code change",
examples: [
"idea: optimize centers and radii together with a constrained optimizer; entry: 把 26 个圆心和半径一起交给 scipy 的约束优化器",
"idea: solve a linear program for the radii with fixed centers; entry: 先问给定圆心下半径和的最大值怎么算,它是不是一个线性规划",
],
},
false: {
what: "The entry heads somewhere else: a different technique, only reading rules, or only measuring, so this technique would not be its next step",
not_for: "Entries that only share the goal of raising the score, or only read the code this technique would change, without heading for the technique",
examples: [
"idea: optimize centers and radii together with a constrained optimizer; entry: 只读评测脚本,弄清容差和合法化规则",
"idea: solve a linear program for the radii with fixed centers; entry: 把三环布局换成六角网格",
],
},
},
},
};
/** 第 3 问后半:这个入口的路线是不是只停在这条死路上(idea 是 D1 或 D2 之一)。 */
export const STOPS_AT_DEAD_END: Record<string, Noul> = {
stays_within_idea: {
type: "noul",
instructions: {
question: "Does the whole route in `entry` stay within the approach described in `idea.description`, with no plan to go beyond it?",
focus:
"Answer yes only if everything the entry plans to change falls inside this approach. " +
"If the entry also plans a continuous optimization that moves the circle centers, or uses the approach only as a helper step " +
"(a starting layout, or a way to score layouts inside a larger search), answer no.",
},
criteria: {
true: {
what: "Everything the entry plans to change is inside this approach; it is the whole route",
examples: [
"idea: hand-designed placement of centers only; entry: 把三环布局换成六角网格,调行数和间距",
"idea: change only the radii with fixed centers; entry: 圆心不动,只改半径的分配方法,这就是整条路线",
],
},
false: {
what: "The entry goes beyond this approach, or uses it only as one step of a larger route",
not_for: "Entries that use this approach as a starting layout or as a scoring helper inside a search that moves the centers",
examples: [
"idea: hand-designed placement of centers only; entry: 从六角起点出发,再用约束优化同时移动圆心和半径",
"idea: change only the radii with fixed centers; entry: 先用精确的半径求解当打分器,再在外层搜索圆心位置",
],
},
},
},
};
/** 第 4、5 问:只看一个入口。第 5 问拆成两问。 */
export const ENTRY_ONLY: Record<string, Noul> = {
changes_code: {
type: "noul",
instructions: {
question: "Is `entry` a route that changes the program's code to raise the score, rather than one that only reads rules, measures, or analyzes?",
focus:
"A route may begin by reading code or measuring something, as long as the reading or measuring is meant to lead to a code change that raises the score. " +
"Answer no only if the entry's purpose ends at understanding, measuring, or checking rules, with no plan to change the code.",
},
criteria: {
true: {
what: "The entry intends to change the program, possibly after a first step of reading or measuring",
examples: ["先读半径的计算方法,量它离最优差多少,然后把它换掉", "从错排起点出发,跑一次约束优化"],
},
false: {
what: "The entry only reads rules, measures, or analyzes, and does not plan any change to the code",
not_for: "Entries that begin with a diagnosis but clearly intend to change the code afterwards",
examples: ["只读评测脚本,列出评测接受和拒绝哪些解", "统计基线产物里有多少个圆贴着边,写成一份报告"],
},
},
},
names_code_change: {
type: "noul",
instructions: {
question: "Does `entry` spell out the finished code change to make, such as the exact algorithm with its settings or the replacement code, instead of only a starting point?",
focus:
"Entries are meant to say where to start looking and why. Mentioning a technique as a question to explore, or as a direction to look in, is a starting point. " +
"Stating the change to make with the method and its settings already decided spells out the answer.",
},
criteria: {
true: {
what: "The entry states what the code should be changed into, with the method and its settings decided",
examples: [
"把布局函数换成 SLSQP,同时优化 78 个变量,起点用 5 行错排,迭代 500 次",
"半径计算改成用 scipy linprog 求解,约束是 r_i + r_j ≤ d_ij 和到四边的距离",
],
},
false: {
what: "The entry names where to look and what to question, leaving the change itself to be worked out",
not_for: "Entries that name a technique only as a question to check or a direction to explore",
examples: ["先读半径的计算方法,问它离给定圆心下的最优半径和差多少", "从基线的布局出发,看哪些圆被挤到了边上"],
},
},
},
states_expected_score: {
type: "noul",
instructions: {
question: "Does `entry` state a score, or an improvement of the score, that following this route is expected to reach?",
focus:
"Look for predictions of the result of this route. Quoting the baseline score, the best known score, or measured facts about the current program is not a prediction.",
},
criteria: {
true: {
what: "The entry predicts the score or the gain that this route will bring",
examples: ["这条路预计能到 2.6 以上", "改完能提升约 0.5"],
},
false: {
what: "The entry predicts no score or gain for this route",
not_for: "Quoting the current score, the best known score, or the size of the current gap without predicting what the route reaches",
examples: ["基线现在是 0.96,已知最好是 2.636", "现在离已知最好还差 1.68"],
},
},
},
};
/** 第 2、6 问:两位研究者从这两个入口出发,多半会做同一类代码改动吗。 */
export const SAME_KIND: Record<string, Noul> = {
same_kind_of_change: {
type: "noul",
instructions: {
question: "Would two researchers, one starting from `entry_a` and one from `entry_b`, most likely make the same kind of code change?",
focus:
"Imagine each researcher follows their entry and writes one code change. Compare which part of the program they would change and by what technique. " +
"Different wording, a different first file to read, or a different motivation does not make the changes different if both would change the same thing in the same way.",
},
criteria: {
true: {
what: "Both would most likely change the same part of the program with the same technique",
examples: [
"a: 用约束优化器同时调整圆心和半径; b: 把 78 个变量一起交给 SLSQP",
"a: 换成六角网格布局; b: 把圆心改成错排的行,调行数",
],
},
false: {
what: "They would most likely change different parts of the program, or the same part with a different technique",
not_for: "Entries that merely share the goal of a higher score, or read the same function, but would change different things",
examples: ["a: 圆心固定,用线性规划求半径; b: 用约束优化器同时调整圆心和半径", "a: 换成六角网格布局; b: 只读评测规则,弄清容差"],
},
},
},
};
File diff suppressed because one or more lines are too long
+51
View File
@@ -0,0 +1,51 @@
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { entriesIn, entryDecision, ofKind, readLedger, sameAssignment, stages, type Assignment } from "../lib/ledger.ts";
import { replayModels, startReplay, type ReplayScript } from "../lib/replay.ts";
import { launch, newEvalDir, writeConfig } from "../lib/study.ts";
// 把参考项目 16-real-1 的第零轮原样交给新 flow 回放一遍(任务书 12 第 5.3 节):基线照样用真 Docker 跑,
// 三位研究者由回放假模型按当年的先后交提交。跑完核对分配是否和当年完全相同,列出被退回的提交和没交出去的提交。
// node eval/round-zero/replay.ts <名字>
// 研究在 <检验目录>/study;之后用 quality.ts 打分。
type Script = ReplayScript & { expected: Assignment[]; expectedEntries: string[] };
const name = process.argv[2];
if (name === undefined) throw new Error("要写检验名字,例如 r0-replay-16");
const script = JSON.parse(readFileSync("eval/round-zero/replay-16-real-1.json", "utf8")) as Script;
const out = newEvalDir(name);
const replay = await startReplay(script, join(out, "pi-agent"), 0);
const researchers = Object.keys(script.models).sort();
const config = writeConfig("eval/round-zero/research.json", join(out, "research.json"), name, replayModels(researchers, "jev-latest"));
const studyDir = join(out, "study");
const { outcome } = await launch(config, studyDir, replay.env).done;
await replay.close();
const records = readLedger(studyDir);
const decision = entryDecision(records);
const got = decision === undefined ? null : decision.output;
const entries = entriesIn(records, new Set(stages(records).filter((s) => s.round === 0).map((s) => s.stageId))).map((e) => e.id);
// 入口全体按编号比(账本里按各人最后一版清单的先后排,当年的清单按编号排);分配按研究者、入口、志愿三样比,不看对象里键的先后。
const sameEntries = JSON.stringify([...entries].sort()) === JSON.stringify([...script.expectedEntries].sort());
const same = got !== null && sameAssignment(got, script.expected);
const report = {
outcome,
assignment: got,
expected: script.expected,
sameAsReference: same,
entries,
expectedEntries: script.expectedEntries,
sameEntriesAsReference: sameEntries,
rejections: ofKind(records, "rejection").map((r) => ({ seq: r.seq, stageId: r.content.stageId, author: r.content.author, kind: r.content.kind, reason: r.content.reason })),
delivered: replay.delivered,
notDelivered: replay.remaining(),
};
writeFileSync(join(out, "replay-report.json"), `${JSON.stringify(report, null, 2)}\n`);
console.log(`\n结局:${JSON.stringify(outcome)}`);
console.log(`分配:${got === null ? "没有写出分配决定" : got.map((a) => `${a.researcher} → ${a.entry}(第 ${a.rank} 志愿)`).join(";")}`);
console.log(`当年:${script.expected.map((a) => `${a.researcher} → ${a.entry}(第 ${a.rank} 志愿)`).join(";")}`);
console.log(`分配和当年相同:${same ? "是" : "否"};入口全体和当年相同:${sameEntries ? "是" : "否"}(${entries.join("、")})`);
console.log(`退回 ${report.rejections.length} 份${report.rejections.map((r) => `\n ${r.author} ${r.stageId} ${r.kind}:${r.reason}`).join("")}`);
console.log(`没交出去的:${JSON.stringify(report.notDelivered)}`);
console.log(`报告:${join(out, "replay-report.json")};研究目录:${studyDir}`);
+33
View File
@@ -0,0 +1,33 @@
{
"studyId": "r0-eval",
"task": "tasks/circle_packing",
"models": {
"researchers": [
{ "provider": "iomgaa", "model": "glm-5.3", "thinking": "medium" },
{ "provider": "iomgaa", "model": "glm-5.3", "thinking": "medium" },
{ "provider": "iomgaa", "model": "glm-5.3", "thinking": "medium" }
],
"experimenter": { "provider": "iomgaa", "model": "glm-5.3", "thinking": "medium" },
"verifier": { "provider": "iomgaa", "model": "glm-5.3", "thinking": "medium" },
"jev": "jev-latest"
},
"proposalLimit": 1,
"batchLimit": 2,
"runs": 13,
"factorLimit": 3,
"maxRepairs": 1,
"lightChecks": { "maxCount": 2, "seconds": 5 },
"promptDir": "prompts",
"devEntry": false,
"flow": {
"preset": "full",
"roundZero": { "enabled": true, "discussions": 2 },
"firstPrediction": true,
"critique": true,
"ablation": { "enabled": true, "cellRule": "valueMinusCost" },
"batchRule": "bestExpected",
"openReserve": 0.1,
"knowledgeUse": 0.5,
"decay": 0.5
}
}
+16
View File
@@ -0,0 +1,16 @@
import { join } from "node:path";
import { launch, newEvalDir, writeConfig } from "../lib/study.ts";
// 用真模型、真 Docker 跑一次第零轮(任务书 12 第 5.2 节"同样的配置真实跑 3 次"):配置照 eval/round-zero/research.json,
// 研究编号换成检验名字。研究在 <检验目录>/study,跑完用 quality.ts 打分。要花钱。
// node eval/round-zero/run.ts <名字>
// 中途按 Ctrl-C 会把停下信号传给研究进程;接着跑用 node eval/lib/cli.ts --resume --dir <检验目录>/study。
const name = process.argv[2];
if (name === undefined) throw new Error("要写检验名字,例如 r0-real-1");
const out = newEvalDir(name);
const config = writeConfig("eval/round-zero/research.json", join(out, "research.json"), name, null);
const study = launch(config, join(out, "study"), {});
process.on("SIGINT", () => study.child.kill("SIGINT"));
const { outcome } = await study.done;
console.log(`\n结局:${JSON.stringify(outcome)}\n研究目录:${join(out, "study")}`);