fix: 用容器生命周期核实恢复并单列三份总体验收

审阅发现spawn后快照早于恢复实际执行,不能证明没有重启;改用杀前至目标结果落账的Docker历史事件,保留缓存完整性限制。逐份stats不再冒称任务书总体通过;独立验收要求三份不同研究、成功杀进程及不重叠区间。两项缺陷附回归,7项定向测试与check通过,独立复审无新问题;真实B尚待启动。
This commit is contained in:
2026-10-04 04:52:13 -04:00
parent b7be524cdc
commit de8e13b031
5 changed files with 473 additions and 25 deletions
+22
View File
@@ -0,0 +1,22 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { jsonFile } from "../lib/forward.ts";
import {
forwardAcceptance,
type StudySummary,
} from "../lib/forward-evidence.ts";
// 显式总体程序验收:先对三份研究运行 stats,再用该统计目录检查任务书16的完整批次。
// node eval/forward/acceptance.ts <统计输出目录>
const [statsDir, ...extra] = process.argv.slice(2);
if (!statsDir || extra.length)
throw new Error("用法:node eval/forward/acceptance.ts <统计输出目录>");
const summaries = JSON.parse(
readFileSync(join(statsDir, "stats.json"), "utf8"),
) as StudySummary[];
const result = forwardAcceptance(summaries);
jsonFile(join(statsDir, "acceptance.json"), result);
console.log(
`任务书16总体程序验收${result.pass ? "通过" : "未通过"};详见 ${join(statsDir, "acceptance.json")};质量判断仍待独立阅读。`,
);
if (!result.pass) process.exitCode = 1;
+86 -5
View File
@@ -1,5 +1,5 @@
import { execFileSync } from "node:child_process";
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { forwardAccounts, frozenConfig, jsonFile } from "../lib/forward.ts";
import { ofKind, readLedger } from "../lib/ledger.ts";
@@ -40,6 +40,10 @@ const config =
name,
null,
);
const daemonTime = () =>
execFileSync("docker", ["info", "--format", "{{.SystemTime}}"], {
encoding: "utf8",
}).trim();
const inspect = (container: string) =>
JSON.parse(
execFileSync("docker", ["inspect", container], {
@@ -130,6 +134,8 @@ try {
if (started !== undefined) {
// 容器名是正式入口记录在结构文档里的约定,不 import flow 内部文件。
const container = `inforsi-${name}-${run}`;
// 从杀前划定历史事件区间,覆盖恢复启动到目标结果落账;不把 spawn 当成恢复已执行。
const lifecycleSince = daemonTime();
const before = inspect(container);
jsonFile(
join(
@@ -208,20 +214,95 @@ try {
afterKill,
);
observe("after-kill");
active = start(null);
const afterResume = inspect(container);
jsonFile(
join(
out,
"container-after-resume.json",
"container-before-resume.json",
),
afterResume,
inspect(container),
);
jsonFile(
join(
out,
"container-events-window.json",
),
{
since: lifecycleSince,
until: null,
run,
resultSeq: null,
},
);
active = start(null);
watching = watchTimeline(
studyDir,
active.child,
join(out, "timeline.jsonl"),
);
let targetResult;
while (true) {
targetResult = ofKind(
readLedger(studyDir),
"runResult",
).find(
(r) =>
r.content
.run ===
run,
);
if (
targetResult !==
undefined ||
active.child
.exitCode !==
null ||
active.child
.signalCode !==
null
)
break;
await sleep(100);
}
const lifecycleUntil = daemonTime();
// Docker CLI 按时间回读,不依赖事件订阅的启动时序;原文全存,stats 核对缓存上限和目标 die。
const events = execFileSync(
"docker",
[
"events",
"--since",
lifecycleSince,
"--until",
lifecycleUntil,
"--format",
"{{json .}}",
],
{ encoding: "utf8" },
);
writeFileSync(
join(
out,
"container-events.jsonl",
),
events,
);
jsonFile(
join(
out,
"container-events-window.json",
),
{
since: lifecycleSince,
until: lifecycleUntil,
run,
resultSeq:
targetResult?.seq ??
null,
},
);
if (targetResult === undefined)
throw new Error(
"恢复进程没有交出目标结果,生命周期证据不完整;保留现场",
);
hit = true;
break;
}
+37 -13
View File
@@ -12,6 +12,12 @@ import {
jsonFile,
scoreBand,
} from "../lib/forward.ts";
import {
sameContainerThroughResult,
type ContainerEvent,
type ContainerSnapshot,
type LifecycleWindow,
} from "../lib/forward-evidence.ts";
import { finalSubmissions, ofKind, readLedger } from "../lib/ledger.ts";
import { lightCheckUsage } from "../lib/light-checks.ts";
import { referenceScores } from "../lib/reference-scores.ts";
@@ -117,16 +123,32 @@ const summaries = dirs.map((dir) => {
if (existsSync(join(dir, "kill.json"))) {
const evidence = JSON.parse(
readFileSync(join(dir, "kill.json"), "utf8"),
) as { run: string; candidate: string; beforeKillSeq: number };
type Container = {
Id: string;
State: { StartedAt: string; Running: boolean };
) as {
run: string;
candidate: string;
beforeKillSeq: number;
container: string;
};
const snapshots = ["before", "after-kill", "after-resume"].map(
const snapshots = ["before", "after-kill", "before-resume"].map(
(part) =>
JSON.parse(
readFileSync(join(dir, `container-${part}.json`), "utf8"),
) as Container,
) as ContainerSnapshot,
);
const lifecycleWindow = JSON.parse(
readFileSync(join(dir, "container-events-window.json"), "utf8"),
) as LifecycleWindow;
const lifecycleEvents = readFileSync(
join(dir, "container-events.jsonl"),
"utf8",
)
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line) as ContainerEvent);
const lifecycleChecks = sameContainerThroughResult(
snapshots,
lifecycleEvents,
evidence.container,
);
const target = accounts.runs.find(
(r) => r.plan.content.run === evidence.run,
@@ -144,16 +166,18 @@ const summaries = dirs.map((dir) => {
evidence,
killSeq,
snapshots,
snapshotMeaning: "杀前、杀后、恢复启动前;没有把快照当成恢复完成的证据",
lifecycleWindow,
lifecycleChecks,
lifecycleEvents,
charged: target.starts.length,
resultSeq: target.results[0]?.seq ?? null,
checks: {
sameContainer:
snapshots[0]!.State.Running &&
snapshots.every(
(c) =>
c.Id === snapshots[0]!.Id &&
c.State.StartedAt === snapshots[0]!.State.StartedAt,
),
Object.values(lifecycleChecks).every(Boolean) &&
lifecycleWindow.run === evidence.run &&
lifecycleWindow.resultSeq === target.results[0]?.seq &&
Date.parse(lifecycleWindow.since) < Date.parse(lifecycleWindow.until),
chargedOnceNoRerun:
target.starts.length === 1 && candidatePlans.length === 1,
resultAndContinued:
@@ -211,6 +235,6 @@ jsonFile(join(output, "manual-review.json"), {
"核对各目录launches.jsonl、machine.jsonl和父会话观察;上一份全部结束后才起下一份;不宣称快照排除任意外部进程",
});
console.log(
`统计:${output};程序检查${summaries.every((s) => s.pass) ? "通过" : "未通过"};质量判断仍待独立阅读。`,
`统计:${output};所给 ${summaries.length} 份研究的逐研究检查${summaries.every((s) => s.pass) ? "通过" : "未通过"},不是任务书16总体验收;总体程序验收另运行 node eval/forward/acceptance.ts ${output};质量判断仍待独立阅读。`,
);
if (!summaries.every((s) => s.pass)) process.exitCode = 1;