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 } | 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。", }; }