b7be524cdc
新增单份串行启动入口、首个正向运行杀进程恢复证据、逐候选审阅导出与账目统计;参考只读副本分数对照。456项免费测试通过,尚未启动三份真实验收。同步结构说明与当前工作状态。
172 lines
5.9 KiB
TypeScript
172 lines
5.9 KiB
TypeScript
import { writeFileSync } from "node:fs";
|
||
import { checkConfig } from "../../src/config/index.ts";
|
||
import type { LedgerRecord } from "../../src/ledger/index.ts";
|
||
import {
|
||
Board,
|
||
FORWARD_NOT_RUN,
|
||
type StudyRecord,
|
||
} from "../../src/materials/index.ts";
|
||
import { placement, runBalance } from "../../src/rules/index.ts";
|
||
import { verificationOutcome } from "../../src/submissions/index.ts";
|
||
import type { Range } from "../../src/vocab/index.ts";
|
||
import { finalSubmissions, ofKind } from "./ledger.ts";
|
||
|
||
export const FORWARD_STOP = "消融还没写(任务书 17)";
|
||
export const jsonFile = (path: string, value: unknown): void =>
|
||
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
||
export const frozenConfig = (records: LedgerRecord[]) =>
|
||
checkConfig((ofKind(records, "config")[0]!.content as StudyRecord).config);
|
||
|
||
/** 区间边界只是报告口径,不参与研究决定;失败或测不到分数保持 null。 */
|
||
export function scoreBand(score: number | null, range: Range): string | null {
|
||
if (score === null) return null;
|
||
return {
|
||
belowQ1: "<q1",
|
||
q1ToQ5: "[q1,q5)",
|
||
q5ToQ9: "[q5,q9]",
|
||
aboveQ9: ">q9",
|
||
}[placement(range, score)];
|
||
}
|
||
|
||
/** 核验要对应当时那版代码,不能拿最终修好后的代码替代历史被审代码。 */
|
||
export function candidateHistory(records: LedgerRecord[], candidate: string) {
|
||
const code = ofKind(records, "code").filter(
|
||
(r) => r.content.object === candidate,
|
||
);
|
||
const submissions = ofKind(records, "submission").filter(
|
||
(r) => r.content.target === candidate,
|
||
);
|
||
const attempts = submissions.flatMap((r) => {
|
||
if (r.content.kind !== "implementation" && r.content.kind !== "repair")
|
||
return [];
|
||
// flow 在同一事务里先写 submission,紧接着写 code。
|
||
return [{ submission: r, code: code.find((c) => c.seq === r.seq + 1)! }];
|
||
});
|
||
const verifications = submissions.flatMap((r) =>
|
||
r.content.kind === "verification"
|
||
? [
|
||
{
|
||
submission: r,
|
||
code: code.filter((c) => c.seq < r.seq).at(-1)!,
|
||
outcome: verificationOutcome(r.content.content),
|
||
},
|
||
]
|
||
: [],
|
||
);
|
||
return { attempts, verifications };
|
||
}
|
||
|
||
/** 程序验收只读记录重算;基线也算正式运行,没启动的结果不扣次。 */
|
||
export function forwardAccounts(records: LedgerRecord[]) {
|
||
const board = new Board(records);
|
||
const config = frozenConfig(records);
|
||
const opens = ofKind(records, "stageOpen");
|
||
const closes = ofKind(records, "stageClose");
|
||
const stages = opens.map((r) => {
|
||
const closing = closes.filter(
|
||
(c) => c.content.stageId === r.content.stageId,
|
||
);
|
||
return {
|
||
...r.content,
|
||
openSeq: r.seq,
|
||
closeSeqs: closing.map((c) => c.seq),
|
||
pass: closing.length === 1 && closing[0]!.seq > r.seq,
|
||
};
|
||
});
|
||
const orphanCloses = closes.filter(
|
||
(c) => !opens.some((o) => o.content.stageId === c.content.stageId),
|
||
);
|
||
const started = ofKind(records, "runStarted");
|
||
const balance = runBalance(config.runs, board.runStates());
|
||
const runs = ofKind(records, "runPlanned").map((p) => ({
|
||
plan: p,
|
||
starts: started.filter((s) => s.content.run === p.content.run),
|
||
results: ofKind(records, "runResult").filter(
|
||
(r) => r.content.run === p.content.run,
|
||
),
|
||
}));
|
||
const batch = board.decision("batch", 1);
|
||
const finals = finalSubmissions(records);
|
||
const candidates = (batch?.output.chosen ?? []).map((candidate) => {
|
||
const history = candidateHistory(records, candidate);
|
||
const events = ofKind(records, "event").filter(
|
||
(r) =>
|
||
r.content.event === FORWARD_NOT_RUN &&
|
||
r.content.objects.includes(candidate),
|
||
);
|
||
const candidateRuns = runs.filter(
|
||
(r) =>
|
||
r.plan.content.purpose === "forward" &&
|
||
r.plan.content.round === 1 &&
|
||
r.plan.content.object === candidate,
|
||
);
|
||
const verificationCount = finals.filter(
|
||
(r) =>
|
||
r.content.target === candidate && r.content.kind === "verification",
|
||
).length;
|
||
const repairCount = finals.filter(
|
||
(r) => r.content.target === candidate && r.content.kind === "repair",
|
||
).length;
|
||
const lastPassed = history.verifications.at(-1)?.outcome.passed ?? null;
|
||
const exhausted = lastPassed === false && repairCount >= config.maxRepairs;
|
||
return {
|
||
candidate,
|
||
verificationCount,
|
||
repairCount,
|
||
lastPassed,
|
||
exhausted,
|
||
notRunEvents: events,
|
||
started: candidateRuns.some((r) => r.starts.length > 0),
|
||
runs: candidateRuns,
|
||
pass: exhausted
|
||
? events.length === 1 && candidateRuns.length === 0
|
||
: lastPassed === true &&
|
||
candidateRuns.some(
|
||
(r) => r.starts.length === 1 && r.results.length === 1,
|
||
),
|
||
};
|
||
});
|
||
const adoption =
|
||
ofKind(records, "decision").find(
|
||
(r) => r.content.decision === "adopt" && r.content.round === 1,
|
||
) ?? null;
|
||
const halt =
|
||
ofKind(records, "event")
|
||
.filter((r) => r.content.event === "halted")
|
||
.at(-1) ?? null;
|
||
const checks = {
|
||
stagesPaired: stages.every((s) => s.pass) && orphanCloses.length === 0,
|
||
chargedOnce:
|
||
balance.charged === started.length &&
|
||
config.runs - balance.balance === started.length &&
|
||
balance.reserved === 0,
|
||
forwardSerial: runs
|
||
.filter((r) => r.plan.content.purpose === "forward")
|
||
.every(
|
||
(r, i, forwards) =>
|
||
i === 0 ||
|
||
(forwards[i - 1]!.results.length === 1 &&
|
||
forwards[i - 1]!.results[0]!.seq < r.plan.seq),
|
||
),
|
||
runsFinished: runs.length > 0 && runs.every((r) => r.results.length === 1),
|
||
candidatesFinished:
|
||
candidates.length > 0 && candidates.every((c) => c.pass),
|
||
adoptedAndStopped:
|
||
adoption !== null &&
|
||
halt?.content.reason === FORWARD_STOP &&
|
||
halt.seq === records.at(-1)!.seq,
|
||
};
|
||
return {
|
||
initial: config.runs,
|
||
confirmedStarts: started.length,
|
||
balance,
|
||
runs,
|
||
candidates,
|
||
stages,
|
||
orphanCloses,
|
||
adoption,
|
||
halt,
|
||
checks,
|
||
};
|
||
}
|