eval: 准备正向采纳的真实运行恢复与质量评估
新增单份串行启动入口、首个正向运行杀进程恢复证据、逐候选审阅导出与账目统计;参考只读副本分数对照。456项免费测试通过,尚未启动三份真实验收。同步结构说明与当前工作状态。
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user