de8e13b031
审阅发现spawn后快照早于恢复实际执行,不能证明没有重启;改用杀前至目标结果落账的Docker历史事件,保留缓存完整性限制。逐份stats不再冒称任务书总体通过;独立验收要求三份不同研究、成功杀进程及不重叠区间。两项缺陷附回归,7项定向测试与check通过,独立复审无新问题;真实B尚待启动。
126 lines
3.8 KiB
TypeScript
126 lines
3.8 KiB
TypeScript
import { readFileSync, realpathSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
export interface ContainerSnapshot {
|
|
Id: string;
|
|
State: { StartedAt: string; Running: boolean };
|
|
}
|
|
export interface ContainerEvent {
|
|
Type: string;
|
|
Action: string;
|
|
Actor: { ID: string; Attributes: { name: string } };
|
|
}
|
|
export interface LifecycleWindow {
|
|
since: string;
|
|
until: string;
|
|
run: string;
|
|
resultSeq: number;
|
|
}
|
|
|
|
/** 原始历史事件覆盖杀前到目标结果落账;spawn 后的快照本身不证明恢复没有重启。 */
|
|
export function sameContainerThroughResult(
|
|
snapshots: ContainerSnapshot[],
|
|
events: ContainerEvent[],
|
|
container: string,
|
|
) {
|
|
const original = snapshots[0]!;
|
|
const target = events.filter(
|
|
(e) =>
|
|
e.Type === "container" &&
|
|
(e.Actor.ID === original.Id || e.Actor.Attributes.name === container),
|
|
);
|
|
return {
|
|
snapshotsAgree:
|
|
original.State.Running &&
|
|
snapshots.every(
|
|
(s) =>
|
|
s.Id === original.Id &&
|
|
s.State.StartedAt === original.State.StartedAt,
|
|
),
|
|
// Docker 只保留最近 256 条事件;碰到上限不能据此声称整个区间没有重启。
|
|
historyWithinLimit: events.length < 256,
|
|
completionObserved: target.some(
|
|
(e) => e.Actor.ID === original.Id && e.Action === "die",
|
|
),
|
|
noRestartOrReplacement: target.every(
|
|
(e) =>
|
|
e.Actor.ID === original.Id &&
|
|
!["start", "restart", "create"].includes(e.Action),
|
|
),
|
|
};
|
|
}
|
|
|
|
export interface StudySummary {
|
|
dir: string;
|
|
study: string;
|
|
pass: boolean;
|
|
accounts: { checks: { adoptedAndStopped: boolean } };
|
|
kill: { checks: Record<string, boolean> } | null;
|
|
}
|
|
interface LaunchEvent {
|
|
event: "start" | "end";
|
|
pid: number;
|
|
at: string;
|
|
}
|
|
|
|
/** 任务书16的总体程序验收;逐研究 stats 通过不等于三份研究的总体验收通过。 */
|
|
export function forwardAcceptance(summaries: StudySummary[]) {
|
|
const intervals = summaries.map((s) => {
|
|
const launches = readFileSync(join(s.dir, "launches.jsonl"), "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line) => JSON.parse(line) as LaunchEvent);
|
|
const paired =
|
|
launches.length > 0 &&
|
|
launches.length % 2 === 0 &&
|
|
launches.every((e, i) => {
|
|
if (i % 2 === 1) return e.event === "end";
|
|
const end = launches[i + 1];
|
|
return (
|
|
e.event === "start" &&
|
|
end?.event === "end" &&
|
|
end.pid === e.pid &&
|
|
Date.parse(e.at) <= Date.parse(end.at) &&
|
|
(i === 0 || Date.parse(launches[i - 1]!.at) <= Date.parse(e.at))
|
|
);
|
|
});
|
|
return {
|
|
study: s.study,
|
|
dir: realpathSync(s.dir),
|
|
paired,
|
|
start: launches[0]?.at ?? null,
|
|
end: launches.at(-1)?.at ?? null,
|
|
};
|
|
});
|
|
const ordered = [...intervals].sort(
|
|
(a, b) => Date.parse(a.start!) - Date.parse(b.start!),
|
|
);
|
|
const checks = {
|
|
threeDifferentStudies:
|
|
summaries.length === 3 &&
|
|
new Set(summaries.map((s) => s.study)).size === 3 &&
|
|
new Set(intervals.map((s) => s.dir)).size === 3,
|
|
allCompletedAndPassed: summaries.every(
|
|
(s) => s.pass && s.accounts.checks.adoptedAndStopped,
|
|
),
|
|
successfulKill: summaries.some(
|
|
(s) => s.kill !== null && Object.values(s.kill.checks).every(Boolean),
|
|
),
|
|
launchIntervalsComplete: intervals.every((s) => s.paired),
|
|
studiesDoNotOverlap: ordered.every(
|
|
(s, i) =>
|
|
s.paired &&
|
|
(i === 0 || Date.parse(ordered[i - 1]!.end!) <= Date.parse(s.start!)),
|
|
),
|
|
};
|
|
return {
|
|
scope: "任务书16总体程序验收(不替代独立研究质量审阅)",
|
|
intervals,
|
|
checks,
|
|
pass: Object.values(checks).every(Boolean),
|
|
limit:
|
|
"启动区间含同份研究杀后恢复的间隔;只核对所给三份研究,不据此排除任意外部进程抢占CPU。",
|
|
};
|
|
}
|