eval: 准备正向采纳的真实运行恢复与质量评估

新增单份串行启动入口、首个正向运行杀进程恢复证据、逐候选审阅导出与账目统计;参考只读副本分数对照。456项免费测试通过,尚未启动三份真实验收。同步结构说明与当前工作状态。
This commit is contained in:
2026-10-04 04:25:00 -04:00
parent bf5b3f2358
commit b7be524cdc
10 changed files with 1329 additions and 14 deletions
+259
View File
@@ -0,0 +1,259 @@
import { execFileSync } from "node:child_process";
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
import { join, resolve } from "node:path";
import { forwardAccounts, frozenConfig, jsonFile } from "../lib/forward.ts";
import { ofKind, readLedger } from "../lib/ledger.ts";
import { launch, sleep, waitForLedger, writeConfig } from "../lib/study.ts";
import { watchTimeline } from "../lib/timeline.ts";
// 任务书16:一次只启动一份;三次由父会话逐个执行。kill 只打首个正向运行,随后恢复同一研究一次。
// node eval/forward/run.ts <new|kill|resume> <研究编号> <绝对输出目录>
const [mode, name, output, ...extra] = process.argv.slice(2);
if (
!mode ||
!name ||
!output ||
extra.length ||
!["new", "kill", "resume"].includes(mode) ||
output !== resolve(output)
) {
throw new Error(
"用法:node eval/forward/run.ts <new|kill|resume> <研究编号> <绝对输出目录>",
);
}
const out = output;
const studyDir = join(out, "study");
if (mode === "resume") {
if (frozenConfig(readLedger(studyDir)).studyId !== name)
throw new Error("研究编号与账本不符");
} else {
if (existsSync(out))
throw new Error(`输出目录已存在,不覆盖现场:${out}`);
mkdirSync(out, { recursive: true });
}
const config =
mode === "resume"
? null
: writeConfig(
"eval/round-zero/research.json",
join(out, "research.json"),
name,
null,
);
const inspect = (container: string) =>
JSON.parse(
execFileSync("docker", ["inspect", container], {
encoding: "utf8",
}),
)[0] as {
Id: string;
Name: string;
Created: string;
State: {
Running: boolean;
StartedAt: string;
FinishedAt: string;
Status: string;
};
};
function observe(label: string) {
appendFileSync(
join(out, "machine.jsonl"),
`${JSON.stringify({
at: new Date().toISOString(),
label,
processes: execFileSync(
"ps",
["-eo", "pid,ppid,lstart,args"],
{ encoding: "utf8" },
),
containers: execFileSync(
"docker",
["ps", "--no-trunc"],
{ encoding: "utf8" },
),
})}\n`,
);
}
function start(path: string | null) {
observe(path === null ? "resume" : "start");
const study = launch(path, studyDir, {});
appendFileSync(
join(out, "launches.jsonl"),
`${JSON.stringify({ event: "start", pid: study.child.pid, at: new Date().toISOString(), config: path })}\n`,
);
const interrupt = () => study.child.kill("SIGINT");
process.on("SIGINT", interrupt);
study.done.then((end) => {
process.off("SIGINT", interrupt);
appendFileSync(
join(out, "launches.jsonl"),
`${JSON.stringify({ event: "end", pid: study.child.pid, at: new Date().toISOString(), ...end })}\n`,
);
});
return study;
}
let active = start(config);
try {
await waitForLedger(studyDir, active.child);
let watching = watchTimeline(
studyDir,
active.child,
join(out, "timeline.jsonl"),
);
if (mode === "kill") {
let hit = false;
while (
active.child.exitCode === null &&
active.child.signalCode === null
) {
const records = readLedger(studyDir);
const plan = ofKind(records, "runPlanned").find(
(r) =>
r.content.purpose === "forward" &&
r.content.round === 1,
);
if (plan !== undefined) {
const run = plan.content.run;
if (
ofKind(records, "runResult").some(
(r) => r.content.run === run,
)
)
throw new Error(
"首个正向运行已经结束,未打中;保留现场,不改杀别的候选",
);
const started = ofKind(
records,
"runStarted",
).find((r) => r.content.run === run);
if (started !== undefined) {
// 容器名是正式入口记录在结构文档里的约定,不 import flow 内部文件。
const container = `inforsi-${name}-${run}`;
const before = inspect(container);
jsonFile(
join(
out,
"container-before.json",
),
before,
);
if (!before.State.Running)
throw new Error(
"首个正向容器已不在运行,未打中",
);
if (
ofKind(
readLedger(studyDir),
"runResult",
).some(
(r) =>
r.content
.run ===
run,
)
)
throw new Error(
"杀前已有结果,未打中",
);
jsonFile(join(out, "kill.json"), {
run,
candidate: plan.content.object,
planSeq: plan.seq,
startSeq: started.seq,
beforeKillSeq:
records.at(-1)!.seq,
pid: active.child.pid,
at: new Date().toISOString(),
container,
});
if (!active.child.kill("SIGKILL"))
throw new Error(
"未发出 SIGKILL",
);
const killed = await active.done;
await watching;
if (killed.signal !== "SIGKILL")
throw new Error(
"控制进程不是以 SIGKILL 结束",
);
const atKill = readLedger(studyDir);
jsonFile(
join(
out,
"ledger-at-kill.json",
),
atKill,
);
if (
ofKind(
atKill,
"runResult",
).some(
(r) =>
r.content
.run ===
run,
)
)
throw new Error(
"杀后核对发现目标运行已经写结果,未打中;保留现场",
);
const afterKill = inspect(container);
jsonFile(
join(
out,
"container-after-kill.json",
),
afterKill,
);
observe("after-kill");
active = start(null);
const afterResume = inspect(container);
jsonFile(
join(
out,
"container-after-resume.json",
),
afterResume,
);
watching = watchTimeline(
studyDir,
active.child,
join(out, "timeline.jsonl"),
);
hit = true;
break;
}
}
await sleep(100);
}
if (!hit)
throw new Error(
`没有打中首个正向运行:${JSON.stringify(await active.done)}`,
);
}
await watching;
const end = await active.done;
jsonFile(join(out, "outcome.json"), end);
observe("end");
const accounts = forwardAccounts(readLedger(studyDir));
jsonFile(join(out, "accounts.json"), accounts);
// 杀进程报告由 stats 读原始容器观察和账本重算;即使外部服务失败后人工恢复,也不覆盖原杀进程证据。
if (end.code !== 0 || !Object.values(accounts.checks).every(Boolean)) {
throw new Error(
`没有完成采纳验收,保留现场;查明原因后才能手动 resume:${JSON.stringify(end)}`,
);
}
console.log(
`已到采纳后停点:${studyDir}。仍需 export、stats 和独立质量审阅。`,
);
} finally {
if (
active.child.exitCode === null &&
active.child.signalCode === null
) {
active.child.kill("SIGINT");
await active.done;
}
}