From fdda4f009b144852ffab2f4a6961868cf5b0e803 Mon Sep 17 00:00:00 2001 From: iomgaa Date: Mon, 5 Oct 2026 00:56:37 -0400 Subject: [PATCH] =?UTF-8?q?style:=20pi-lens=20=E5=AF=B9=20flow/vocab/tests?= =?UTF-8?q?=20=E7=9A=84=E5=BC=95=E5=8F=B7=E4=B8=8E=E6=8D=A2=E8=A1=8C?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E9=87=8D=E6=8E=92=EF=BC=88=E6=97=A0=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA=E5=8F=98=E5=8C=96=EF=BC=9Bcheck=20=E4=B8=8E=20477=20?= =?UTF-8?q?=E6=9D=A1=E6=B5=8B=E8=AF=95=E9=80=9A=E8=BF=87=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/flow/next-step.ts | 553 +++++++++++--- src/flow/steps/ablation.ts | 124 +++- src/vocab/submissions.ts | 87 ++- tests/flow-next-step.test.ts | 1335 +++++++++++++++++++++++++++++----- 4 files changed, 1767 insertions(+), 332 deletions(-) diff --git a/src/flow/next-step.ts b/src/flow/next-step.ts index 696d05f..14a387e 100644 --- a/src/flow/next-step.ts +++ b/src/flow/next-step.ts @@ -1,9 +1,30 @@ import type { RoundStepId, StudyStepId } from "../config/index.ts"; import type { Content, LedgerRecord } from "../ledger/index.ts"; -import { Board, CELL_NOT_RUN, cellId, DECISION_KINDS, experimenterId, factorKey, FORWARD_NOT_RUN, ORIGINAL_BASELINE, researcherId, splitCell, verifierId } from "../materials/index.ts"; -import { compareIds, listCells, runBalance, sharedSettings } from "../rules/index.ts"; +import { + Board, + CELL_NOT_RUN, + cellId, + DECISION_KINDS, + experimenterId, + factorKey, + FORWARD_NOT_RUN, + ORIGINAL_BASELINE, + researcherId, + splitCell, + verifierId, +} from "../materials/index.ts"; +import { + compareIds, + listCells, + runBalance, + sharedSettings, +} from "../rules/index.ts"; import { ablationOutcome, verificationOutcome } from "../submissions/index.ts"; -import { SUBMISSION_KINDS, type Stage, type SubmissionKind } from "../vocab/index.ts"; +import { + SUBMISSION_KINDS, + type Stage, + type SubmissionKind, +} from "../vocab/index.ts"; import { readFrozen, type Frozen } from "./frozen.ts"; // "下一步做什么"(docs/结构.md 第二部分"几条边界"):步骤的先后只写在这里。 @@ -19,14 +40,20 @@ const ROUND_ZERO_STAGES = { discussion1: { stage: "discussion", kind: "discussion" }, discussion2: { stage: "discussion", kind: "discussion" }, claim: { stage: "claim", kind: "claim" }, -} as const satisfies Partial>; +} as const satisfies Partial< + Record +>; type RoundZeroStep = keyof typeof ROUND_ZERO_STAGES; /** 第 r 轮的研究者阶段:步骤名就是阶段名(vocab STAGES)。 */ -type RoundStageStep = Extract; +type RoundStageStep = Extract< + RoundStepId, + "proposal" | "firstPrediction" | "critique" | "response" | "secondPrediction" +>; /** 阶段编号由 flow 起:r<轮>-<步骤>,一个步骤一个阶段。第 r 轮按配置跳过的步骤也用这个编号记"按配置跳过"。 */ -export const stageIdOf = (round: number, step: string): string => `r${round}-${step}`; +export const stageIdOf = (round: number, step: string): string => + `r${round}-${step}`; /** 基线运行编号:没启动就起下一个编号重跑(docs/结构.md 第四部分)。 */ export const baselineRunId = (n: number): string => `base-${n}`; @@ -41,14 +68,24 @@ export const EVENTS = { } as const; /** 一个任务:一个人对一个对象交一种提交;提案和第零轮任务没有对象。 */ -export type TaskRef = { actor: string; kind: SubmissionKind; object: string | null }; +export type TaskRef = { + actor: string; + kind: SubmissionKind; + object: string | null; +}; export type Next = | { do: "freezeConfig" } | { do: "saveBaseline" } | { do: "baselineRun"; run: string } | { do: "skip"; step: string; reason: string } - | { do: "openStage"; stageId: string; stage: Stage; round: number; roster: string[] } + | { + do: "openStage"; + stageId: string; + stage: Stage; + round: number; + roster: string[]; + } | { do: "closeStage"; stageId: string } | { do: "dispatch"; stageId: string; tasks: TaskRef[] } | { do: "assignEntries" } @@ -72,7 +109,11 @@ export function nextStep(records: readonly LedgerRecord[]): Next { const view = new View(records); for (const step of frozen.steps.study) { const next = - step.status === "skip" ? skipNext(step.id, step.reason, view) : step.id === "rounds" ? roundsNext(view, frozen) : studyStepNext(step.id, view, frozen); + step.status === "skip" + ? skipNext(step.id, step.reason, view) + : step.id === "rounds" + ? roundsNext(view, frozen) + : studyStepNext(step.id, view, frozen); if (next !== null) return next; } throw new Error("步骤清单走完了却没有停下:export 一步应当停下"); @@ -80,11 +121,17 @@ export function nextStep(records: readonly LedgerRecord[]): Next { /** 按配置跳过的步骤 → 一条"按配置跳过"事件,写明步骤和原因。 */ function skipNext(step: string, reason: string, view: View): Next | null { - return view.events(EVENTS.skipped).some((e) => e.objects[0] === step) ? null : { do: "skip", step, reason }; + return view.events(EVENTS.skipped).some((e) => e.objects[0] === step) + ? null + : { do: "skip", step, reason }; } /** 整份研究的一个要执行的步骤还要做什么;做完了交回 null。 */ -function studyStepNext(step: Exclude, view: View, frozen: Frozen): Next | null { +function studyStepNext( + step: Exclude, + view: View, + frozen: Frozen, +): Next | null { switch (step) { case "freezeConfig": return null; @@ -95,11 +142,21 @@ function studyStepNext(step: Exclude, view: View, frozen: case "discussion2": case "claim": { const { stage, kind } = ROUND_ZERO_STAGES[step]; - return stageNext(stageIdOf(0, step), stage, 0, (roster) => roster.map((actor) => ({ actor, kind, object: null })), view, frozen, researchers(frozen)); + return stageNext( + stageIdOf(0, step), + stage, + 0, + (roster) => roster.map((actor) => ({ actor, kind, object: null })), + view, + frozen, + researchers(frozen), + ); } case "assignEntries": // 分配 → 第 0 轮的入口分配决定(decision,种类 entries)。 - return view.board.decision("entries", 0) === undefined ? { do: "assignEntries" } : null; + return view.board.decision("entries", 0) === undefined + ? { do: "assignEntries" } + : null; case "export": // 余额用完、一轮接一轮走完后走到这里:导出还没写,停下(任务书 17 第 2.1 节)。 return { do: "halt", reason: "导出还没写", objects: [] }; @@ -116,9 +173,16 @@ function roundsNext(view: View, frozen: Frozen): Next | null { // 余额为 0 不开【下一轮】;已经开始的轮次照常走完——最后一次正向用掉全部余额时,这一轮的采纳、 // 消融(各格记“没测:剩余次数不够”)、收尾与解封照样做(方法.md 第七部分“什么时候开下一轮”)。 // 一轮在账本里留下的第一处痕迹是阶段记录(或按配置跳过事件、决定记录),此前这一轮还不算开始。 - if (!roundStarted(view, round) && runBalance(frozen.config.runs, view.board.runStates()).balance === 0) return null; + if ( + !roundStarted(view, round) && + runBalance(frozen.config.runs, view.board.runStates()).balance === 0 + ) + return null; for (const step of frozen.steps.round) { - const next = step.status === "skip" ? skipNext(stageIdOf(round, step.id), step.reason, view) : roundStepNext(step.id, round, view, frozen); + const next = + step.status === "skip" + ? skipNext(stageIdOf(round, step.id), step.reason, view) + : roundStepNext(step.id, round, view, frozen); if (next !== null) return next; } } @@ -127,13 +191,29 @@ function roundsNext(view: View, frozen: Frozen): Next | null { /** 第 round 轮开始了没有:看账本里有没有这一轮的阶段、跳过事件或决定。 */ function roundStarted(view: View, round: number): boolean { const prefix = `r${round}-`; - if ([...view.board.stages.keys()].some((id) => id.startsWith(prefix))) return true; - if (DECISION_KINDS.some((kind) => view.board.decision(kind, round) !== undefined)) return true; - return view.events(EVENTS.skipped).some((e) => typeof e.objects[0] === "string" && e.objects[0].startsWith(prefix)); + if ([...view.board.stages.keys()].some((id) => id.startsWith(prefix))) + return true; + if ( + DECISION_KINDS.some( + (kind) => view.board.decision(kind, round) !== undefined, + ) + ) + return true; + return view + .events(EVENTS.skipped) + .some( + (e) => + typeof e.objects[0] === "string" && e.objects[0].startsWith(prefix), + ); } /** 第 round 轮的一个要执行的步骤还要做什么;做完了交回 null。 */ -function roundStepNext(step: RoundStepId, round: number, view: View, frozen: Frozen): Next | null { +function roundStepNext( + step: RoundStepId, + round: number, + view: View, + frozen: Frozen, +): Next | null { switch (step) { case "openRound": case "carryOverPool": @@ -144,25 +224,55 @@ function roundStepNext(step: RoundStepId, round: number, view: View, frozen: Fro case "critique": case "response": case "secondPrediction": - return stageNext(stageIdOf(round, step), step, round, (roster) => roundTasks(step, round, roster, view.board), view, frozen, researchers(frozen)); + return stageNext( + stageIdOf(round, step), + step, + round, + (roster) => roundTasks(step, round, roster, view.board), + view, + frozen, + researchers(frozen), + ); case "dedupe": // 判重合并 → 本轮的判重合并决定(decision,种类 dedup)。 - return view.board.decision("dedup", round) === undefined ? { do: "dedupe", round } : null; + return view.board.decision("dedup", round) === undefined + ? { do: "dedupe", round } + : null; case "freezeProtocol": { // 冻结协议 → 本轮每个新候选都有冻结协议记录(protocol)。全部候选在同一个事务里写; // 这一步排在回应阶段关闭之后,materials 把冻结协议的出现当作"已公开",早写会泄露。 - if (frozen.steps.round.some((s) => s.id === "response" && s.status === "skip")) { - return { do: "halt", reason: "配置关掉了挑刺与回应时怎么冻结协议还没写:submissions 的 protocolBody 只收回应和统一", objects: [] }; + if ( + frozen.steps.round.some( + (s) => s.id === "response" && s.status === "skip", + ) + ) { + return { + do: "halt", + reason: + "配置关掉了挑刺与回应时怎么冻结协议还没写:submissions 的 protocolBody 只收回应和统一", + objects: [], + }; } const frozenIds = new Set(view.board.protocols().map((p) => p.candidate)); - return freshOf(view.board, round).every((c) => frozenIds.has(c.candidate)) ? null : { do: "freezeProtocols", round }; + return freshOf(view.board, round).every((c) => frozenIds.has(c.candidate)) + ? null + : { do: "freezeProtocols", round }; } case "selectBatch": // 选批 → 本轮的选批决定(decision,种类 batch)。 - return view.board.decision("batch", round) === undefined ? { do: "selectBatch", round } : null; + return view.board.decision("batch", round) === undefined + ? { do: "selectBatch", round } + : null; case "implementation": { const candidates = view.board.decision("batch", round)!.output.chosen; - return assignedStage("implementation", round, "implementation", candidates, view, frozen); + return assignedStage( + "implementation", + round, + "implementation", + candidates, + view, + frozen, + ); } case "verification": case "repair": @@ -174,42 +284,94 @@ function roundStepNext(step: RoundStepId, round: number, view: View, frozen: Fro // 不另写记录:整批运行已结束。采纳决定使结果整批公开,信息账从预测和结果现算。 return null; case "adopt": - return view.board.decision("adopt", round) === undefined ? { do: "adopt", round } : null; + return view.board.decision("adopt", round) === undefined + ? { do: "adopt", round } + : null; case "listCells": // 不写记录:格从采纳决定和冻结协议现算(方法.md 4.3 ⑫),格编号用 materials 的 cellId。 return null; case "explanation": { // ⑭:这一轮有消融(采纳了、且被采纳改法有两个以上因子)才开解释阶段;否则直接到收尾(方法.md 4.3 ⑫)。 // 解释 → 这一轮解释阶段的关闭记录。 - const adopted = view.board.decision("adopt", round)?.output.adopted ?? null; - if (adopted === null || view.board.protocol(adopted).factors.length < 2) return null; - if (frozen.steps.round.some((s) => s.id === "listCells" && s.status === "skip")) { - return { do: "halt", reason: "配置关掉了消融、又采纳了多因子改法:不交三个数的解释还没写(cells 只按完整流程全开实现,做步骤开关时再补)", objects: [] }; + const adopted = + view.board.decision("adopt", round)?.output.adopted ?? null; + if (adopted === null || view.board.protocol(adopted).factors.length < 2) + return null; + if ( + frozen.steps.round.some( + (s) => s.id === "listCells" && s.status === "skip", + ) + ) { + return { + do: "halt", + reason: + "配置关掉了消融、又采纳了多因子改法:不交三个数的解释还没写(cells 只按完整流程全开实现,做步骤开关时再补)", + objects: [], + }; } - return stageNext(stageIdOf(round, "explanation"), "explanation", round, (roster) => roster.map((actor) => ({ actor, kind: "explanation" as const, object: null })), view, frozen, researchers(frozen)); + return stageNext( + stageIdOf(round, "explanation"), + "explanation", + round, + (roster) => + roster.map((actor) => ({ + actor, + kind: "explanation" as const, + object: null, + })), + view, + frozen, + researchers(frozen), + ); } case "selectCells": // ⑮ → 本轮的选格决定(decision,种类 cells)。没选中的格不另写事件:原因都在这条决定里。 if (ablationOf(view.board, round) === null) return null; - return view.board.decision("cells", round) === undefined ? { do: "selectCells", round } : null; + return view.board.decision("cells", round) === undefined + ? { do: "selectCells", round } + : null; case "ablationImplementation": case "ablationVerification": // ⑯ → 每个选中的格:交了"跑不起来",或最近一次核验通过,或重做次数用完、有"不跑"事件。 - return ablationOf(view.board, round) === null ? null : ablationCellsNext(round, view, frozen); + return ablationOf(view.board, round) === null + ? null + : ablationCellsNext(round, view, frozen); case "ablationRun": // ⑯ → 每个通过核验的格,最近一次消融运行有结果,而且不是"没启动"。 - return ablationOf(view.board, round) === null ? null : ablationRunNext(round, view); + return ablationOf(view.board, round) === null + ? null + : ablationRunNext(round, view); case "settle": // ⑰ → 本轮的结算记录(settlement),以及同一事务里的押的因子决定(decision,种类 bet)。 if (ablationOf(view.board, round) === null) return null; - return view.board.settlement(round) === undefined || view.board.decision("bet", round) === undefined ? { do: "settle", round } : null; + return view.board.settlement(round) === undefined || + view.board.decision("bet", round) === undefined + ? { do: "settle", round } + : null; case "wrapUp": { // ⑱ → 收尾阶段的关闭记录,以及本轮选中的每个候选都有解封记录。 - const stage = stageNext(stageIdOf(round, "wrapUp"), "wrapUp", round, (roster) => roster.map((actor) => ({ actor, kind: "wrapUp" as const, object: null })), view, frozen, researchers(frozen)); + const stage = stageNext( + stageIdOf(round, "wrapUp"), + "wrapUp", + round, + (roster) => + roster.map((actor) => ({ + actor, + kind: "wrapUp" as const, + object: null, + })), + view, + frozen, + researchers(frozen), + ); if (stage !== null) return stage; // 轮末解封(方法.md 4.3 ⑱):只在收尾阶段关闭之后写,提前写会泄露。本轮全部解封写在同一个事务里。 const chosen = view.board.decision("batch", round)!.output.chosen; - return chosen.every((c) => view.of("unseal").some((u) => u.candidate === c)) ? null : { do: "unsealChosen", round }; + return chosen.every((c) => + view.of("unseal").some((u) => u.candidate === c), + ) + ? null + : { do: "unsealChosen", round }; } default: throw new Error(`第 ${round} 轮的 ${step} 没有对应的下一步写法`); @@ -217,13 +379,19 @@ function roundStepNext(step: RoundStepId, round: number, view: View, frozen: Fro } /** 第 round 轮判重合并出的新候选,以及各合并了哪几条原提案。 */ -export function freshOf(board: Board, round: number): Array<{ candidate: string; proposals: string[] }> { +export function freshOf( + board: Board, + round: number, +): Array<{ candidate: string; proposals: string[] }> { return board.decision("dedup", round)!.output.fresh; } /** 第 round 轮的候选池:新候选加留池候选(方法.md 4.3 ①⑦)。第二次预测和选批都用这一份。 */ export function poolOf(board: Board, round: number): string[] { - return [...freshOf(board, round).map((c) => c.candidate), ...board.carried(round)]; + return [ + ...freshOf(board, round).map((c) => c.candidate), + ...board.carried(round), + ]; } /** @@ -234,17 +402,34 @@ export function poolOf(board: Board, round: number): string[] { * - 回应与统一:每个新候选一个任务,一条原提案的由作者交回应,几条合成的由其中编号最小的作者交统一(方法.md 4.3 ③⑥); * - 第二次预测:每人 × 候选池里每个候选。 */ -function roundTasks(step: RoundStageStep, round: number, roster: readonly string[], board: Board): TaskRef[] { +function roundTasks( + step: RoundStageStep, + round: number, + roster: readonly string[], + board: Board, +): TaskRef[] { const each = (kind: TaskRef["kind"], objects: readonly string[]): TaskRef[] => - roster.flatMap((actor) => objects.map((object) => ({ actor, kind, object }))); + roster.flatMap((actor) => + objects.map((object) => ({ actor, kind, object })), + ); switch (step) { case "proposal": return roster.map((actor) => ({ actor, kind: "proposal", object: null })); case "firstPrediction": - return each("firstPrediction", freshOf(board, round).map((c) => c.candidate)); + return each( + "firstPrediction", + freshOf(board, round).map((c) => c.candidate), + ); case "critique": { - const proposals = board.published("proposal").filter((p) => p.round === round).sort((a, b) => compareIds(a.item!, b.item!)); - return roster.flatMap((actor) => proposals.filter((p) => p.author !== actor).map((p) => ({ actor, kind: "critique" as const, object: p.item! }))); + const proposals = board + .published("proposal") + .filter((p) => p.round === round) + .sort((a, b) => compareIds(a.item!, b.item!)); + return roster.flatMap((actor) => + proposals + .filter((p) => p.author !== actor) + .map((p) => ({ actor, kind: "critique" as const, object: p.item! })), + ); } case "response": return freshOf(board, round).map((c) => ({ @@ -262,16 +447,28 @@ function roundTasks(step: RoundStageStep, round: number, roster: readonly string * 没启动的起新编号重跑;判为不算数的停下(方法.md 4.1)。 */ function baselineNext(view: View): Next | null { - if (!view.of("code").some((c) => c.object === ORIGINAL_BASELINE)) return { do: "saveBaseline" }; + if (!view.of("code").some((c) => c.object === ORIGINAL_BASELINE)) + return { do: "saveBaseline" }; const plans = view.of("runPlanned").filter((p) => p.purpose === "baseline"); const last = plans.at(-1); if (last === undefined) return { do: "baselineRun", run: baselineRunId(1) }; const result = view.of("runResult").find((r) => r.run === last.run); - if (result === undefined) throw new Error(`基线运行 ${last.run} 登记了还没有结果:恢复入口应当已经核对过`); - if (result.process.ending === "notStarted") return { do: "baselineRun", run: baselineRunId(plans.length + 1) }; + if (result === undefined) + throw new Error( + `基线运行 ${last.run} 登记了还没有结果:恢复入口应当已经核对过`, + ); + if (result.process.ending === "notStarted") + return { do: "baselineRun", run: baselineRunId(plans.length + 1) }; if (result.judged?.verdict === "valid") return null; - const why = result.judged === null ? result.process.ending : result.judged.verdictReason; - return { do: "halt", reason: `基线运行 ${last.run} 判为不算数(${why}),不能当基线`, objects: [last.run] }; + const why = + result.judged === null + ? result.process.ending + : result.judged.verdictReason; + return { + do: "halt", + reason: `基线运行 ${last.run} 判为不算数(${why}),不能当基线`, + objects: [last.run], + }; } /** @@ -300,7 +497,8 @@ function stageNext( for (const t of todo) { const last = view.lastCall(stageId, t); // 没有调用、被用户停下、出错(含恢复时补写的"进程中断")都派;没交的只在开发入口开着时派(方法.md 第九部分"开发入口")。 - if (last?.status === "notSubmitted" && !frozen.config.devEntry) stuck.push({ call: last.call, reason: last.reason }); + if (last?.status === "notSubmitted" && !frozen.config.devEntry) + stuck.push({ call: last.call, reason: last.reason }); else tasks.push(t); } if (tasks.length > 0) return { do: "dispatch", stageId, tasks }; @@ -312,44 +510,95 @@ function stageNext( } /** 研究者阶段的冻结名单。实验员、核验员阶段则按本阶段的任务列名单。 */ -const researchers = (frozen: Frozen): string[] => frozen.config.models.researchers.map((_, i) => researcherId(i)); +const researchers = (frozen: Frozen): string[] => + frozen.config.models.researchers.map((_, i) => researcherId(i)); /** 正向的实现、修复、核验共用阶段开关和派发规则,每个候选固定一个人。 */ function assignedStage( step: string, round: number, - kind: "implementation" | "repair" | "verification" | "ablationImplementation" | "ablationVerification", + kind: + | "implementation" + | "repair" + | "verification" + | "ablationImplementation" + | "ablationVerification", candidates: readonly string[], view: View, frozen: Frozen, ): Next | null { - const actorOf = SUBMISSION_KINDS[kind].role === "verifier" ? verifierId : experimenterId; - const tasks = candidates.map((object) => ({ actor: actorOf(object), kind, object })); - return stageNext(stageIdOf(round, step), SUBMISSION_KINDS[kind].stage, round, () => tasks, view, frozen, tasks.map((t) => t.actor)); + const actorOf = + SUBMISSION_KINDS[kind].role === "verifier" ? verifierId : experimenterId; + const tasks = candidates.map((object) => ({ + actor: actorOf(object), + kind, + object, + })); + return stageNext( + stageIdOf(round, step), + SUBMISSION_KINDS[kind].stage, + round, + () => tasks, + view, + frozen, + tasks.map((t) => t.actor), + ); } /** * ⑨:实现全部关闭后开核验;核验全部关闭后,失败的才修复。每次只重验上次修复的候选。 * 第几次由已开关的阶段推出来;次数用完先写不跑事件,不登记运行(方法.md 4.3 ⑨)。 */ -function verificationNext(round: number, view: View, frozen: Frozen): Next | null { +function verificationNext( + round: number, + view: View, + frozen: Frozen, +): Next | null { let candidates = view.board.decision("batch", round)!.output.chosen; for (let n = 1; candidates.length > 0; n++) { const verificationId = stageIdOf(round, `verification-${n}`); - const verify = assignedStage(`verification-${n}`, round, "verification", candidates, view, frozen); + const verify = assignedStage( + `verification-${n}`, + round, + "verification", + candidates, + view, + frozen, + ); if (verify !== null) return verify; - const failed = view.board.published("verification") - .filter((s) => s.stageId === verificationId && !verificationOutcome(s.content).passed); + const failed = view.board + .published("verification") + .filter( + (s) => + s.stageId === verificationId && + !verificationOutcome(s.content).passed, + ); if (n > frozen.config.maxRepairs) { for (const s of failed) { - if (view.events(FORWARD_NOT_RUN).some((e) => e.objects.includes(s.target!))) continue; - return { do: "forwardNotRun", candidate: s.target!, reason: `第 ${n} 次核验仍不通过,修复次数上限 ${frozen.config.maxRepairs} 已用完:${JSON.stringify(verificationOutcome(s.content))};${s.content.body}` }; + if ( + view + .events(FORWARD_NOT_RUN) + .some((e) => e.objects.includes(s.target!)) + ) + continue; + return { + do: "forwardNotRun", + candidate: s.target!, + reason: `第 ${n} 次核验仍不通过,修复次数上限 ${frozen.config.maxRepairs} 已用完:${JSON.stringify(verificationOutcome(s.content))};${s.content.body}`, + }; } return null; } candidates = failed.map((s) => s.target!); if (candidates.length === 0) return null; - const repair = assignedStage(`repair-${n}`, round, "repair", candidates, view, frozen); + const repair = assignedStage( + `repair-${n}`, + round, + "repair", + candidates, + view, + frozen, + ); if (repair !== null) return repair; } return null; @@ -358,12 +607,29 @@ function verificationNext(round: number, view: View, frozen: Frozen): Next | nul /** ⑩:按选批顺序一次只交回一个运行;没启动的下次启动起新编号,不会把半批提前公开。 */ function forwardNext(round: number, view: View): Next | null { for (const candidate of view.board.decision("batch", round)!.output.chosen) { - if (view.events(FORWARD_NOT_RUN).some((e) => e.objects.includes(candidate))) continue; - const runs = view.board.runs().filter((r) => r.plan.purpose === "forward" && r.plan.round === round && r.plan.object === candidate); + if (view.events(FORWARD_NOT_RUN).some((e) => e.objects.includes(candidate))) + continue; + const runs = view.board + .runs() + .filter( + (r) => + r.plan.purpose === "forward" && + r.plan.round === round && + r.plan.object === candidate, + ); const last = runs.at(-1); - if (last !== undefined && last.result === null) throw new Error(`正向运行 ${last.plan.run} 登记了还没有结果:恢复入口应当已经核对过`); - if (last !== undefined && last.result!.process.ending !== "notStarted") continue; - return { do: "forwardRun", round, candidate, run: `r${round}-forward-${candidate}-${runs.length + 1}` }; + if (last !== undefined && last.result === null) + throw new Error( + `正向运行 ${last.plan.run} 登记了还没有结果:恢复入口应当已经核对过`, + ); + if (last !== undefined && last.result!.process.ending !== "notStarted") + continue; + return { + do: "forwardRun", + round, + candidate, + run: `r${round}-forward-${candidate}-${runs.length + 1}`, + }; } return null; } @@ -372,40 +638,74 @@ function forwardNext(round: number, view: View): Next | null { * 这一轮做不做消融、消融哪个候选的哪几个因子(方法.md 4.3 ⑫):采纳了、且被采纳改法有两个以上因子。 * 没采纳或只采纳了单因子改法时 ⑬–⑰ 都不做:这几步不写记录,从采纳决定和冻结协议就算得出。 */ -export function ablationOf(board: Board, round: number): { candidate: string; keys: string[] } | null { +export function ablationOf( + board: Board, + round: number, +): { candidate: string; keys: string[] } | null { const adopted = board.decision("adopt", round)?.output.adopted ?? null; if (adopted === null) return null; - const keys = listCells(board.protocol(adopted).factors.map((_, i) => factorKey(i))); + const keys = listCells( + board.protocol(adopted).factors.map((_, i) => factorKey(i)), + ); return keys.length < 2 ? null : { candidate: adopted, keys }; } /** 选格决定选中要测的格(对象编号用 materials 的 cellId),按因子编号排。 */ export function chosenCells(board: Board, round: number): string[] { const { candidate } = ablationOf(board, round)!; - return board.decision("cells", round)!.output.chosen.map((key) => cellId(candidate, key)); + return board + .decision("cells", round)! + .output.chosen.map((key) => cellId(candidate, key)); } /** 一格最近一次已公开的消融实现提交(阶段关闭即公开);没有为 undefined。 */ export function lastAblationImpl(board: Board, cell: string) { - return board.published("ablationImplementation").filter((s) => s.target === cell).at(-1); + return board + .published("ablationImplementation") + .filter((s) => s.target === cell) + .at(-1); } /** 选中的格里"活着"的(没交"跑不起来"、没有"不跑"事件),以及每格实际关掉的全部因子(指定的加实验员报的一起关的)。 */ -export function aliveCells(board: Board, events: readonly Content<"event">[], round: number): Array<{ cell: string; off: string[] }> { +export function aliveCells( + board: Board, + events: readonly Content<"event">[], + round: number, +): Array<{ cell: string; off: string[] }> { return chosenCells(board, round) .map((cell) => ({ cell, impl: lastAblationImpl(board, cell) })) - .filter((x) => x.impl !== undefined && x.impl.content.cannotRun === null && !events.some((e) => e.event === CELL_NOT_RUN && e.objects.includes(x.cell))) - .map((x) => ({ cell: x.cell, off: [splitCell(x.cell).key, ...x.impl!.content.alsoOff] })); + .filter( + (x) => + x.impl !== undefined && + x.impl.content.cannotRun === null && + !events.some( + (e) => e.event === CELL_NOT_RUN && e.objects.includes(x.cell), + ), + ) + .map((x) => ({ + cell: x.cell, + off: [splitCell(x.cell).key, ...x.impl!.content.alsoOff], + })); } /** * 要跑消融运行的格:活着的格里同设置的第一格(方法.md 6.6"一起关":几格实际关掉的因子完全相同就是同一种设置, * 沿用第一格的结果,不重跑、不重复扣次、不重复记账)。 */ -export function cellsToRun(board: Board, events: readonly Content<"event">[], round: number): string[] { +export function cellsToRun( + board: Board, + events: readonly Content<"event">[], + round: number, +): string[] { const alive = aliveCells(board, events, round); - const sameAs = new Map(sharedSettings(alive.map((x) => ({ factor: splitCell(x.cell).key, off: x.off }))).map((x) => [x.factor, x.sameAs])); - return alive.filter((x) => sameAs.get(splitCell(x.cell).key) === null).map((x) => x.cell); + const sameAs = new Map( + sharedSettings( + alive.map((x) => ({ factor: splitCell(x.cell).key, off: x.off })), + ).map((x) => [x.factor, x.sameAs]), + ); + return alive + .filter((x) => sameAs.get(splitCell(x.cell).key) === null) + .map((x) => x.cell); } /** @@ -413,26 +713,60 @@ export function cellsToRun(board: Board, events: readonly Content<"event">[], ro * 从它上一次交的这一格代码接着改,每次只重验刚重做的格。阶段编号按"第几次"排,第几次从已开关的阶段数出来,不另存。 * 交了"跑不起来"的格不核验、不运行、不扣次;核验不通过、次数用完写"不跑"事件,不登记运行(方法.md 4.3 ⑯、6.6)。 */ -function ablationCellsNext(round: number, view: View, frozen: Frozen): Next | null { - let cells = chosenCells(view.board, round).filter((cell) => !view.events(CELL_NOT_RUN).some((e) => e.objects.includes(cell))); +function ablationCellsNext( + round: number, + view: View, + frozen: Frozen, +): Next | null { + let cells = chosenCells(view.board, round).filter( + (cell) => !view.events(CELL_NOT_RUN).some((e) => e.objects.includes(cell)), + ); for (let n = 1; cells.length > 0; n++) { - const impl = assignedStage(`ablationImplementation-${n}`, round, "ablationImplementation", cells, view, frozen); + const impl = assignedStage( + `ablationImplementation-${n}`, + round, + "ablationImplementation", + cells, + view, + frozen, + ); if (impl !== null) return impl; - const runnable = cells.filter((cell) => lastAblationImpl(view.board, cell)!.content.cannotRun === null); + const runnable = cells.filter( + (cell) => lastAblationImpl(view.board, cell)!.content.cannotRun === null, + ); const failed: Array<{ cell: string; reason: string }> = []; if (runnable.length > 0) { - const ver = assignedStage(`ablationVerification-${n}`, round, "ablationVerification", runnable, view, frozen); + const ver = assignedStage( + `ablationVerification-${n}`, + round, + "ablationVerification", + runnable, + view, + frozen, + ); if (ver !== null) return ver; - for (const s of view.board.published("ablationVerification").filter((x) => x.stageId === stageIdOf(round, `ablationVerification-${n}`))) { - const outcome = ablationOutcome(s.content, splitCell(s.target!).key, lastAblationImpl(view.board, s.target!)!.content.alsoOff); + for (const s of view.board + .published("ablationVerification") + .filter( + (x) => x.stageId === stageIdOf(round, `ablationVerification-${n}`), + )) { + const outcome = ablationOutcome( + s.content, + splitCell(s.target!).key, + lastAblationImpl(view.board, s.target!)!.content.alsoOff, + ); if (!outcome.passed) { - failed.push({ cell: s.target!, reason: `第 ${n} 次核验仍不通过,修复次数上限 ${frozen.config.maxRepairs} 已用完:${JSON.stringify({ leftOn: outcome.leftOn, wronglyOff: outcome.wronglyOff })}` }); + failed.push({ + cell: s.target!, + reason: `第 ${n} 次核验仍不通过,修复次数上限 ${frozen.config.maxRepairs} 已用完:${JSON.stringify({ leftOn: outcome.leftOn, wronglyOff: outcome.wronglyOff })}`, + }); } } } if (n > frozen.config.maxRepairs) { for (const f of failed) { - if (view.events(CELL_NOT_RUN).some((e) => e.objects.includes(f.cell))) continue; + if (view.events(CELL_NOT_RUN).some((e) => e.objects.includes(f.cell))) + continue; return { do: "cellNotRun", cell: f.cell, reason: f.reason }; } return null; @@ -446,11 +780,27 @@ function ablationCellsNext(round: number, view: View, frozen: Frozen): Next | nu /** ⑯ 消融运行:按格编号顺序一次只交回一个运行(一份研究里的消融运行一个接一个跑,同正向);没启动的下次启动起新编号。 */ function ablationRunNext(round: number, view: View): Next | null { for (const cell of cellsToRun(view.board, view.of("event"), round)) { - const runs = view.board.runs().filter((r) => r.plan.purpose === "ablation" && r.plan.round === round && r.plan.object === cell); + const runs = view.board + .runs() + .filter( + (r) => + r.plan.purpose === "ablation" && + r.plan.round === round && + r.plan.object === cell, + ); const last = runs.at(-1); - if (last !== undefined && last.result === null) throw new Error(`消融运行 ${last.plan.run} 登记了还没有结果:恢复入口应当已经核对过`); - if (last !== undefined && last.result!.process.ending !== "notStarted") continue; - return { do: "ablationRun", round, cell, run: `r${round}-ablation-${cell}-${runs.length + 1}` }; + if (last !== undefined && last.result === null) + throw new Error( + `消融运行 ${last.plan.run} 登记了还没有结果:恢复入口应当已经核对过`, + ); + if (last !== undefined && last.result!.process.ending !== "notStarted") + continue; + return { + do: "ablationRun", + round, + cell, + run: `r${round}-ablation-${cell}-${runs.length + 1}`, + }; } return null; } @@ -466,7 +816,9 @@ class View { } of(kind: K): Array> { - return (this.#records.filter((r) => r.kind === kind) as Array>).map((r) => r.content); + return ( + this.#records.filter((r) => r.kind === kind) as Array> + ).map((r) => r.content); } events(event: string): Array> { @@ -475,7 +827,10 @@ class View { /** 一个任务做完了:这个人在这个阶段对这个对象有一份被收下的提交(任务书 14 第 2.3 节)。 */ done(stageId: string, t: TaskRef): boolean { - return this.of("submission").some((s) => s.stageId === stageId && s.author === t.actor && s.target === t.object); + return this.of("submission").some( + (s) => + s.stageId === stageId && s.author === t.actor && s.target === t.object, + ); } /** @@ -484,11 +839,19 @@ class View { */ lastCall(stageId: string, t: TaskRef): Content<"callEnded"> | null { const started = this.of("callStarted") - .filter((c) => c.stageId === stageId && c.actor === t.actor && (c.materials as { object: string | null }).object === t.object) + .filter( + (c) => + c.stageId === stageId && + c.actor === t.actor && + (c.materials as { object: string | null }).object === t.object, + ) .at(-1); if (started === undefined) return null; const ended = this.of("callEnded").find((e) => e.call === started.call); - if (ended === undefined) throw new Error(`模型调用 ${started.call} 开始了还没有结束记录:恢复入口应当已经补写`); + if (ended === undefined) + throw new Error( + `模型调用 ${started.call} 开始了还没有结束记录:恢复入口应当已经补写`, + ); return ended; } } diff --git a/src/flow/steps/ablation.ts b/src/flow/steps/ablation.ts index 4514bea..9aeaf7d 100644 --- a/src/flow/steps/ablation.ts +++ b/src/flow/steps/ablation.ts @@ -1,5 +1,13 @@ import type { Content } from "../../ledger/index.ts"; -import { Board, cellId, CELL_NOT_RUN, explanationId, protocolMetrics, splitCell, validScore } from "../../materials/index.ts"; +import { + Board, + cellId, + CELL_NOT_RUN, + explanationId, + protocolMetrics, + splitCell, + validScore, +} from "../../materials/index.ts"; import { chooseBet, decisionSeed, @@ -30,7 +38,11 @@ import type { Study } from "../study.ts"; export function selectCells(s: Study, round: number): void { const board = new Board(s.ledger.records()); const { config, steps } = s.frozen; - const seed = decisionSeed({ studyId: config.studyId, round, version: SEED_VERSIONS.cells }); + const seed = decisionSeed({ + studyId: config.studyId, + round, + version: SEED_VERSIONS.cells, + }); const input: CellRuleInput = { ...modelInput(board, s, round), balance: runBalance(config.runs, board.runStates()).balance, @@ -39,7 +51,8 @@ export function selectCells(s: Study, round: number): void { decay: config.flow.decay, seed, }; - const rule = steps.round.find((x) => x.id === "selectCells")!.rule!.name as CellRuleName; + const rule = steps.round.find((x) => x.id === "selectCells")!.rule! + .name as CellRuleName; const { rule: used, ...output } = runCellRule(rule, input); // SAFETY: rules 是纯计算,输入输出都是纯 Json 值;账本在写入时校验 Json。 s.ledger.append("decision", { @@ -55,14 +68,30 @@ export function selectCells(s: Study, round: number): void { } /** ⑯ 消融运行:用核验过的这一格快照,复用正式运行的登记、扣次、判分入口。完整代码和基线的分数取本批已有的运行结果,不重跑。 */ -export function ablationRun(s: Study, round: number, cell: string, run: string): Promise { +export function ablationRun( + s: Study, + round: number, + cell: string, + run: string, +): Promise { const board = new Board(s.ledger.records()); - return formalRun(s, { run, purpose: "ablation", round, object: cell }, board.code(cell).version, ablationMetrics(board, splitCell(cell).candidate)); + return formalRun( + s, + { run, purpose: "ablation", round, object: cell }, + board.code(cell).version, + ablationMetrics(board, splitCell(cell).candidate), + ); } /** 消融格正式运行登记的指标:照被采纳候选的冻结协议取;消融格里假设专用指标缺失一律只记缺失、不作废这一格(方法.md 第五部分"指标")。 */ -export function ablationMetrics(board: Board, candidate: string): HypothesisMetric[] { - return protocolMetrics(board, board.protocol(candidate)).map((m) => ({ ...m, missing: "recordMissing" })); +export function ablationMetrics( + board: Board, + candidate: string, +): HypothesisMetric[] { + return protocolMetrics(board, board.protocol(candidate)).map((m) => ({ + ...m, + missing: "recordMissing", + })); } /** @@ -73,21 +102,37 @@ export function ablationMetrics(board: Board, candidate: string): HypothesisMetr export function settle(s: Study, round: number): void { const board = new Board(s.ledger.records()); const { candidate, keys } = ablationOf(board, round)!; - const events = s.ledger.records().flatMap((r) => (r.kind === "event" ? [r.content] : [])); + const events = s.ledger + .records() + .flatMap((r) => (r.kind === "event" ? [r.content] : [])); const model = modelInput(board, s, round); - const input: SettleInput = { ...model, cells: keys.map((key) => ({ factor: key, outcome: outcomeOf(board, events, round, candidate, key) })) }; + const input: SettleInput = { + ...model, + cells: keys.map((key) => ({ + factor: key, + outcome: outcomeOf(board, events, round, candidate, key), + })), + }; const output = settleAblation(input); const betInput = { ...model, cells: input.cells }; const bet = chooseBet(betInput); // SAFETY: rules 是纯计算,结算和决定的输入输出都是纯 Json 值;账本在写入时校验 Json。 s.ledger.transaction(() => { - s.ledger.append("settlement", { round, result: { candidate, input, output } as unknown as Json }); + s.ledger.append("settlement", { + round, + result: { candidate, input, output } as unknown as Json, + }); s.ledger.append("decision", { decision: "bet", round, rule: "chooseBet", ruleVersion: 1, - params: { direction: model.direction, epsilon: model.epsilon, knownBest: model.knownBest, lambda: model.lambda }, + params: { + direction: model.direction, + epsilon: model.epsilon, + knownBest: model.knownBest, + lambda: model.lambda, + }, seed: null, input: betInput as unknown as Json, output: bet as unknown as Json, @@ -96,19 +141,40 @@ export function settle(s: Study, round: number): void { } /** 一格的结局(方法.md 6.6"一格的几种结局"):没测、跑不起来、证据不足、一起关、有效分数。 */ -function outcomeOf(board: Board, events: readonly Content<"event">[], round: number, candidate: string, key: string): CellOutcome { - if (!board.decision("cells", round)!.output.chosen.includes(key)) return { kind: "untested" }; +function outcomeOf( + board: Board, + events: readonly Content<"event">[], + round: number, + candidate: string, + key: string, +): CellOutcome { + if (!board.decision("cells", round)!.output.chosen.includes(key)) + return { kind: "untested" }; const cell = cellId(candidate, key); const impl = lastAblationImpl(board, cell)!; if (impl.content.cannotRun !== null) return { kind: "cannotRun" }; - if (events.some((e) => e.event === CELL_NOT_RUN && e.objects.includes(cell))) return { kind: "insufficient" }; + if (events.some((e) => e.event === CELL_NOT_RUN && e.objects.includes(cell))) + return { kind: "insufficient" }; const off = [key, ...impl.content.alsoOff]; // 几格实际关掉的因子完全相同时,沿用同设置第一格的运行结果(不重跑,方法.md 6.6"一起关")。 const alive = aliveCells(board, events, round); - const sameAs = new Map(sharedSettings(alive.map((x) => ({ factor: splitCell(x.cell).key, off: x.off }))).map((x) => [x.factor, x.sameAs])); + const sameAs = new Map( + sharedSettings( + alive.map((x) => ({ factor: splitCell(x.cell).key, off: x.off })), + ).map((x) => [x.factor, x.sameAs]), + ); const first = sameAs.get(key); - const ranCell = first === null || first === undefined ? cell : cellId(candidate, first); - const run = board.runs().filter((r) => r.plan.purpose === "ablation" && r.plan.round === round && r.plan.object === ranCell).at(-1)!; + const ranCell = + first === null || first === undefined ? cell : cellId(candidate, first); + const run = board + .runs() + .filter( + (r) => + r.plan.purpose === "ablation" && + r.plan.round === round && + r.plan.object === ranCell, + ) + .at(-1)!; const score = validScore(run.result!.judged); if (off.length > 1) return { kind: "joint", off, score }; return score === null ? { kind: "insufficient" } : { kind: "valid", score }; @@ -125,11 +191,21 @@ export function unsealChosen(s: Study, round: number): void { const chosen = board.decision("batch", round)!.output.chosen; s.ledger.transaction(() => { for (const candidate of chosen) { - if (records.some((r) => r.kind === "unseal" && r.content.candidate === candidate)) continue; + if ( + records.some( + (r) => r.kind === "unseal" && r.content.candidate === candidate, + ) + ) + continue; const seals = board.proposalsOf(candidate).map((id) => { const seq = board.proposal(id).seq; - const seal = records.find((r) => r.kind === "seal" && r.content.proposal === seq); - if (seal === undefined) throw new Error(`提案 ${id}(记录 ${seq})没有封存记录:提案的作者自评在写入提案时一并封存`); + const seal = records.find( + (r) => r.kind === "seal" && r.content.proposal === seq, + ); + if (seal === undefined) + throw new Error( + `提案 ${id}(记录 ${seq})没有封存记录:提案的作者自评在写入提案时一并封存`, + ); return seal.seq; }); s.ledger.append("unseal", { candidate, seals }); @@ -158,5 +234,11 @@ function explanationsOf(board: Board, round: number): AblationExplanation[] { return board .published("explanation") .filter((x) => x.round === round) - .flatMap((x) => x.content.explanations.map((e, i) => ({ id: explanationId(round, x.author, i + 1), author: x.author, cells: e.cells! }))); + .flatMap((x) => + x.content.explanations.map((e, i) => ({ + id: explanationId(round, x.author, i + 1), + author: x.author, + cells: e.cells!, + })), + ); } diff --git a/src/vocab/submissions.ts b/src/vocab/submissions.ts index 6a41ea1..c8e5166 100644 --- a/src/vocab/submissions.ts +++ b/src/vocab/submissions.ts @@ -31,7 +31,7 @@ export const Range = Type.Object( ...CLOSED, description: "三个数,按分数原值写,不管越大越好还是越小越好。必须严格递增 q1 < q5 < q9,认为几乎不变就写一个很窄的范围。" + - "没有\"不知道\"这个选项:拿不准就把 q1 和 q9 拉开", + '没有"不知道"这个选项:拿不准就把 q1 和 q9 拉开', }, ); export type Range = Static; @@ -55,14 +55,15 @@ const ClauseKey = Type.String({ const CodeLocation = Type.String({ pattern: "^[^:\\s]+(:[1-9][0-9]*(-[1-9][0-9]*)?)?$", - description: "代码位置:\"文件\"、\"文件:行号\"或\"文件:起-止\",行号以本次提交代码为准。一个值只写一个连续范围,不带括号标签;多处位置拆开写", + description: + '代码位置:"文件"、"文件:行号"或"文件:起-止",行号以本次提交代码为准。一个值只写一个连续范围,不带括号标签;多处位置拆开写', }); const CONCERNS = Type.String({ ...TEXT, description: "照冻结原文实现、但你认为会出问题的地方,尤其是某条保障会挡掉或改变什么。" + - "它会转给研究者,不阻止运行,也不改动协议。没有就写\"无\"", + '它会转给研究者,不阻止运行,也不改动协议。没有就写"无"', }); // --------------------------------------------------------------------------- @@ -79,7 +80,7 @@ const Entry = Type.Object( ...TEXT, description: "Markdown:从哪里开始看(先读哪段代码、先问哪个问题、先质疑哪个设定)、为什么值得有人去走。" + - "只写起点,不写\"应该改成什么、能到多少分\"", + '只写起点,不写"应该改成什么、能到多少分"', }), }, CLOSED, @@ -88,7 +89,8 @@ const Entry = Type.Object( const ENTRIES = Type.Array(Entry, { minItems: 2, maxItems: 3, - description: "2–3 个入口,彼此尽量不重叠;想不出第三个就交两个。编号由程序按顺序分配", + description: + "2–3 个入口,彼此尽量不重叠;想不出第三个就交两个。编号由程序按顺序分配", }); export const EntrySubmission = Type.Object( @@ -108,7 +110,8 @@ export const DiscussionSubmission = Type.Object( }), entries: Type.Optional({ ...ENTRIES, - description: "可选:整份替换你自己的入口清单(2–3 个)。不换就不交这个字段", + description: + "可选:整份替换你自己的入口清单(2–3 个)。不换就不交这个字段", }), }, CLOSED, @@ -142,7 +145,8 @@ const Factor = Type.Object( }), keySettings: Type.String({ ...TEXT, - description: "关键设定:决定这个因子效果的设定值,例如\"统计窗口 1000 次查找、每次最多删四分之一\";没有写\"无\"", + description: + '关键设定:决定这个因子效果的设定值,例如"统计窗口 1000 次查找、每次最多删四分之一";没有写"无"', }), offBehavior: Type.Optional( Type.String({ @@ -180,7 +184,7 @@ const METRICS = Type.Object( }), reused: Type.Array(Id, { description: - "引用已登记的假设专用指标,照材料里已登记指标清单的全名(\"候选编号.短名\")写;没有就交空数组。" + + '引用已登记的假设专用指标,照材料里已登记指标清单的全名("候选编号.短名")写;没有就交空数组。' + "要测的量清单里已经有,就引用它,不换个名字重新登记:两个名字测同一个东西,会被当成两份独立的证据", }), }, @@ -188,7 +192,7 @@ const METRICS = Type.Object( ...CLOSED, description: "指标登记:这个候选要采集哪些假设专用指标。最终指标和常规辅助指标程序每次都会采集,不用在这里写。" + - "假设专用指标由候选代码在运行当中算出,候选代码拿不到基线的成绩,\"比基线好多少\"这类量测不到。" + + '假设专用指标由候选代码在运行当中算出,候选代码拿不到基线的成绩,"比基线好多少"这类量测不到。' + "协议在某种情形下会让某个指标写 null(例如某条分支没走到、某个因子被关掉),就别给它选 invalidateRun,否则那次正式运行白跑", }, ); @@ -199,7 +203,7 @@ const PROPOSAL_BODY = Type.String({ "提案正文,Markdown,带小标题:每个因子为什么可能有效;" + "每条保障做什么、属于拦 / 补 / 改哪一种、自检时怎么想的;" + "协议(不许改什么、随机性怎么处理、何时停、什么样的运行不算数);因子之间的关系。" + - "某方面不适用写\"不适用:原因\"。协议里不要求实验员按正式规模运行程序来核对什么,正式规模只由程序跑。" + + '某方面不适用写"不适用:原因"。协议里不要求实验员按正式规模运行程序来核对什么,正式规模只由程序跑。' + "不写自己估的分数或提升量;引用已测到的分数要写明出自哪次运行;" + "文献和别人报告的数值只当起点,说明与本任务的区别;从第 2 轮起说明哪些新的观测影响了这条提案", }); @@ -245,7 +249,8 @@ export const CritiqueSubmission = Type.Object( { target: Type.String({ pattern: "^(whole|p[1-9][0-9]*)$", - description: "针对谁:整条提案写 whole,针对某个因子写它的编号(p1、p2……)", + description: + "针对谁:整条提案写 whole,针对某个因子写它的编号(p1、p2……)", }), body: Type.String({ ...TEXT, @@ -280,7 +285,8 @@ const REPLIES = Type.Array( CLOSED, ), { - description: "材料\"本任务要答复的挑刺\"里列出的每一份挑刺各答复一条;没有挑刺交空数组", + description: + '材料"本任务要答复的挑刺"里列出的每一份挑刺各答复一条;没有挑刺交空数组', }, ); @@ -288,7 +294,7 @@ const CLAUSES = Type.Array( Type.String({ ...TEXT, description: - "一条实现条款:实验员要做的一件事,写到不用猜就能实现。开头写位置:\"文件\"、\"文件:行号\"或\"文件:函数名\"," + + '一条实现条款:实验员要做的一件事,写到不用猜就能实现。开头写位置:"文件"、"文件:行号"或"文件:函数名",' + "只写一个行号或一个函数名、不写范围,文件和函数要在基线代码里真实存在", }), { @@ -319,7 +325,8 @@ export const ResponseSubmission = Type.Object( clauses: CLAUSES, body: Type.String({ ...TEXT, - description: "答复正文,Markdown:逐条答复之外要补充的话。没有挑刺、也没有要补充的,写\"无\"", + description: + '答复正文,Markdown:逐条答复之外要补充的话。没有挑刺、也没有要补充的,写"无"', }), }, CLOSED, @@ -342,7 +349,8 @@ export const UnificationSubmission = Type.Object( }), coverage: Type.String({ ...TEXT, - description: "这份原提案在统一后的协议里哪里体现;没采纳的部分写明为什么", + description: + "这份原提案在统一后的协议里哪里体现;没采纳的部分写明为什么", }), }, CLOSED, @@ -407,25 +415,29 @@ export const ImplementationSubmission = Type.Object( CLOSED, ), { - description: "不属于任何因子或保障的改动,按本次代码相对本批基线的完整差异列出,并说明服务哪项要求;没有就交空数组", + description: + "不属于任何因子或保障的改动,按本次代码相对本批基线的完整差异列出,并说明服务哪项要求;没有就交空数组", }, ), concerns: CONCERNS, body: Type.String({ ...TEXT, - description: "实现说明,Markdown:位置表之外需要说明的事。修复时逐条回应退回意见:改在哪里,或以协议原文和代码位置说明为什么不改。协议要求自行求解核数而未执行的,说明数值留给正式实验核对,不声称已经验证。没有要补充的就写\"无\"", + description: + '实现说明,Markdown:位置表之外需要说明的事。修复时逐条回应退回意见:改在哪里,或以协议原文和代码位置说明为什么不改。协议要求自行求解核数而未执行的,说明数值留给正式实验核对,不声称已经验证。没有要补充的就写"无"', }), }, CLOSED, ); const FOUND = Type.Boolean({ - description: "找到代码且实际行为符合该项要求写 true;找不到、找到但不一致或材料不足以核清,写 false(记为没落实)", + description: + "找到代码且实际行为符合该项要求写 true;找不到、找到但不一致或材料不足以核清,写 false(记为没落实)", }); const WHERE = Type.String({ ...TEXT, - description: "代码位置和足以复核的依据,对应冻结要求。没落实时区分具体偏差与核不了:写明看过哪里、哪里不符或还缺什么,不把猜测写成事实", + description: + "代码位置和足以复核的依据,对应冻结要求。没落实时区分具体偏差与核不了:写明看过哪里、哪里不符或还缺什么,不把猜测写成事实", }); export const VerificationSubmission = Type.Object( @@ -474,32 +486,37 @@ export const VerificationSubmission = Type.Object( { location: Type.String({ ...CodeLocation, - description: "材料列出的改动块位置,逐字照抄位置本身,不带展示用的括号标签,不合并不同改动块", + description: + "材料列出的改动块位置,逐字照抄位置本身,不带展示用的括号标签,不合并不同改动块", }), outOfScope: Type.Boolean({ - description: "超出冻结协议、改了最终指标的测量或校验、未按登记定义采集或用占位数掩盖缺测,写 true;落实冻结要求所需的辅助代码写 false", + description: + "超出冻结协议、改了最终指标的测量或校验、未按登记定义采集或用占位数掩盖缺测,写 true;落实冻结要求所需的辅助代码写 false", }), reason: Type.String({ ...TEXT, - description: "指出该块代码及对应的冻结要求,说明它为什么必要或哪里超出要求", + description: + "指出该块代码及对应的冻结要求,说明它为什么必要或哪里超出要求", }), }, CLOSED, ), { - description: "材料里列出的没有归属的改动,逐条判断是否越界;材料说没有就交空数组", + description: + "材料里列出的没有归属的改动,逐条判断是否越界;材料说没有就交空数组", }, ), body: Type.String({ ...TEXT, description: "说明,Markdown:逐项结果之外需要说明的事。材料里列出的同一行既属于因子又属于保障的地方,在这里各写一句提醒(只是提醒,不判越界)。" + - "没有要补充的就写\"无\"", + '没有要补充的就写"无"', }), }, { ...CLOSED, - description: "只交逐项结果,通过与否由程序推出:任何一项没落实或判为越界,就退回实验员修复", + description: + "只交逐项结果,通过与否由程序推出:任何一项没落实或判为越界,就退回实验员修复", }, ); @@ -513,7 +530,7 @@ const Explanation = Type.Object( ...TEXT, description: "Markdown:机制是什么、哪个因子在出力、什么情况下不成立、因子之间在效果上怎么互相依赖" + - "(例如\"没有 p2,p3 就没用\",当作预测交给消融检验)、结论覆盖哪些设定、没覆盖哪些。" + + '(例如"没有 p2,p3 就没用",当作预测交给消融检验)、结论覆盖哪些设定、没覆盖哪些。' + "没有具体的机制判断就直说", }), cells: Type.Optional( @@ -541,7 +558,8 @@ export const ExplanationSubmission = Type.Object( { explanations: Type.Array(Explanation, { minItems: 1, - description: "一般交一条就够,真有第二个机制判断时再交第二条——比如你的主判断若错了最可能是哪种错法,答得出来就写成第二条,写明哪一格能把两条分开;条数上限见材料", + description: + "一般交一条就够,真有第二个机制判断时再交第二条——比如你的主判断若错了最可能是哪种错法,答得出来就写成第二条,写明哪一格能把两条分开;条数上限见材料", }), }, CLOSED, @@ -562,7 +580,8 @@ export const AblationImplementationSubmission = Type.Object( }, { ...CLOSED, - description: "从完整代码出发,把指定的因子换回关掉时的做法,其余因子和全部保障照留", + description: + "从完整代码出发,把指定的因子换回关掉时的做法,其余因子和全部保障照留", }, ); @@ -573,11 +592,13 @@ export const AblationVerificationSubmission = Type.Object( { factor: FactorKey, off: Type.Boolean({ - description: "这份代码里这个因子是关着的写 true,照完整版本保留写 false", + description: + "这份代码里这个因子是关着的写 true,照完整版本保留写 false", }), where: Type.String({ ...TEXT, - description: "依据:在哪几行看到的。不属于任何单个因子的总体核对说明(怎么核的、行号怎么对上的),写进被关掉因子那一项的依据里", + description: + "依据:在哪几行看到的。不属于任何单个因子的总体核对说明(怎么核的、行号怎么对上的),写进被关掉因子那一项的依据里", }), }, CLOSED, @@ -599,7 +620,7 @@ export const WrapUpSubmission = Type.Object( ...TEXT, description: "收尾,Markdown:什么被支持了、什么被挑战了、下一步想往哪走:沿某个因子继续,或换一条更可能拿最高分的路线。" + - "推广结论时写清它覆盖哪些设定。某方面没有写\"没有:为什么\"。" + + '推广结论时写清它覆盖哪些设定。某方面没有写"没有:为什么"。' + "原样放进下一轮的提案材料,所有人都读得到", }), }, @@ -627,4 +648,6 @@ export const SUBMISSION_SCHEMAS = { } as const satisfies Record; /** 某种提交的内容类型,例如 Submission<"proposal">。 */ -export type Submission = Static<(typeof SUBMISSION_SCHEMAS)[K]>; +export type Submission = Static< + (typeof SUBMISSION_SCHEMAS)[K] +>; diff --git a/tests/flow-next-step.test.ts b/tests/flow-next-step.test.ts index c1fb41a..32e169b 100644 --- a/tests/flow-next-step.test.ts +++ b/tests/flow-next-step.test.ts @@ -4,7 +4,11 @@ import { join } from "node:path"; import { test } from "node:test"; import { stepList, type ResearchConfig } from "../src/config/index.ts"; import type { AppendKind, Content, LedgerRecord } from "../src/ledger/index.ts"; -import { Board, forwardNotRunEvent, type StudyRecord } from "../src/materials/index.ts"; +import { + Board, + forwardNotRunEvent, + type StudyRecord, +} from "../src/materials/index.ts"; import { runBalance } from "../src/rules/index.ts"; import { definition, exited } from "./ledger-helpers.ts"; import { judged } from "./materials-helpers.ts"; @@ -15,19 +19,34 @@ import { fullConfig } from "./config-helpers.ts"; // flow 的"下一步做什么"(任务书 12 第六部分):纯函数,手写账本记录,看它给出的下一步。 // 其余都由真实运行证明(任务书 12 第五部分),这里不写假模型、假 Docker。 -const TASK_FACTS = JSON.parse(readFileSync(join(import.meta.dirname, "..", "tasks", "circle_packing", "task.json"), "utf8")) as Json; +const TASK_FACTS = JSON.parse( + readFileSync( + join(import.meta.dirname, "..", "tasks", "circle_packing", "task.json"), + "utf8", + ), +) as Json; const HASH = "0".repeat(64); const measured = (value: number) => ({ status: "measured", value }) as const; /** 调用结束记录去掉调用编号(四种结局各自去掉)。 */ -type Ended = Content<"callEnded"> extends infer E ? (E extends unknown ? Omit : never) : never; +type Ended = + Content<"callEnded"> extends infer E + ? E extends unknown + ? Omit + : never + : never; /** 一本手写的账本:按写入先后编序号。内容照账本的形状写,nextStep 不再检查。 */ class Book { readonly records: LedgerRecord[] = []; add(kind: K, content: Content): this { - this.records.push({ seq: this.records.length + 1, kind, hash: HASH, content } as LedgerRecord); + this.records.push({ + seq: this.records.length + 1, + kind, + hash: HASH, + content, + } as LedgerRecord); return this; } @@ -35,7 +54,10 @@ class Book { const record: StudyRecord = { config: config as unknown as Json, steps: stepList(config) as unknown as Json, - prompts: { roles: {} as StudyRecord["prompts"]["roles"], tasks: {} as StudyRecord["prompts"]["tasks"] }, + prompts: { + roles: {} as StudyRecord["prompts"]["roles"], + tasks: {} as StudyRecord["prompts"]["tasks"], + }, image: `sha256:${HASH}`, task: TASK_FACTS, seed: 1, @@ -44,7 +66,11 @@ class Book { } baselineCode(): this { - return this.add("code", { object: "base", version: "v-base", locations: null }); + return this.add("code", { + object: "base", + version: "v-base", + locations: null, + }); } /** 一次基线运行:没启动,或启动了、判分算数或不算数。 */ @@ -58,12 +84,28 @@ class Book { inheritEnv: false, env: {}, }; - this.add("runPlanned", { run, purpose: "baseline", round: 0, object: null, definition }); + this.add("runPlanned", { + run, + purpose: "baseline", + round: 0, + object: null, + definition, + }); if (outcome === "notStarted") { - return this.add("runResult", { run, process: { ending: "notStarted", reason: "docker run 失败" }, judged: null }); + return this.add("runResult", { + run, + process: { ending: "notStarted", reason: "docker run 失败" }, + judged: null, + }); } this.add("runStarted", { run }); - const process = { ending: "exited", exitCode: 0, cpuSeconds: measured(10), wallSeconds: measured(10), peakMemoryBytes: measured(1) } as const; + const process = { + ending: "exited", + exitCode: 0, + cpuSeconds: measured(10), + wallSeconds: measured(10), + peakMemoryBytes: measured(1), + } as const; const judged = { verdict: outcome, verdictReason: outcome === "valid" ? "合法" : "不合法:圆重叠", @@ -76,35 +118,86 @@ class Book { return this.add("runResult", { run, process, judged }); } - open(stageId: string, stage: Content<"stageOpen">["stage"], round: number, roster: string[]): this { + open( + stageId: string, + stage: Content<"stageOpen">["stage"], + round: number, + roster: string[], + ): this { return this.add("stageOpen", { stageId, stage, round, roster }); } /** 一份被收下的提交。nextStep 只看这个人在这个阶段有没有被收下的提交,种类一律写成入口。 */ entry(stageId: string, author: string): this { - const content = { entries: [{ name: "入口一", body: "从这里开始看" }, { name: "入口二", body: "从那里开始看" }] }; - return this.add("submission", { stageId, author, target: null, item: null, version: 1, kind: "entry", content, jev: null }); + const content = { + entries: [ + { name: "入口一", body: "从这里开始看" }, + { name: "入口二", body: "从那里开始看" }, + ], + }; + return this.add("submission", { + stageId, + author, + target: null, + item: null, + version: 1, + kind: "entry", + content, + jev: null, + }); } /** 一份被收下的提交,内容 nextStep 不读。 */ - sub(stageId: string, author: string, kind: SubmissionKind, target: string | null, item: string | null): this { - return this.add("submission", { stageId, author, target, item, version: 1, kind, content: {}, jev: null } as unknown as Content<"submission">); + sub( + stageId: string, + author: string, + kind: SubmissionKind, + target: string | null, + item: string | null, + ): this { + return this.add("submission", { + stageId, + author, + target, + item, + version: 1, + kind, + content: {}, + jev: null, + } as unknown as Content<"submission">); } /** 一次模型调用:开始和结束。"模型调用开始"里的材料只写 nextStep 认任务用的种类和对象。 */ - call(stageId: string, actor: string, kind: SubmissionKind, object: string | null, n: number, ended: Ended): this { + call( + stageId: string, + actor: string, + kind: SubmissionKind, + object: string | null, + n: number, + ended: Ended, + ): this { const call = `${stageId}.${actor}.${kind}.${object ?? "none"}.${n}`; - this.add("callStarted", { call, actor, stageId, model: "iomgaa/glm-5.3", materials: { kind, object } }); + this.add("callStarted", { + call, + actor, + stageId, + model: "iomgaa/glm-5.3", + materials: { kind, object }, + }); return this.add("callEnded", { call, ...ended } as Content<"callEnded">); } } const R = ["researcher-1", "researcher-2", "researcher-3"]; -const entryTasks = (actors: string[]) => actors.map((actor) => ({ actor, kind: "entry", object: null })); +const entryTasks = (actors: string[]) => + actors.map((actor) => ({ actor, kind: "entry", object: null })); /** 冻结了配置、基线跑完算数的账本。 */ function afterBaseline(config: ResearchConfig = fullConfig()): Book { - return new Book().config(config).baselineCode().baselineRun("base-1", "valid"); + return new Book() + .config(config) + .baselineCode() + .baselineRun("base-1", "valid"); } test("空账本:冻结配置", () => { @@ -115,41 +208,80 @@ test("有配置、没有基线:先存基线代码,再用 base-1 跑基线", const book = new Book().config(fullConfig()); assert.deepEqual(nextStep(book.records), { do: "saveBaseline" }); book.baselineCode(); - assert.deepEqual(nextStep(book.records), { do: "baselineRun", run: "base-1" }); + assert.deepEqual(nextStep(book.records), { + do: "baselineRun", + run: "base-1", + }); }); -test("基线最近一次是\"没启动\":起新编号再跑", () => { - const book = new Book().config(fullConfig()).baselineCode().baselineRun("base-1", "notStarted"); - assert.deepEqual(nextStep(book.records), { do: "baselineRun", run: "base-2" }); +test('基线最近一次是"没启动":起新编号再跑', () => { + const book = new Book() + .config(fullConfig()) + .baselineCode() + .baselineRun("base-1", "notStarted"); + assert.deepEqual(nextStep(book.records), { + do: "baselineRun", + run: "base-2", + }); book.baselineRun("base-2", "notStarted"); - assert.deepEqual(nextStep(book.records), { do: "baselineRun", run: "base-3" }); + assert.deepEqual(nextStep(book.records), { + do: "baselineRun", + run: "base-3", + }); }); test("基线判为不算数:停下,原因写明", () => { - const book = new Book().config(fullConfig()).baselineCode().baselineRun("base-1", "invalid"); + const book = new Book() + .config(fullConfig()) + .baselineCode() + .baselineRun("base-1", "invalid"); const next = nextStep(book.records); assert.equal(next.do, "halt"); - assert.match((next as { reason: string }).reason, /base-1 判为不算数(不合法:圆重叠)/); + assert.match( + (next as { reason: string }).reason, + /base-1 判为不算数(不合法:圆重叠)/, + ); }); test("基线算数之后:开写入口阶段,名单是全部研究者", () => { - assert.deepEqual(nextStep(afterBaseline().records), { do: "openStage", stageId: "r0-entryDraft", stage: "entryDraft", round: 0, roster: R }); + assert.deepEqual(nextStep(afterBaseline().records), { + do: "openStage", + stageId: "r0-entryDraft", + stage: "entryDraft", + round: 0, + roster: R, + }); }); test("阶段开着、一人已交、两人没交:派那两人", () => { - const book = afterBaseline().open("r0-entryDraft", "entryDraft", 0, R).entry("r0-entryDraft", "researcher-1"); - assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r0-entryDraft", tasks: entryTasks(["researcher-2", "researcher-3"]) }); + const book = afterBaseline() + .open("r0-entryDraft", "entryDraft", 0, R) + .entry("r0-entryDraft", "researcher-1"); + assert.deepEqual(nextStep(book.records), { + do: "dispatch", + stageId: "r0-entryDraft", + tasks: entryTasks(["researcher-2", "researcher-3"]), + }); }); test("都交了:关阶段;关了以后开第 1 次讨论", () => { const book = afterBaseline().open("r0-entryDraft", "entryDraft", 0, R); for (const r of R) book.entry("r0-entryDraft", r); - assert.deepEqual(nextStep(book.records), { do: "closeStage", stageId: "r0-entryDraft" }); + assert.deepEqual(nextStep(book.records), { + do: "closeStage", + stageId: "r0-entryDraft", + }); book.add("stageClose", { stageId: "r0-entryDraft" }); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r0-discussion1", stage: "discussion", round: 0, roster: R }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r0-discussion1", + stage: "discussion", + round: 0, + roster: R, + }); }); -test("有人最近一次是\"没交\":开发入口关着先派能派的、再停下;开着重新派", () => { +test('有人最近一次是"没交":开发入口关着先派能派的、再停下;开着重新派', () => { const stuck = (devEntry: boolean) => new Book() .config({ ...fullConfig(), devEntry }) @@ -157,41 +289,95 @@ test("有人最近一次是\"没交\":开发入口关着先派能派的、再 .baselineRun("base-1", "valid") .open("r0-entryDraft", "entryDraft", 0, R) .entry("r0-entryDraft", "researcher-1") - .call("r0-entryDraft", "researcher-2", "entry", null, 1, { wallSeconds: measured(30), status: "notSubmitted", reason: "模型结束了回复,没有一份提交被收下" }); + .call("r0-entryDraft", "researcher-2", "entry", null, 1, { + wallSeconds: measured(30), + status: "notSubmitted", + reason: "模型结束了回复,没有一份提交被收下", + }); // researcher-3 还没派过,先派他。 - assert.deepEqual(nextStep(stuck(false).records), { do: "dispatch", stageId: "r0-entryDraft", tasks: entryTasks(["researcher-3"]) }); + assert.deepEqual(nextStep(stuck(false).records), { + do: "dispatch", + stageId: "r0-entryDraft", + tasks: entryTasks(["researcher-3"]), + }); // researcher-3 交上以后只剩"没交"的 researcher-2:停下,附调用编号。 const closed = stuck(false).entry("r0-entryDraft", "researcher-3"); const next = nextStep(closed.records); assert.equal(next.do, "halt"); assert.match((next as { reason: string }).reason, /^交不出合格提交/); - assert.deepEqual((next as { objects: string[] }).objects, ["r0-entryDraft.researcher-2.entry.none.1"]); + assert.deepEqual((next as { objects: string[] }).objects, [ + "r0-entryDraft.researcher-2.entry.none.1", + ]); // 开发入口开着:重新派。 const dev = stuck(true).entry("r0-entryDraft", "researcher-3"); - assert.deepEqual(nextStep(dev.records), { do: "dispatch", stageId: "r0-entryDraft", tasks: entryTasks(["researcher-2"]) }); + assert.deepEqual(nextStep(dev.records), { + do: "dispatch", + stageId: "r0-entryDraft", + tasks: entryTasks(["researcher-2"]), + }); }); -test("有人最近一次是\"出错\"或\"进程中断\":重新派;只看最近一次调用", () => { +test('有人最近一次是"出错"或"进程中断":重新派;只看最近一次调用', () => { const book = afterBaseline() .open("r0-entryDraft", "entryDraft", 0, R) .entry("r0-entryDraft", "researcher-1") - .call("r0-entryDraft", "researcher-2", "entry", null, 1, { wallSeconds: measured(5), status: "notSubmitted", reason: "没交" }) - .call("r0-entryDraft", "researcher-2", "entry", null, 2, { wallSeconds: measured(5), status: "failed", reason: "模型服务报错" }) - .call("r0-entryDraft", "researcher-3", "entry", null, 1, { wallSeconds: { status: "unmeasured", reason: "进程中断" }, status: "failed", reason: "进程中断" }); - assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r0-entryDraft", tasks: entryTasks(["researcher-2", "researcher-3"]) }); + .call("r0-entryDraft", "researcher-2", "entry", null, 1, { + wallSeconds: measured(5), + status: "notSubmitted", + reason: "没交", + }) + .call("r0-entryDraft", "researcher-2", "entry", null, 2, { + wallSeconds: measured(5), + status: "failed", + reason: "模型服务报错", + }) + .call("r0-entryDraft", "researcher-3", "entry", null, 1, { + wallSeconds: { status: "unmeasured", reason: "进程中断" }, + status: "failed", + reason: "进程中断", + }); + assert.deepEqual(nextStep(book.records), { + do: "dispatch", + stageId: "r0-entryDraft", + tasks: entryTasks(["researcher-2", "researcher-3"]), + }); }); test("只有一位研究者:两次讨论各写一条按配置跳过,接着开认领", () => { const one = fullConfig(); one.models.researchers = [one.models.researchers[0]!]; - const book = afterBaseline(one).open("r0-entryDraft", "entryDraft", 0, ["researcher-1"]).entry("r0-entryDraft", "researcher-1"); + const book = afterBaseline(one) + .open("r0-entryDraft", "entryDraft", 0, ["researcher-1"]) + .entry("r0-entryDraft", "researcher-1"); book.add("stageClose", { stageId: "r0-entryDraft" }); const reason = "只有一位研究者,第零轮不讨论(方法.md 4.2)"; - assert.deepEqual(nextStep(book.records), { do: "skip", step: "discussion1", reason }); - book.add("event", { event: "skippedByConfig", reason, objects: ["discussion1"] }); - assert.deepEqual(nextStep(book.records), { do: "skip", step: "discussion2", reason }); - book.add("event", { event: "skippedByConfig", reason, objects: ["discussion2"] }); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r0-claim", stage: "claim", round: 0, roster: ["researcher-1"] }); + assert.deepEqual(nextStep(book.records), { + do: "skip", + step: "discussion1", + reason, + }); + book.add("event", { + event: "skippedByConfig", + reason, + objects: ["discussion1"], + }); + assert.deepEqual(nextStep(book.records), { + do: "skip", + step: "discussion2", + reason, + }); + book.add("event", { + event: "skippedByConfig", + reason, + objects: ["discussion2"], + }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r0-claim", + stage: "claim", + round: 0, + roster: ["researcher-1"], + }); }); /** 第零轮做完:四个阶段都交齐关闭,分配决定已写。 */ @@ -213,15 +399,34 @@ function afterRoundZero(config: ResearchConfig): Book { /** 分配决定。 */ function assigned(book: Book): Book { - const output = R.map((researcher, i) => ({ researcher, entry: `e-${researcher}-1`, rank: i + 1 })); - return book.add("decision", { decision: "entries", round: 0, rule: "assignEntries", ruleVersion: 1, params: null, seed: null, input: null, output }); + const output = R.map((researcher, i) => ({ + researcher, + entry: `e-${researcher}-1`, + rank: i + 1, + })); + return book.add("decision", { + decision: "entries", + round: 0, + rule: "assignEntries", + ruleVersion: 1, + params: null, + seed: null, + input: null, + output, + }); } test("认领关了:分配;分配做完:开第 1 轮的提案阶段,名单是全部研究者", () => { const book = afterRoundZero(fullConfig()); assert.deepEqual(nextStep(book.records), { do: "assignEntries" }); assigned(book); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-proposal", stage: "proposal", round: 1, roster: R }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-proposal", + stage: "proposal", + round: 1, + roster: R, + }); }); // --------------------------------------------------------------------------- @@ -235,26 +440,88 @@ const P3 = "r1-researcher-3-1"; const C = ["r1-c1", "r1-c2"]; /** 一个研究者阶段:开、每个任务交一份、关。tasks 是 [作者, 种类, 对象, 编号]。 */ -function stage(book: Book, stageId: string, kind: Content<"stageOpen">["stage"], tasks: Array<[string, SubmissionKind, string | null, string | null]>): Book { +function stage( + book: Book, + stageId: string, + kind: Content<"stageOpen">["stage"], + tasks: Array<[string, SubmissionKind, string | null, string | null]>, +): Book { book.open(stageId, kind, 1, R); - for (const [author, k, target, item] of tasks) book.sub(stageId, author, k, target, item); + for (const [author, k, target, item] of tasks) + book.sub(stageId, author, k, target, item); return book.add("stageClose", { stageId }); } /** 每人 × 每个对象的任务。 */ -const each = (kind: SubmissionKind, objects: string[]) => R.flatMap((actor) => objects.map((object) => ({ actor, kind, object }))); +const each = (kind: SubmissionKind, objects: string[]) => + R.flatMap((actor) => objects.map((object) => ({ actor, kind, object }))); /** 第 1 轮写到 last 这一步做完为止。 */ -function roundOne(last: "proposal" | "dedupe" | "firstPrediction" | "critique" | "response" | "freezeProtocol" | "secondPrediction" | "selectBatch"): Book { +function roundOne( + last: + | "proposal" + | "dedupe" + | "firstPrediction" + | "critique" + | "response" + | "freezeProtocol" + | "secondPrediction" + | "selectBatch", +): Book { const book = assigned(afterRoundZero(fullConfig())); - const order = ["proposal", "dedupe", "firstPrediction", "critique", "response", "freezeProtocol", "secondPrediction", "selectBatch"]; + const order = [ + "proposal", + "dedupe", + "firstPrediction", + "critique", + "response", + "freezeProtocol", + "secondPrediction", + "selectBatch", + ]; const steps: Record void> = { - proposal: () => stage(book, "r1-proposal", "proposal", [["researcher-1", "proposal", null, P1], ["researcher-2", "proposal", null, P2], ["researcher-3", "proposal", null, P3]]), + proposal: () => + stage(book, "r1-proposal", "proposal", [ + ["researcher-1", "proposal", null, P1], + ["researcher-2", "proposal", null, P2], + ["researcher-3", "proposal", null, P3], + ]), dedupe: () => { - const output = { fresh: [{ candidate: "r1-c1", proposals: [P1] }, { candidate: "r1-c2", proposals: [P2, P3] }], carried: [] }; - book.add("decision", { decision: "dedup", round: 1, rule: "jevSameChange", ruleVersion: 1, params: null, seed: null, input: null, output }); + const output = { + fresh: [ + { candidate: "r1-c1", proposals: [P1] }, + { candidate: "r1-c2", proposals: [P2, P3] }, + ], + carried: [], + }; + book.add("decision", { + decision: "dedup", + round: 1, + rule: "jevSameChange", + ruleVersion: 1, + params: null, + seed: null, + input: null, + output, + }); }, - firstPrediction: () => stage(book, "r1-firstPrediction", "firstPrediction", R.flatMap((a) => C.map((c) => [a, "firstPrediction", c, null] as [string, SubmissionKind, string, null]))), + firstPrediction: () => + stage( + book, + "r1-firstPrediction", + "firstPrediction", + R.flatMap((a) => + C.map( + (c) => + [a, "firstPrediction", c, null] as [ + string, + SubmissionKind, + string, + null, + ], + ), + ), + ), critique: () => stage(book, "r1-critique", "critique", [ ["researcher-1", "critique", P2, `${P2}.researcher-1.1`], @@ -264,51 +531,158 @@ function roundOne(last: "proposal" | "dedupe" | "firstPrediction" | "critique" | ["researcher-3", "critique", P1, `${P1}.researcher-3.1`], ["researcher-3", "critique", P2, `${P2}.researcher-3.1`], ]), - response: () => stage(book, "r1-response", "response", [["researcher-1", "response", "r1-c1", null], ["researcher-2", "unification", "r1-c2", null]]), + response: () => + stage(book, "r1-response", "response", [ + ["researcher-1", "response", "r1-c1", null], + ["researcher-2", "unification", "r1-c2", null], + ]), freezeProtocol: () => { // r1-c1 一个因子(采纳它不做消融),r1-c2 两个因子(采纳它走 ⑬–⑰)。 - book.add("protocol", { candidate: "r1-c1", protocol: { round: 1, baseline: "base", proposals: [], factors: [{}] } }); - book.add("protocol", { candidate: "r1-c2", protocol: { round: 1, baseline: "base", proposals: [], factors: [{}, {}] } }); + book.add("protocol", { + candidate: "r1-c1", + protocol: { round: 1, baseline: "base", proposals: [], factors: [{}] }, + }); + book.add("protocol", { + candidate: "r1-c2", + protocol: { + round: 1, + baseline: "base", + proposals: [], + factors: [{}, {}], + }, + }); }, - secondPrediction: () => stage(book, "r1-secondPrediction", "secondPrediction", R.flatMap((a) => C.map((c) => [a, "secondPrediction", c, null] as [string, SubmissionKind, string, null]))), - selectBatch: () => book.add("decision", { decision: "batch", round: 1, rule: "bestExpected", ruleVersion: 1, params: null, seed: "s", input: null, output: { chosen: ["r1-c1"] } }), + secondPrediction: () => + stage( + book, + "r1-secondPrediction", + "secondPrediction", + R.flatMap((a) => + C.map( + (c) => + [a, "secondPrediction", c, null] as [ + string, + SubmissionKind, + string, + null, + ], + ), + ), + ), + selectBatch: () => + book.add("decision", { + decision: "batch", + round: 1, + rule: "bestExpected", + ruleVersion: 1, + params: null, + seed: "s", + input: null, + output: { chosen: ["r1-c1"] }, + }), }; for (const step of order.slice(0, order.indexOf(last) + 1)) steps[step]!(); return book; } test("提案阶段一人交了两条、两人没交:只派没交的两人,提案没有对象", () => { - const book = assigned(afterRoundZero(fullConfig())).open("r1-proposal", "proposal", 1, R).sub("r1-proposal", "researcher-1", "proposal", null, P1); - book.sub("r1-proposal", "researcher-1", "proposal", null, "r1-researcher-1-2"); - const tasks = ["researcher-2", "researcher-3"].map((actor) => ({ actor, kind: "proposal", object: null })); - assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-proposal", tasks }); + const book = assigned(afterRoundZero(fullConfig())) + .open("r1-proposal", "proposal", 1, R) + .sub("r1-proposal", "researcher-1", "proposal", null, P1); + book.sub( + "r1-proposal", + "researcher-1", + "proposal", + null, + "r1-researcher-1-2", + ); + const tasks = ["researcher-2", "researcher-3"].map((actor) => ({ + actor, + kind: "proposal", + object: null, + })); + assert.deepEqual(nextStep(book.records), { + do: "dispatch", + stageId: "r1-proposal", + tasks, + }); }); test("提案交齐关闭后:判重合并", () => { - assert.deepEqual(nextStep(roundOne("proposal").records), { do: "dedupe", round: 1 }); + assert.deepEqual(nextStep(roundOne("proposal").records), { + do: "dedupe", + round: 1, + }); }); test("判重做完:开第一次预测;任务是每人 × 每个新候选(含自己的),同一个人的按候选编号排", () => { const book = roundOne("dedupe"); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-firstPrediction", stage: "firstPrediction", round: 1, roster: R }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-firstPrediction", + stage: "firstPrediction", + round: 1, + roster: R, + }); book.open("r1-firstPrediction", "firstPrediction", 1, R); - assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-firstPrediction", tasks: each("firstPrediction", C) }); + assert.deepEqual(nextStep(book.records), { + do: "dispatch", + stageId: "r1-firstPrediction", + tasks: each("firstPrediction", C), + }); }); test("同一个人的任务按对象分开算:对 r1-c1 没交的停住,r1-c2 照派;能派的派完才停下", () => { - const notSubmitted = { wallSeconds: measured(30), status: "notSubmitted", reason: "模型结束了回复,没有一份提交被收下" } as const; - const book = roundOne("dedupe").open("r1-firstPrediction", "firstPrediction", 1, R); - for (const a of ["researcher-1", "researcher-3"]) for (const c of C) book.sub("r1-firstPrediction", a, "firstPrediction", c, null); - book.call("r1-firstPrediction", "researcher-2", "firstPrediction", "r1-c1", 1, notSubmitted); - assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-firstPrediction", tasks: [{ actor: "researcher-2", kind: "firstPrediction", object: "r1-c2" }] }); - book.sub("r1-firstPrediction", "researcher-2", "firstPrediction", "r1-c2", null); + const notSubmitted = { + wallSeconds: measured(30), + status: "notSubmitted", + reason: "模型结束了回复,没有一份提交被收下", + } as const; + const book = roundOne("dedupe").open( + "r1-firstPrediction", + "firstPrediction", + 1, + R, + ); + for (const a of ["researcher-1", "researcher-3"]) + for (const c of C) + book.sub("r1-firstPrediction", a, "firstPrediction", c, null); + book.call( + "r1-firstPrediction", + "researcher-2", + "firstPrediction", + "r1-c1", + 1, + notSubmitted, + ); + assert.deepEqual(nextStep(book.records), { + do: "dispatch", + stageId: "r1-firstPrediction", + tasks: [ + { actor: "researcher-2", kind: "firstPrediction", object: "r1-c2" }, + ], + }); + book.sub( + "r1-firstPrediction", + "researcher-2", + "firstPrediction", + "r1-c2", + null, + ); const next = nextStep(book.records); assert.equal(next.do, "halt"); - assert.deepEqual((next as { objects: string[] }).objects, ["r1-firstPrediction.researcher-2.firstPrediction.r1-c1.1"]); + assert.deepEqual((next as { objects: string[] }).objects, [ + "r1-firstPrediction.researcher-2.firstPrediction.r1-c1.1", + ]); }); test("挑刺阶段:每人 × 每条不是自己写的原提案,没有挑自己的", () => { - const book = roundOne("firstPrediction").open("r1-critique", "critique", 1, R); + const book = roundOne("firstPrediction").open( + "r1-critique", + "critique", + 1, + R, + ); const expected = [ { actor: "researcher-1", kind: "critique", object: P2 }, { actor: "researcher-1", kind: "critique", object: P3 }, @@ -317,7 +691,11 @@ test("挑刺阶段:每人 × 每条不是自己写的原提案,没有挑自 { actor: "researcher-3", kind: "critique", object: P1 }, { actor: "researcher-3", kind: "critique", object: P2 }, ]; - assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-critique", tasks: expected }); + assert.deepEqual(nextStep(book.records), { + do: "dispatch", + stageId: "r1-critique", + tasks: expected, + }); }); test("回应阶段:单条原提案的候选派作者交回应,合并而成的派编号最小的作者交统一", () => { @@ -326,24 +704,47 @@ test("回应阶段:单条原提案的候选派作者交回应,合并而成 { actor: "researcher-1", kind: "response", object: "r1-c1" }, { actor: "researcher-2", kind: "unification", object: "r1-c2" }, ]; - assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-response", tasks: expected }); + assert.deepEqual(nextStep(book.records), { + do: "dispatch", + stageId: "r1-response", + tasks: expected, + }); }); test("回应阶段关闭、还没有冻结协议:写冻结协议;写完开第二次预测,任务是每人 × 池里每个候选", () => { const book = roundOne("response"); assert.deepEqual(nextStep(book.records), { do: "freezeProtocols", round: 1 }); const frozen = roundOne("freezeProtocol"); - assert.deepEqual(nextStep(frozen.records), { do: "openStage", stageId: "r1-secondPrediction", stage: "secondPrediction", round: 1, roster: R }); + assert.deepEqual(nextStep(frozen.records), { + do: "openStage", + stageId: "r1-secondPrediction", + stage: "secondPrediction", + round: 1, + roster: R, + }); frozen.open("r1-secondPrediction", "secondPrediction", 1, R); - assert.deepEqual(nextStep(frozen.records), { do: "dispatch", stageId: "r1-secondPrediction", tasks: each("secondPrediction", C) }); + assert.deepEqual(nextStep(frozen.records), { + do: "dispatch", + stageId: "r1-secondPrediction", + tasks: each("secondPrediction", C), + }); }); test("第二次预测关闭:选批", () => { - assert.deepEqual(nextStep(roundOne("secondPrediction").records), { do: "selectBatch", round: 1 }); + assert.deepEqual(nextStep(roundOne("secondPrediction").records), { + do: "selectBatch", + round: 1, + }); }); test("选批做完:开实现阶段,每个入选候选固定一位实验员", () => { - assert.deepEqual(nextStep(roundOne("selectBatch").records), { do: "openStage", stageId: "r1-implementation", stage: "implementation", round: 1, roster: ["experimenter-r1-c1"] }); + assert.deepEqual(nextStep(roundOne("selectBatch").records), { + do: "openStage", + stageId: "r1-implementation", + stage: "implementation", + round: 1, + roster: ["experimenter-r1-c1"], + }); }); test("关掉第一次预测:第 1 轮写一条按配置跳过(对象 r1-firstPrediction),接着开挑刺", () => { @@ -351,16 +752,48 @@ test("关掉第一次预测:第 1 轮写一条按配置跳过(对象 r1-firs if (config.flow.preset !== "full") throw new Error("测试配置应是完整流程"); config.flow.firstPrediction = false; const book = assigned(afterRoundZero(config)); - stage(book, "r1-proposal", "proposal", [["researcher-1", "proposal", null, P1], ["researcher-2", "proposal", null, P2], ["researcher-3", "proposal", null, P3]]); - const output = { fresh: [{ candidate: "r1-c1", proposals: [P1] }, { candidate: "r1-c2", proposals: [P2, P3] }], carried: [] }; - book.add("decision", { decision: "dedup", round: 1, rule: "jevSameChange", ruleVersion: 1, params: null, seed: null, input: null, output }); + stage(book, "r1-proposal", "proposal", [ + ["researcher-1", "proposal", null, P1], + ["researcher-2", "proposal", null, P2], + ["researcher-3", "proposal", null, P3], + ]); + const output = { + fresh: [ + { candidate: "r1-c1", proposals: [P1] }, + { candidate: "r1-c2", proposals: [P2, P3] }, + ], + carried: [], + }; + book.add("decision", { + decision: "dedup", + round: 1, + rule: "jevSameChange", + ruleVersion: 1, + params: null, + seed: null, + input: null, + output, + }); const reason = "配置关掉了第一次预测"; - assert.deepEqual(nextStep(book.records), { do: "skip", step: "r1-firstPrediction", reason }); - book.add("event", { event: "skippedByConfig", reason, objects: ["r1-firstPrediction"] }); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-critique", stage: "critique", round: 1, roster: R }); + assert.deepEqual(nextStep(book.records), { + do: "skip", + step: "r1-firstPrediction", + reason, + }); + book.add("event", { + event: "skippedByConfig", + reason, + objects: ["r1-firstPrediction"], + }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-critique", + stage: "critique", + round: 1, + roster: R, + }); }); - // ⑨–⑫:预期动作手写,不调模型、Jev 或 Docker。 const X = ["experimenter-r1-c1", "experimenter-r1-c2"]; const V = ["verifier-r1-c1", "verifier-r1-c2"]; @@ -368,25 +801,62 @@ const V = ["verifier-r1-c1", "verifier-r1-c2"]; function batchTwo(maxRepairs: number): Book { const book = roundOne("secondPrediction"); const record = book.records.find((r) => r.kind === "config")!; - (record.content as unknown as StudyRecord).config = { ...fullConfig(), maxRepairs } as unknown as Json; - return book.add("decision", { decision: "batch", round: 1, rule: "bestExpected", ruleVersion: 1, params: null, seed: "s", input: null, output: { chosen: C } }); + (record.content as unknown as StudyRecord).config = { + ...fullConfig(), + maxRepairs, + } as unknown as Json; + return book.add("decision", { + decision: "batch", + round: 1, + rule: "bestExpected", + ruleVersion: 1, + params: null, + seed: "s", + input: null, + output: { chosen: C }, + }); } function implemented(book: Book): Book { book.open("r1-implementation", "implementation", 1, X); - for (const [i, candidate] of C.entries()) book.sub("r1-implementation", X[i]!, "implementation", candidate, null); + for (const [i, candidate] of C.entries()) + book.sub("r1-implementation", X[i]!, "implementation", candidate, null); return book.add("stageClose", { stageId: "r1-implementation" }); } -function verified(book: Book, n: number, candidate: string, passed: boolean): Book { +function verified( + book: Book, + n: number, + candidate: string, + passed: boolean, +): Book { // 对应真实 15-tight-2 的 submissions created_seq=173(墙约束行和右端错位)、194(修复后通过)。 // 只转录驱动程序所需的逐项 found,不伪造 Jev 的判断或模型运行。 const content: Submission<"verification"> = { - clauses: [{ clause: "c1", found: passed, where: passed ? "initial.py 墙约束行和右端已对齐" : "initial.py:225-228 与 243-246:墙约束行和右端错位" }], + clauses: [ + { + clause: "c1", + found: passed, + where: passed + ? "initial.py 墙约束行和右端已对齐" + : "initial.py:225-228 与 243-246:墙约束行和右端错位", + }, + ], factors: [{ factor: "p1", found: passed, where: "LP 墙约束" }], - safeguards: [], unassigned: [], body: passed ? "通过" : "解的不是协议写的线性规划", + safeguards: [], + unassigned: [], + body: passed ? "通过" : "解的不是协议写的线性规划", }; - return book.add("submission", { stageId: `r1-verification-${n}`, author: `verifier-${candidate}`, kind: "verification", target: candidate, item: null, version: 1, content, jev: null }); + return book.add("submission", { + stageId: `r1-verification-${n}`, + author: `verifier-${candidate}`, + kind: "verification", + target: candidate, + item: null, + version: 1, + content, + jev: null, + }); } function firstVerification(book: Book, a: boolean, b: boolean): Book { @@ -396,46 +866,138 @@ function firstVerification(book: Book, a: boolean, b: boolean): Book { return book.add("stageClose", { stageId: "r1-verification-1" }); } -function recordForward(book: Book, candidate: string, n: number, ending: "valid" | "notStarted" | "timeout"): Book { +function recordForward( + book: Book, + candidate: string, + n: number, + ending: "valid" | "notStarted" | "timeout", +): Book { const run = `r1-forward-${candidate}-${n}`; - book.add("runPlanned", { run, purpose: "forward", round: 1, object: candidate, definition }); - if (ending === "notStarted") return book.add("runResult", { run, process: { ending, reason: "Docker 未启动" }, judged: null }); + book.add("runPlanned", { + run, + purpose: "forward", + round: 1, + object: candidate, + definition, + }); + if (ending === "notStarted") + return book.add("runResult", { + run, + process: { ending, reason: "Docker 未启动" }, + judged: null, + }); book.add("runStarted", { run }); - const process = ending === "timeout" ? { ending: "timeout" as const, wallSeconds: measured(120), cpuSeconds: measured(100), peakMemoryBytes: measured(1024) } : exited; - return book.add("runResult", { run, process, judged: ending === "valid" ? judged(1.25, "main") : null }); + const process = + ending === "timeout" + ? { + ending: "timeout" as const, + wallSeconds: measured(120), + cpuSeconds: measured(100), + peakMemoryBytes: measured(1024), + } + : exited; + return book.add("runResult", { + run, + process, + judged: ending === "valid" ? judged(1.25, "main") : null, + }); } -const forward = (candidate: string, n: number) => ({ do: "forwardRun", round: 1, candidate, run: `r1-forward-${candidate}-${n}` }); +const forward = (candidate: string, n: number) => ({ + do: "forwardRun", + round: 1, + candidate, + run: `r1-forward-${candidate}-${n}`, +}); test("实现的任务同时可派;已提交后中断不重派,等全体交齐关阶段后才核验", () => { const book = batchTwo(1).open("r1-implementation", "implementation", 1, X); - assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-implementation", tasks: C.map((object, i) => ({ actor: X[i], kind: "implementation", object })) }); + assert.deepEqual(nextStep(book.records), { + do: "dispatch", + stageId: "r1-implementation", + tasks: C.map((object, i) => ({ + actor: X[i], + kind: "implementation", + object, + })), + }); book.sub("r1-implementation", X[0]!, "implementation", C[0]!, null); - book.call("r1-implementation", X[0]!, "implementation", C[0]!, 1, { wallSeconds: { status: "unmeasured", reason: "进程中断" }, status: "failed", reason: "进程中断" }); - assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-implementation", tasks: [{ actor: X[1], kind: "implementation", object: C[1] }] }); + book.call("r1-implementation", X[0]!, "implementation", C[0]!, 1, { + wallSeconds: { status: "unmeasured", reason: "进程中断" }, + status: "failed", + reason: "进程中断", + }); + assert.deepEqual(nextStep(book.records), { + do: "dispatch", + stageId: "r1-implementation", + tasks: [{ actor: X[1], kind: "implementation", object: C[1] }], + }); book.sub("r1-implementation", X[1]!, "implementation", C[1]!, null); - assert.deepEqual(nextStep(book.records), { do: "closeStage", stageId: "r1-implementation" }); + assert.deepEqual(nextStep(book.records), { + do: "closeStage", + stageId: "r1-implementation", + }); book.add("stageClose", { stageId: "r1-implementation" }); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-verification-1", stage: "verification", round: 1, roster: V }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-verification-1", + stage: "verification", + round: 1, + roster: V, + }); }); test("15-tight-2 的一次修复路径:核验关后才派原实验员,修复关后只重验修过的候选", () => { - const book = implemented(batchTwo(1)).open("r1-verification-1", "verification", 1, V); + const book = implemented(batchTwo(1)).open( + "r1-verification-1", + "verification", + 1, + V, + ); verified(book, 1, C[0]!, false); verified(book, 1, C[1]!, true); - assert.deepEqual(nextStep(book.records), { do: "closeStage", stageId: "r1-verification-1" }); + assert.deepEqual(nextStep(book.records), { + do: "closeStage", + stageId: "r1-verification-1", + }); book.add("stageClose", { stageId: "r1-verification-1" }); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-repair-1", stage: "implementation", round: 1, roster: [X[0]] }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-repair-1", + stage: "implementation", + round: 1, + roster: [X[0]], + }); book.open("r1-repair-1", "implementation", 1, [X[0]!]); - assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-repair-1", tasks: [{ actor: X[0], kind: "repair", object: C[0] }] }); + assert.deepEqual(nextStep(book.records), { + do: "dispatch", + stageId: "r1-repair-1", + tasks: [{ actor: X[0], kind: "repair", object: C[0] }], + }); book.sub("r1-repair-1", X[0]!, "repair", C[0]!, null); - assert.deepEqual(nextStep(book.records), { do: "closeStage", stageId: "r1-repair-1" }); + assert.deepEqual(nextStep(book.records), { + do: "closeStage", + stageId: "r1-repair-1", + }); book.add("stageClose", { stageId: "r1-repair-1" }); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-verification-2", stage: "verification", round: 1, roster: [V[0]] }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-verification-2", + stage: "verification", + round: 1, + roster: [V[0]], + }); book.open("r1-verification-2", "verification", 1, [V[0]!]); - assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-verification-2", tasks: [{ actor: V[0], kind: "verification", object: C[0] }] }); + assert.deepEqual(nextStep(book.records), { + do: "dispatch", + stageId: "r1-verification-2", + tasks: [{ actor: V[0], kind: "verification", object: C[0] }], + }); verified(book, 2, C[0]!, true); - assert.deepEqual(nextStep(book.records), { do: "closeStage", stageId: "r1-verification-2" }); + assert.deepEqual(nextStep(book.records), { + do: "closeStage", + stageId: "r1-verification-2", + }); book.add("stageClose", { stageId: "r1-verification-2" }); assert.deepEqual(nextStep(book.records), forward(C[0]!, 1)); }); @@ -443,11 +1005,24 @@ test("15-tight-2 的一次修复路径:核验关后才派原实验员,修复 test("修复次数从关闭的阶段数出:两次修复后仍失败,逐候选写不跑事件,不占预算", () => { const book = firstVerification(batchTwo(2), false, false); for (let n = 1; n <= 2; n++) { - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: `r1-repair-${n}`, stage: "implementation", round: 1, roster: X }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: `r1-repair-${n}`, + stage: "implementation", + round: 1, + roster: X, + }); book.open(`r1-repair-${n}`, "implementation", 1, X); - for (const [i, candidate] of C.entries()) book.sub(`r1-repair-${n}`, X[i]!, "repair", candidate, null); + for (const [i, candidate] of C.entries()) + book.sub(`r1-repair-${n}`, X[i]!, "repair", candidate, null); book.add("stageClose", { stageId: `r1-repair-${n}` }); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: `r1-verification-${n + 1}`, stage: "verification", round: 1, roster: V }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: `r1-verification-${n + 1}`, + stage: "verification", + round: 1, + roster: V, + }); book.open(`r1-verification-${n + 1}`, "verification", 1, V); for (const candidate of C) verified(book, n + 1, candidate, false); book.add("stageClose", { stageId: `r1-verification-${n + 1}` }); @@ -461,7 +1036,11 @@ test("修复次数从关闭的阶段数出:两次修复后仍失败,逐候 assert.match(next.reason, /解的不是协议写的线性规划/); book.add("event", forwardNotRunEvent(candidate, next.reason)); } - assert.deepEqual(runBalance(13, new Board(book.records).runStates()), { charged: 1, reserved: 0, balance: 12 }); + assert.deepEqual(runBalance(13, new Board(book.records).runStates()), { + charged: 1, + reserved: 0, + balance: 12, + }); assert.deepEqual(nextStep(book.records), { do: "adopt", round: 1 }); }); @@ -479,32 +1058,93 @@ test("正向串行:上次未启动起新编号;已登记未结束必须先 assert.deepEqual(nextStep(book.records), forward(C[0]!, 1)); recordForward(book, C[0]!, 1, "notStarted"); assert.deepEqual(nextStep(book.records), forward(C[0]!, 2)); - assert.deepEqual(runBalance(13, new Board(book.records).runStates()), { charged: 1, reserved: 0, balance: 12 }); + assert.deepEqual(runBalance(13, new Board(book.records).runStates()), { + charged: 1, + reserved: 0, + balance: 12, + }); const run = `r1-forward-${C[0]}-2`; - book.add("runPlanned", { run, purpose: "forward", round: 1, object: C[0]!, definition }); - assert.throws(() => nextStep(book.records), /登记了还没有结果:恢复入口应当已经核对过/); - assert.deepEqual(runBalance(13, new Board(book.records).runStates()), { charged: 1, reserved: 1, balance: 11 }); + book.add("runPlanned", { + run, + purpose: "forward", + round: 1, + object: C[0]!, + definition, + }); + assert.throws( + () => nextStep(book.records), + /登记了还没有结果:恢复入口应当已经核对过/, + ); + assert.deepEqual(runBalance(13, new Board(book.records).runStates()), { + charged: 1, + reserved: 1, + balance: 11, + }); book.add("runStarted", { run }); assert.throws(() => nextStep(book.records), /登记了还没有结果/); book.add("runResult", { run, process: exited, judged: judged(1.25, "main") }); assert.deepEqual(nextStep(book.records), forward(C[1]!, 1)); recordForward(book, C[1]!, 1, "timeout"); assert.deepEqual(nextStep(book.records), { do: "adopt", round: 1 }); - assert.deepEqual(runBalance(13, new Board(book.records).runStates()), { charged: 3, reserved: 0, balance: 10 }); + assert.deepEqual(runBalance(13, new Board(book.records).runStates()), { + charged: 3, + reserved: 0, + balance: 10, + }); const before = new Board(book.records); - assert.ok(before.runs().filter((r) => r.plan.purpose === "forward").every((r) => !before.runPublic(r))); - book.add("decision", { decision: "adopt", round: 1, rule: "adopt", ruleVersion: 1, params: null, seed: null, input: null, output: { adopted: null } }); + assert.ok( + before + .runs() + .filter((r) => r.plan.purpose === "forward") + .every((r) => !before.runPublic(r)), + ); + book.add("decision", { + decision: "adopt", + round: 1, + rule: "adopt", + ruleVersion: 1, + params: null, + seed: null, + input: null, + output: { adopted: null }, + }); const after = new Board(book.records); - assert.ok(after.runs().filter((r) => r.plan.purpose === "forward").every((r) => after.runPublic(r))); + assert.ok( + after + .runs() + .filter((r) => r.plan.purpose === "forward") + .every((r) => after.runPublic(r)), + ); // 没有采纳:不开解释阶段,直接开收尾(方法.md 4.3 ⑫)。 - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-wrapUp", stage: "wrapUp", round: 1, roster: R }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-wrapUp", + stage: "wrapUp", + round: 1, + roster: R, + }); }); test("采纳只有一个因子的候选:不做消融,直接开收尾(方法.md 4.3 ⑫)", () => { const book = firstVerification(batchTwo(1), true, true); for (const c of C) recordForward(book, c, 1, "valid"); - book.add("decision", { decision: "adopt", round: 1, rule: "adopt", ruleVersion: 1, params: null, seed: null, input: null, output: { adopted: C[0]! } }); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-wrapUp", stage: "wrapUp", round: 1, roster: R }); + book.add("decision", { + decision: "adopt", + round: 1, + rule: "adopt", + ruleVersion: 1, + params: null, + seed: null, + input: null, + output: { adopted: C[0]! }, + }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-wrapUp", + stage: "wrapUp", + round: 1, + roster: R, + }); }); // --------------------------------------------------------------------------- @@ -519,137 +1159,428 @@ const CELL2 = "r1-c2-p2"; function adoptedBook(): Book { const book = firstVerification(batchTwo(1), true, true); for (const c of C) recordForward(book, c, 1, "valid"); - return book.add("decision", { decision: "adopt", round: 1, rule: "adopt", ruleVersion: 1, params: null, seed: null, input: null, output: { adopted: ADOPTED } }); + return book.add("decision", { + decision: "adopt", + round: 1, + rule: "adopt", + ruleVersion: 1, + params: null, + seed: null, + input: null, + output: { adopted: ADOPTED }, + }); } /** 解释阶段:开、三人各交一份、关。 */ function explained(book: Book): Book { - return stage(book, "r1-explanation", "explanation", R.map((a) => [a, "explanation", null, null])); + return stage( + book, + "r1-explanation", + "explanation", + R.map((a) => [a, "explanation", null, null]), + ); } /** 选格决定。 */ function cellsChosen(book: Book, chosen: string[]): Book { - return book.add("decision", { decision: "cells", round: 1, rule: "valueMinusCost", ruleVersion: 1, params: null, seed: "s2", input: null, output: { chosen } }); + return book.add("decision", { + decision: "cells", + round: 1, + rule: "valueMinusCost", + ruleVersion: 1, + params: null, + seed: "s2", + input: null, + output: { chosen }, + }); } /** 一次消融实现阶段:subs 是 [格, cannotRun, alsoOff]。 */ -function implStage(book: Book, n: number, subs: Array<[string, string | null, string[]?]>): Book { +function implStage( + book: Book, + n: number, + subs: Array<[string, string | null, string[]?]>, +): Book { const stageId = `r1-ablationImplementation-${n}`; - book.open(stageId, "ablationImplementation", 1, subs.map(([cell]) => `experimenter-${cell}`)); + book.open( + stageId, + "ablationImplementation", + 1, + subs.map(([cell]) => `experimenter-${cell}`), + ); for (const [cell, cannotRun, alsoOff] of subs) { - book.add("submission", { stageId, author: `experimenter-${cell}`, kind: "ablationImplementation", target: cell, item: null, version: 1, content: { alsoOff: alsoOff ?? [], cannotRun, concerns: "无" }, jev: null }); + book.add("submission", { + stageId, + author: `experimenter-${cell}`, + kind: "ablationImplementation", + target: cell, + item: null, + version: 1, + content: { alsoOff: alsoOff ?? [], cannotRun, concerns: "无" }, + jev: null, + }); } return book.add("stageClose", { stageId }); } /** 一次消融核验阶段:subs 是 [格, [因子, 关没关]]。 */ -function verStage(book: Book, n: number, subs: Array<[string, Array<[string, boolean]>]>): Book { +function verStage( + book: Book, + n: number, + subs: Array<[string, Array<[string, boolean]>]>, +): Book { const stageId = `r1-ablationVerification-${n}`; - book.open(stageId, "ablationVerification", 1, subs.map(([cell]) => `verifier-${cell}`)); + book.open( + stageId, + "ablationVerification", + 1, + subs.map(([cell]) => `verifier-${cell}`), + ); for (const [cell, offs] of subs) { - book.add("submission", { stageId, author: `verifier-${cell}`, kind: "ablationVerification", target: cell, item: null, version: 1, content: { factors: offs.map(([factor, off]) => ({ factor, off, where: "initial.py:1" })) }, jev: null }); + book.add("submission", { + stageId, + author: `verifier-${cell}`, + kind: "ablationVerification", + target: cell, + item: null, + version: 1, + content: { + factors: offs.map(([factor, off]) => ({ + factor, + off, + where: "initial.py:1", + })), + }, + jev: null, + }); } return book.add("stageClose", { stageId }); } /** 一次消融运行:没启动,或启动了、判分算数。 */ -function recordAblation(book: Book, cell: string, n: number, ending: "valid" | "notStarted"): Book { +function recordAblation( + book: Book, + cell: string, + n: number, + ending: "valid" | "notStarted", +): Book { const run = `r1-ablation-${cell}-${n}`; - book.add("runPlanned", { run, purpose: "ablation", round: 1, object: cell, definition }); - if (ending === "notStarted") return book.add("runResult", { run, process: { ending, reason: "Docker 未启动" }, judged: null }); + book.add("runPlanned", { + run, + purpose: "ablation", + round: 1, + object: cell, + definition, + }); + if (ending === "notStarted") + return book.add("runResult", { + run, + process: { ending, reason: "Docker 未启动" }, + judged: null, + }); book.add("runStarted", { run }); - return book.add("runResult", { run, process: exited, judged: judged(1.2, "main") }); + return book.add("runResult", { + run, + process: exited, + judged: judged(1.2, "main"), + }); } test("采纳多因子改法:先开解释阶段,每人一个任务、没有对象;关闭后选格", () => { const book = adoptedBook(); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-explanation", stage: "explanation", round: 1, roster: R }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-explanation", + stage: "explanation", + round: 1, + roster: R, + }); book.open("r1-explanation", "explanation", 1, R); - assert.deepEqual(nextStep(book.records), { do: "dispatch", stageId: "r1-explanation", tasks: R.map((actor) => ({ actor, kind: "explanation", object: null })) }); + assert.deepEqual(nextStep(book.records), { + do: "dispatch", + stageId: "r1-explanation", + tasks: R.map((actor) => ({ actor, kind: "explanation", object: null })), + }); for (const a of R) book.sub("r1-explanation", a, "explanation", null, null); - assert.deepEqual(nextStep(book.records), { do: "closeStage", stageId: "r1-explanation" }); + assert.deepEqual(nextStep(book.records), { + do: "closeStage", + stageId: "r1-explanation", + }); book.add("stageClose", { stageId: "r1-explanation" }); assert.deepEqual(nextStep(book.records), { do: "selectCells", round: 1 }); }); test("选格做完:每格固定一位实验员;交了跑不起来的格不核验、不运行,没启动的下次起新编号", () => { const book = cellsChosen(explained(adoptedBook()), ["p1", "p2"]); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-ablationImplementation-1", stage: "ablationImplementation", round: 1, roster: [`experimenter-${CELL1}`, `experimenter-${CELL2}`] }); - implStage(book, 1, [[CELL1, null], [CELL2, "离了 p2 跑不起来"]]); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-ablationVerification-1", stage: "ablationVerification", round: 1, roster: [`verifier-${CELL1}`] }); - verStage(book, 1, [[CELL1, [["p1", true], ["p2", false]]]]); - assert.deepEqual(nextStep(book.records), { do: "ablationRun", round: 1, cell: CELL1, run: `r1-ablation-${CELL1}-1` }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-ablationImplementation-1", + stage: "ablationImplementation", + round: 1, + roster: [`experimenter-${CELL1}`, `experimenter-${CELL2}`], + }); + implStage(book, 1, [ + [CELL1, null], + [CELL2, "离了 p2 跑不起来"], + ]); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-ablationVerification-1", + stage: "ablationVerification", + round: 1, + roster: [`verifier-${CELL1}`], + }); + verStage(book, 1, [ + [ + CELL1, + [ + ["p1", true], + ["p2", false], + ], + ], + ]); + assert.deepEqual(nextStep(book.records), { + do: "ablationRun", + round: 1, + cell: CELL1, + run: `r1-ablation-${CELL1}-1`, + }); recordAblation(book, CELL1, 1, "notStarted"); - assert.deepEqual(nextStep(book.records), { do: "ablationRun", round: 1, cell: CELL1, run: `r1-ablation-${CELL1}-2` }); + assert.deepEqual(nextStep(book.records), { + do: "ablationRun", + round: 1, + cell: CELL1, + run: `r1-ablation-${CELL1}-2`, + }); recordAblation(book, CELL1, 2, "valid"); assert.deepEqual(nextStep(book.records), { do: "settle", round: 1 }); }); test("核验不通过:退回原实验员重做,只重验重做的格;次数用完写不跑事件,不登记运行", () => { const book = cellsChosen(explained(adoptedBook()), ["p1", "p2"]); - implStage(book, 1, [[CELL1, null], [CELL2, null]]); - verStage(book, 1, [[CELL1, [["p1", false], ["p2", false]]], [CELL2, [["p1", false], ["p2", true]]]]); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-ablationImplementation-2", stage: "ablationImplementation", round: 1, roster: [`experimenter-${CELL1}`] }); + implStage(book, 1, [ + [CELL1, null], + [CELL2, null], + ]); + verStage(book, 1, [ + [ + CELL1, + [ + ["p1", false], + ["p2", false], + ], + ], + [ + CELL2, + [ + ["p1", false], + ["p2", true], + ], + ], + ]); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-ablationImplementation-2", + stage: "ablationImplementation", + round: 1, + roster: [`experimenter-${CELL1}`], + }); implStage(book, 2, [[CELL1, null]]); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-ablationVerification-2", stage: "ablationVerification", round: 1, roster: [`verifier-${CELL1}`] }); - verStage(book, 2, [[CELL1, [["p1", false], ["p2", false]]]]); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-ablationVerification-2", + stage: "ablationVerification", + round: 1, + roster: [`verifier-${CELL1}`], + }); + verStage(book, 2, [ + [ + CELL1, + [ + ["p1", false], + ["p2", false], + ], + ], + ]); const next = nextStep(book.records); assert.equal(next.do, "cellNotRun"); if (next.do !== "cellNotRun") throw new Error("应写不跑事件"); assert.equal(next.cell, CELL1); assert.match(next.reason, /第 2 次核验仍不通过,修复次数上限 1 已用完/); - book.add("event", { event: "cellNotRun", reason: next.reason, objects: [next.cell] }); + book.add("event", { + event: "cellNotRun", + reason: next.reason, + objects: [next.cell], + }); // p1 不跑不扣次;只跑 p2。 - assert.deepEqual(nextStep(book.records), { do: "ablationRun", round: 1, cell: CELL2, run: `r1-ablation-${CELL2}-1` }); + assert.deepEqual(nextStep(book.records), { + do: "ablationRun", + round: 1, + cell: CELL2, + run: `r1-ablation-${CELL2}-1`, + }); }); test("几格实际关掉的因子完全相同:沿用第一格,不重复跑", () => { const book = cellsChosen(explained(adoptedBook()), ["p1", "p2"]); - implStage(book, 1, [[CELL1, null, ["p2"]], [CELL2, null, ["p1"]]]); - verStage(book, 1, [[CELL1, [["p1", true], ["p2", true]]], [CELL2, [["p1", true], ["p2", true]]]]); + implStage(book, 1, [ + [CELL1, null, ["p2"]], + [CELL2, null, ["p1"]], + ]); + verStage(book, 1, [ + [ + CELL1, + [ + ["p1", true], + ["p2", true], + ], + ], + [ + CELL2, + [ + ["p1", true], + ["p2", true], + ], + ], + ]); // 两格实际关的都是 {p1, p2}:只跑第一格。 - assert.deepEqual(nextStep(book.records), { do: "ablationRun", round: 1, cell: CELL1, run: `r1-ablation-${CELL1}-1` }); + assert.deepEqual(nextStep(book.records), { + do: "ablationRun", + round: 1, + cell: CELL1, + run: `r1-ablation-${CELL1}-1`, + }); recordAblation(book, CELL1, 1, "valid"); assert.deepEqual(nextStep(book.records), { do: "settle", round: 1 }); }); test("全部交跑不起来:不核验、不运行,直接结算", () => { const book = cellsChosen(explained(adoptedBook()), ["p1", "p2"]); - implStage(book, 1, [[CELL1, "跑不起来一"], [CELL2, "跑不起来二"]]); + implStage(book, 1, [ + [CELL1, "跑不起来一"], + [CELL2, "跑不起来二"], + ]); assert.deepEqual(nextStep(book.records), { do: "settle", round: 1 }); }); /** 结算和押的因子写完、收尾关闭、解封也写完的账本(余额还有 8 次)。 */ function afterWrapUp(): Book { const book = cellsChosen(explained(adoptedBook()), ["p1", "p2"]); - implStage(book, 1, [[CELL1, null], [CELL2, null]]); - verStage(book, 1, [[CELL1, [["p1", true], ["p2", false]]], [CELL2, [["p1", false], ["p2", true]]]]); + implStage(book, 1, [ + [CELL1, null], + [CELL2, null], + ]); + verStage(book, 1, [ + [ + CELL1, + [ + ["p1", true], + ["p2", false], + ], + ], + [ + CELL2, + [ + ["p1", false], + ["p2", true], + ], + ], + ]); recordAblation(book, CELL1, 1, "valid"); recordAblation(book, CELL2, 1, "valid"); - book.add("settlement", { round: 1, result: { candidate: ADOPTED, input: null, output: null } }); - book.add("decision", { decision: "bet", round: 1, rule: "chooseBet", ruleVersion: 1, params: null, seed: null, input: null, output: null }); - return stage(book, "r1-wrapUp", "wrapUp", R.map((a) => [a, "wrapUp", null, null])); + book.add("settlement", { + round: 1, + result: { candidate: ADOPTED, input: null, output: null }, + }); + book.add("decision", { + decision: "bet", + round: 1, + rule: "chooseBet", + ruleVersion: 1, + params: null, + seed: null, + input: null, + output: null, + }); + return stage( + book, + "r1-wrapUp", + "wrapUp", + R.map((a) => [a, "wrapUp", null, null]), + ); } test("结算和押的因子写完:开收尾;收尾关闭后解封本轮选中的全部候选,然后开下一轮", () => { const book = cellsChosen(explained(adoptedBook()), ["p1", "p2"]); - implStage(book, 1, [[CELL1, null], [CELL2, null]]); - verStage(book, 1, [[CELL1, [["p1", true], ["p2", false]]], [CELL2, [["p1", false], ["p2", true]]]]); + implStage(book, 1, [ + [CELL1, null], + [CELL2, null], + ]); + verStage(book, 1, [ + [ + CELL1, + [ + ["p1", true], + ["p2", false], + ], + ], + [ + CELL2, + [ + ["p1", false], + ["p2", true], + ], + ], + ]); recordAblation(book, CELL1, 1, "valid"); - assert.deepEqual(nextStep(book.records), { do: "ablationRun", round: 1, cell: CELL2, run: `r1-ablation-${CELL2}-1` }); + assert.deepEqual(nextStep(book.records), { + do: "ablationRun", + round: 1, + cell: CELL2, + run: `r1-ablation-${CELL2}-1`, + }); recordAblation(book, CELL2, 1, "valid"); assert.deepEqual(nextStep(book.records), { do: "settle", round: 1 }); - book.add("settlement", { round: 1, result: { candidate: ADOPTED, input: null, output: null } }); - book.add("decision", { decision: "bet", round: 1, rule: "chooseBet", ruleVersion: 1, params: null, seed: null, input: null, output: null }); - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-wrapUp", stage: "wrapUp", round: 1, roster: R }); - stage(book, "r1-wrapUp", "wrapUp", R.map((a) => [a, "wrapUp", null, null])); + book.add("settlement", { + round: 1, + result: { candidate: ADOPTED, input: null, output: null }, + }); + book.add("decision", { + decision: "bet", + round: 1, + rule: "chooseBet", + ruleVersion: 1, + params: null, + seed: null, + input: null, + output: null, + }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-wrapUp", + stage: "wrapUp", + round: 1, + roster: R, + }); + stage( + book, + "r1-wrapUp", + "wrapUp", + R.map((a) => [a, "wrapUp", null, null]), + ); // 收尾关闭后:解封本轮选批选中的 r1-c1、r1-c2;还有一个没解封就继续。 assert.deepEqual(nextStep(book.records), { do: "unsealChosen", round: 1 }); book.add("unseal", { candidate: "r1-c1", seals: [1] }); assert.deepEqual(nextStep(book.records), { do: "unsealChosen", round: 1 }); book.add("unseal", { candidate: "r1-c2", seals: [2] }); // 余额还有(已扣 5 次):开第 2 轮。 - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r2-proposal", stage: "proposal", round: 2, roster: R }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r2-proposal", + stage: "proposal", + round: 2, + roster: R, + }); }); test("最后一次正向用掉全部余额:进行中的这一轮照常走完采纳与收尾,不开下一轮", () => { @@ -663,9 +1594,24 @@ test("最后一次正向用掉全部余额:进行中的这一轮照常走完 study.config = { ...fullConfig(), runs: 3 } as unknown as Json; // 这一轮已经开始(r1- 的阶段都在账上):下一步是写采纳决定,不是停下。 assert.deepEqual(nextStep(book.records), { do: "adopt", round: 1 }); - book.add("decision", { decision: "adopt", round: 1, rule: "adopt", ruleVersion: 1, params: null, seed: null, input: null, output: { adopted: null } }); + book.add("decision", { + decision: "adopt", + round: 1, + rule: "adopt", + ruleVersion: 1, + params: null, + seed: null, + input: null, + output: { adopted: null }, + }); // 没有采纳:不做消融,照样开收尾阶段。 - assert.deepEqual(nextStep(book.records), { do: "openStage", stageId: "r1-wrapUp", stage: "wrapUp", round: 1, roster: R }); + assert.deepEqual(nextStep(book.records), { + do: "openStage", + stageId: "r1-wrapUp", + stage: "wrapUp", + round: 1, + roster: R, + }); }); test("余额为 0:不开下一轮,走到导出停下,原因写明", () => { @@ -676,13 +1622,26 @@ test("余额为 0:不开下一轮,走到导出停下,原因写明", () => const record = book.records.find((r) => r.kind === "config")!; const study = record.content as unknown as StudyRecord; study.config = { ...fullConfig(), runs: 5 } as unknown as Json; - assert.deepEqual(nextStep(book.records), { do: "halt", reason: "导出还没写", objects: [] }); + assert.deepEqual(nextStep(book.records), { + do: "halt", + reason: "导出还没写", + objects: [], + }); }); test("配置关掉消融、又采纳了多因子改法:先记按配置跳过,再在解释停下", () => { const book = firstVerification(batchTwo(1), true, true); for (const c of C) recordForward(book, c, 1, "valid"); - book.add("decision", { decision: "adopt", round: 1, rule: "adopt", ruleVersion: 1, params: null, seed: null, input: null, output: { adopted: ADOPTED } }); + book.add("decision", { + decision: "adopt", + round: 1, + rule: "adopt", + ruleVersion: 1, + params: null, + seed: null, + input: null, + output: { adopted: ADOPTED }, + }); // 冻结的配置改为关掉消融:配置和步骤清单一起换(步骤清单在研究开始时冻结)。 const config = fullConfig(); if (config.flow.preset !== "full") throw new Error("测试配置应是完整流程"); @@ -693,8 +1652,16 @@ test("配置关掉消融、又采纳了多因子改法:先记按配置跳过 study.config = config as unknown as Json; study.steps = stepList(config) as unknown as Json; const reason = "配置关掉了消融"; - assert.deepEqual(nextStep(book.records), { do: "skip", step: "r1-listCells", reason }); - book.add("event", { event: "skippedByConfig", reason, objects: ["r1-listCells"] }); + assert.deepEqual(nextStep(book.records), { + do: "skip", + step: "r1-listCells", + reason, + }); + book.add("event", { + event: "skippedByConfig", + reason, + objects: ["r1-listCells"], + }); const next = nextStep(book.records); assert.equal(next.do, "halt"); assert.match((next as { reason: string }).reason, /不交三个数的解释还没写/);