feat: flow 第 1 部分(骨架、研究开始、第零轮)与第零轮检验

按任务书 12 实现 src/flow:从账本现算下一步的纯函数、主循环、恢复入口、派模型任务和跑正式运行两个共用动作、冻结配置、基线、第零轮三个研究者阶段和入口分配,走到第 1 轮时停下。materials 导出 finalEntries、加 researcherId 和 StudyRecord。

审阅采纳两条:研究编号本机已用过时冻结前报错;docker run 没启动时写完结果就停下,不在本进程里无限起新编号。

eval/ 新增第零轮检验:起研究命令行、回放假模型、检验用 Jev 客户端、六问质量评估、16-real-1 回放、杀进程与两份同时跑。tsc 与 depcruise 也检查 eval/,新增 eval 只经 index.ts 用正式代码的规则。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 10:58:52 -04:00
parent 8491191a2b
commit 75d7cffd27
36 changed files with 2456 additions and 14 deletions
+21
View File
@@ -0,0 +1,21 @@
import { ORIGINAL_BASELINE } from "../../materials/index.ts";
import { saveVersion } from "../../workspace/index.ts";
import { formalRun, type RunOutcome } from "../formal-run.ts";
import { workspaceRepo } from "../paths.ts";
import type { Study } from "../study.ts";
// 基线(方法.md 4.1):存任务的基线代码,写代码记录,再用共用动作二跑一次;每份研究都跑,计入运行次数。
/** 存任务的基线目录("不存"清单取冻结的任务事实),写代码记录:对象 ORIGINAL_BASELINE,位置表 null。 */
export function saveBaseline(s: Study): void {
const saved = saveVersion(workspaceRepo(s.dir), s.task.baselineDir, s.frozen.facts.unversionedNames);
if (!saved.saved) throw new Error(`任务的基线目录 ${s.task.baselineDir} 存不成版本:${saved.path},${saved.reason}`);
s.ledger.append("code", { object: ORIGINAL_BASELINE, version: saved.version, locations: null });
}
/** 跑一次基线运行。基线不测假设专用指标。 */
export function baselineRun(s: Study, run: string): Promise<RunOutcome> {
const code = s.ledger.records().findLast((r) => r.kind === "code" && r.content.object === ORIGINAL_BASELINE);
const version = (code!.content as { version: string }).version;
return formalRun(s, { run, purpose: "baseline", round: 0, object: null }, version, []);
}
+44
View File
@@ -0,0 +1,44 @@
import { finalEntries } from "../../materials/index.ts";
import { assignEntries as assign } from "../../rules/index.ts";
import type { Json } from "../../vocab/index.ts";
import { runModelTask } from "../model-task.ts";
import type { Next } from "../next-step.ts";
import { halt, USER_STOPPED, type Study, type StudyEnding } from "../study.ts";
// 第零轮(方法.md 4.2):三个研究者阶段派任务,认领之后由程序分配入口。
/**
* 派一个研究者阶段里要派的人,同时跑,各自一个会话(任务书 12 第 2.6 节)。第零轮的提交种类交上来之后没有要另算的东西。
* 有人没交上(没交或出错),等这次派发的会话全部结束后停下,不打断别人(第 2.4 节)。
* 某个会话抛出内部错误时,也等别人结束再把它抛出去。
*/
export async function dispatchStage(s: Study, next: Extract<Next, { do: "dispatch" }>): Promise<StudyEnding | null> {
const settled = await Promise.allSettled(
next.actors.map((actor) => runModelTask(s, { kind: next.kind, actor, stageId: next.stageId, object: null, known: () => null })),
);
const failure = settled.find((r) => r.status === "rejected");
if (failure !== undefined) throw failure.reason;
if (s.stop.aborted) return USER_STOPPED;
const results = settled.map((r) => (r as PromiseFulfilledResult<Awaited<ReturnType<typeof runModelTask>>>).value);
const missed = results.flatMap(({ call, ending }) =>
ending.status === "notSubmitted" || ending.status === "failed" ? [{ call, text: `${ending.status === "failed" ? "出错" : "没交"}:${ending.reason}` }] : [],
);
if (missed.length === 0) return null;
return halt(s.ledger, `阶段 ${next.stageId} 有人没交上,等这次派发的会话全部结束后停下:${missed.map((m) => `调用 ${m.call},${m.text}`).join(";")}`, missed.map((m) => m.call));
}
/**
* 分配(方法.md 4.2"分配规则"):每人最后一份认领和最终有效的入口交给 rules 的 assignEntries,写一条入口分配决定。
* assignEntries 不在规则登记表里,规则名就写它,版本 1;没有方法参数,也不用随机。
*/
export function assignEntries(s: Study): void {
const roster = s.ledger.records().find((r) => r.kind === "stageOpen" && r.content.stage === "claim" && r.content.round === 0);
const claim = roster!.content as { stageId: string; roster: string[] };
const researchers = claim.roster.map((id) => {
const last = s.ledger.latestSubmission({ stageId: claim.stageId, author: id, kind: "claim", target: null, item: null });
return { id, choices: (last!.content.content as { choices: string[] }).choices };
});
const entries = finalEntries(s.ledger).map((e) => ({ id: e.id, author: e.author }));
const input = { researchers, entries };
s.ledger.append("decision", { decision: "entries", round: 0, rule: "assignEntries", ruleVersion: 1, params: null, seed: null, input, output: assign(input) as unknown as Json });
}