feat: flow 第 2 部分,第 r 轮开轮到选批(任务书 14 第二块)
- next-step 按"第 r 轮"列出提案、第一次预测、挑刺、回应与统一、第二次预测的任务 (一人对一个对象),判重、冻结协议、选批三步,走到 implementation 停下(任务书 15) - steps/dispatch.ts:同阶段一人任务先后跑、不同人同时跑;提案、挑刺按 proposalId、critiqueId 分份 - steps/round.ts:判重两两问 Jev、连通成组、并进留池候选;冻结协议在回应阶段关闭后一个事务写全; 选批照 rules 的 BatchInput 拼,种子用 decisionSeed 版本 1(选格预留版本 2) - materials 导出 Board(构造改收记录数组,给纯函数用);讨论原文里已作废的入口编号就地标"(已作废)" - 删 SOURCES.md 里参考项目实测事实那一段(用户 2026-10-03 同意) - 测试:下一步做什么 21 条、判重连组 4 条、冻结与选批 2 条、作废标注 1 条;npm test 437 通过 全新上下文审阅一轮(只看三个程序步骤与恢复):没有会污染账本的问题。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+159
-41
@@ -1,6 +1,8 @@
|
||||
import type { StudyStepId } from "../config/index.ts";
|
||||
import type { KindOf } from "../agents/index.ts";
|
||||
import type { RoundStepId, StudyStepId } from "../config/index.ts";
|
||||
import type { Content, LedgerRecord } from "../ledger/index.ts";
|
||||
import { ORIGINAL_BASELINE, researcherId } from "../materials/index.ts";
|
||||
import { Board, ORIGINAL_BASELINE, researcherId } from "../materials/index.ts";
|
||||
import { compareIds } from "../rules/index.ts";
|
||||
import type { Stage } from "../vocab/index.ts";
|
||||
import { readFrozen, type Frozen } from "./frozen.ts";
|
||||
|
||||
@@ -19,16 +21,18 @@ const ROUND_ZERO_STAGES = {
|
||||
claim: { stage: "claim", kind: "claim" },
|
||||
} as const satisfies Partial<Record<StudyStepId, { stage: Stage; kind: string }>>;
|
||||
type RoundZeroStep = keyof typeof ROUND_ZERO_STAGES;
|
||||
export type RoundZeroKind = (typeof ROUND_ZERO_STAGES)[RoundZeroStep]["kind"];
|
||||
|
||||
/** 阶段编号由 flow 起:第零轮是 r0-<步骤>,一个步骤一个阶段。 */
|
||||
export const stageIdOf = (step: RoundZeroStep): string => `r0-${step}`;
|
||||
/** 第 r 轮的研究者阶段:步骤名就是阶段名(vocab STAGES)。 */
|
||||
type RoundStageStep = Extract<RoundStepId, "proposal" | "firstPrediction" | "critique" | "response" | "secondPrediction">;
|
||||
|
||||
/** 阶段编号由 flow 起:r<轮>-<步骤>,一个步骤一个阶段。第 r 轮按配置跳过的步骤也用这个编号记"按配置跳过"。 */
|
||||
export const stageIdOf = (round: number, step: string): string => `r${round}-${step}`;
|
||||
/** 基线运行编号:没启动就起下一个编号重跑(docs/结构.md 第四部分)。 */
|
||||
export const baselineRunId = (n: number): string => `base-${n}`;
|
||||
|
||||
/** flow 写的事件种类(账本记录 event)。 */
|
||||
export const EVENTS = {
|
||||
/** 按配置跳过的步骤,objects 是步骤编号,reason 是 config 给的原因。 */
|
||||
/** 按配置跳过的步骤,objects 是步骤编号(第 r 轮的步骤写 r<轮>-<步骤>),reason 是 config 给的原因。 */
|
||||
skipped: "skippedByConfig",
|
||||
/** 研究停下,reason 是停下的原因,objects 是相关的调用或运行。 */
|
||||
halted: "halted",
|
||||
@@ -36,15 +40,21 @@ export const EVENTS = {
|
||||
runStatusUnknown: "runStatusUnknown",
|
||||
} as const;
|
||||
|
||||
/** 一个研究者任务:一个人对一个对象(候选、原提案;提案和第零轮的任务没有对象)交一种提交。 */
|
||||
export type TaskRef = { actor: string; kind: KindOf<"researcher">; object: string | null };
|
||||
|
||||
export type Next =
|
||||
| { do: "freezeConfig" }
|
||||
| { do: "saveBaseline" }
|
||||
| { do: "baselineRun"; run: string }
|
||||
| { do: "skip"; step: StudyStepId; reason: string }
|
||||
| { do: "openStage"; stageId: string; stage: Stage; roster: string[] }
|
||||
| { do: "skip"; step: string; reason: string }
|
||||
| { do: "openStage"; stageId: string; stage: Stage; round: number; roster: string[] }
|
||||
| { do: "closeStage"; stageId: string }
|
||||
| { do: "dispatch"; stageId: string; kind: RoundZeroKind; actors: string[] }
|
||||
| { do: "dispatch"; stageId: string; tasks: TaskRef[] }
|
||||
| { do: "assignEntries" }
|
||||
| { do: "dedupe"; round: number }
|
||||
| { do: "freezeProtocols"; round: number }
|
||||
| { do: "selectBatch"; round: number }
|
||||
| { do: "halt"; reason: string; objects: string[] };
|
||||
|
||||
export function nextStep(records: readonly LedgerRecord[]): Next {
|
||||
@@ -53,19 +63,20 @@ export function nextStep(records: readonly LedgerRecord[]): Next {
|
||||
if (frozen === null) return { do: "freezeConfig" };
|
||||
const view = new View(records);
|
||||
for (const step of frozen.steps.study) {
|
||||
if (step.status === "skip") {
|
||||
// 按配置跳过的步骤 → 一条"按配置跳过"事件,写明步骤和原因。
|
||||
if (!view.events(EVENTS.skipped).some((e) => e.objects[0] === step.id)) return { do: "skip", step: step.id, reason: step.reason };
|
||||
continue;
|
||||
}
|
||||
const next = stepNext(step.id, view, frozen);
|
||||
const next =
|
||||
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("步骤清单走完了却没有停下:rounds 一步应当停下");
|
||||
}
|
||||
|
||||
/** 一个要执行的步骤还要做什么;做完了交回 null。 */
|
||||
function stepNext(step: StudyStepId, view: View, frozen: Frozen): Next | null {
|
||||
/** 按配置跳过的步骤 → 一条"按配置跳过"事件,写明步骤和原因。 */
|
||||
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 };
|
||||
}
|
||||
|
||||
/** 整份研究的一个要执行的步骤还要做什么;做完了交回 null。 */
|
||||
function studyStepNext(step: Exclude<StudyStepId, "rounds">, view: View, frozen: Frozen): Next | null {
|
||||
switch (step) {
|
||||
case "freezeConfig":
|
||||
return null;
|
||||
@@ -74,18 +85,107 @@ function stepNext(step: StudyStepId, view: View, frozen: Frozen): Next | null {
|
||||
case "entryDraft":
|
||||
case "discussion1":
|
||||
case "discussion2":
|
||||
case "claim":
|
||||
return stageNext(step, view, frozen);
|
||||
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);
|
||||
}
|
||||
case "assignEntries":
|
||||
// 分配 → 第 0 轮的入口分配决定(decision,种类 entries)。
|
||||
return view.of("decision").some((d) => d.decision === "entries" && d.round === 0) ? null : { do: "assignEntries" };
|
||||
case "rounds":
|
||||
return { do: "halt", reason: "第 1 轮还没写(任务书 13)", objects: [] };
|
||||
return view.board.decision("entries", 0) === undefined ? { do: "assignEntries" } : null;
|
||||
case "export":
|
||||
throw new Error("走到了导出:rounds 一步应当先停下");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 一轮接一轮(结构.md 第三部分"每一轮"):第一个没做完的轮次里第一个没做完的步骤。
|
||||
* 每一步做完的记录都带轮次,做完的轮次整轮跳过;现在走到实现一步就停下,所以循环一定会返回。
|
||||
*/
|
||||
function roundsNext(view: View, frozen: Frozen): Next {
|
||||
for (let round = 1; ; round++) {
|
||||
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);
|
||||
if (next !== null) return next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 第 round 轮的一个要执行的步骤还要做什么;做完了交回 null。 */
|
||||
function roundStepNext(step: RoundStepId, round: number, view: View, frozen: Frozen): Next | null {
|
||||
switch (step) {
|
||||
case "openRound":
|
||||
case "carryOverPool":
|
||||
// 不写记录:本轮名单和提案上限就是冻结的配置(方法.md 4.3 ①),留池候选由 materials 从记录算出(Board.carried)。
|
||||
return null;
|
||||
case "proposal":
|
||||
case "firstPrediction":
|
||||
case "critique":
|
||||
case "response":
|
||||
case "secondPrediction":
|
||||
return stageNext(stageIdOf(round, step), step, round, (roster) => roundTasks(step, round, roster, view.board), view, frozen);
|
||||
case "dedupe":
|
||||
// 判重合并 → 本轮的判重合并决定(decision,种类 dedup)。
|
||||
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: [] };
|
||||
}
|
||||
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 };
|
||||
}
|
||||
case "selectBatch":
|
||||
// 选批 → 本轮的选批决定(decision,种类 batch)。
|
||||
return view.board.decision("batch", round) === undefined ? { do: "selectBatch", round } : null;
|
||||
case "implementation":
|
||||
return { do: "halt", reason: "实现还没写(任务书 15)", objects: [] };
|
||||
default:
|
||||
throw new Error(`走到了第 ${round} 轮的 ${step}:implementation 一步应当先停下`);
|
||||
}
|
||||
}
|
||||
|
||||
/** 第 round 轮判重合并出的新候选,以及各合并了哪几条原提案。 */
|
||||
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)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 第 r 轮一个研究者阶段的任务(任务书 14 第 2.3 节)。同一个人的几个任务按这里的先后跑。
|
||||
* - 提案:每人一个任务,没有对象;
|
||||
* - 第一次预测:每人 × 本轮每个新候选(含自己的;留池候选不做,方法.md 4.3 ④);
|
||||
* - 挑刺:每人 × 本轮每条不是自己写的原提案(监督者定,照参考项目);
|
||||
* - 回应与统一:每个新候选一个任务,一条原提案的由作者交回应,几条合成的由其中编号最小的作者交统一(方法.md 4.3 ③⑥);
|
||||
* - 第二次预测:每人 × 候选池里每个候选。
|
||||
*/
|
||||
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 })));
|
||||
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));
|
||||
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! })));
|
||||
}
|
||||
case "response":
|
||||
return freshOf(board, round).map((c) => ({
|
||||
actor: board.authorsOf(c.candidate)[0]!,
|
||||
kind: c.proposals.length === 1 ? "response" : "unification",
|
||||
object: c.candidate,
|
||||
}));
|
||||
case "secondPrediction":
|
||||
return each("secondPrediction", poolOf(board, round));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基线 → 基线代码记录(对象 ORIGINAL_BASELINE),而且最近一次基线运行的结果不是"没启动"。
|
||||
* 没启动的起新编号重跑;判为不算数的停下(方法.md 4.1)。
|
||||
@@ -104,28 +204,34 @@ function baselineNext(view: View): Next | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* 研究者阶段 → 这个阶段的关闭记录(stageClose)。没开就开;名单上的人都有被收下的提交才关;
|
||||
* 没交上的人按最近一次调用怎么结束的来定派不派(任务书 12 第 2.3 节的表)。能派的先全派,派完还有人停在"没交"才停下。
|
||||
* 研究者阶段 → 这个阶段的关闭记录(stageClose)。没开就开,名单是全部研究者(本轮名单就是冻结的配置);
|
||||
* 每个任务(一个人对一个对象)都有被收下的提交才关;没做完的任务按它最近一次调用怎么结束的来定派不派
|
||||
* (任务书 12 第 2.3 节的表)。能派的先全派,派完还有任务停在"没交"才停下。
|
||||
*/
|
||||
function stageNext(step: RoundZeroStep, view: View, frozen: Frozen): Next | null {
|
||||
const stageId = stageIdOf(step);
|
||||
const { stage, kind } = ROUND_ZERO_STAGES[step];
|
||||
const open = view.of("stageOpen").find((s) => s.stageId === stageId);
|
||||
function stageNext(
|
||||
stageId: string,
|
||||
stage: Stage,
|
||||
round: number,
|
||||
tasksOf: (roster: readonly string[]) => TaskRef[],
|
||||
view: View,
|
||||
frozen: Frozen,
|
||||
): Next | null {
|
||||
const open = view.board.stages.get(stageId);
|
||||
if (open === undefined) {
|
||||
return { do: "openStage", stageId, stage, roster: frozen.config.models.researchers.map((_, i) => researcherId(i)) };
|
||||
return { do: "openStage", stageId, stage, round, roster: frozen.config.models.researchers.map((_, i) => researcherId(i)) };
|
||||
}
|
||||
if (view.of("stageClose").some((s) => s.stageId === stageId)) return null;
|
||||
const missing = open.roster.filter((a) => !view.of("submission").some((s) => s.stageId === stageId && s.author === a));
|
||||
if (missing.length === 0) return { do: "closeStage", stageId };
|
||||
const actors: string[] = [];
|
||||
if (open.closed) return null;
|
||||
const todo = tasksOf(open.roster).filter((t) => !view.done(stageId, t));
|
||||
if (todo.length === 0) return { do: "closeStage", stageId };
|
||||
const tasks: TaskRef[] = [];
|
||||
const stuck: Array<{ call: string; reason: string }> = [];
|
||||
for (const actor of missing) {
|
||||
const last = view.lastCall(stageId, actor);
|
||||
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 });
|
||||
else actors.push(actor);
|
||||
else tasks.push(t);
|
||||
}
|
||||
if (actors.length > 0) return { do: "dispatch", stageId, kind, actors };
|
||||
if (tasks.length > 0) return { do: "dispatch", stageId, tasks };
|
||||
return {
|
||||
do: "halt",
|
||||
reason: `交不出合格提交(方法.md 4.3 ②:按内部错误停下):${stuck.map((s) => `调用 ${s.call},${s.reason}`).join(";")}`,
|
||||
@@ -133,12 +239,14 @@ function stageNext(step: RoundZeroStep, view: View, frozen: Frozen): Next | null
|
||||
};
|
||||
}
|
||||
|
||||
/** 按种类取记录内容的小工具。 */
|
||||
/** 按种类取记录内容的小工具,加上 materials 读账本的那一份(Board)。 */
|
||||
class View {
|
||||
readonly #records: readonly LedgerRecord[];
|
||||
readonly board: Board;
|
||||
|
||||
constructor(records: readonly LedgerRecord[]) {
|
||||
this.#records = records;
|
||||
this.board = new Board(records);
|
||||
}
|
||||
|
||||
of<K extends LedgerRecord["kind"]>(kind: K): Array<Content<K>> {
|
||||
@@ -149,9 +257,19 @@ class View {
|
||||
return this.of("event").filter((e) => e.event === event);
|
||||
}
|
||||
|
||||
/** 某人在某阶段最近一次模型调用的结束记录,没有调用为 null。开始了没结束的,恢复入口应当已经补写。 */
|
||||
lastCall(stageId: string, actor: string): Content<"callEnded"> | null {
|
||||
const started = this.of("callStarted").filter((c) => c.stageId === stageId && c.actor === actor).at(-1);
|
||||
/** 一个任务做完了:这个人在这个阶段对这个对象有一份被收下的提交(任务书 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个任务最近一次模型调用的结束记录,没有调用为 null。"模型调用开始"里存的材料带着对象,按它认出是哪个任务。
|
||||
* 开始了没结束的,恢复入口应当已经补写。
|
||||
*/
|
||||
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)
|
||||
.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} 开始了还没有结束记录:恢复入口应当已经补写`);
|
||||
|
||||
Reference in New Issue
Block a user