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:
@@ -0,0 +1,89 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { Ledger, type LedgerRecord } from "../../src/ledger/index.ts";
|
||||
import { entryId, type DecisionShapes } from "../../src/materials/index.ts";
|
||||
|
||||
// 读账本的小工具:只经 src/ledger/index.ts 打开、只读记录,不写。
|
||||
// 入口编号用 materials 的 entryId,和正式代码同一条编号规则。
|
||||
|
||||
/** 研究目录里账本的全部记录。账本还没有时报错:Ledger.open 会新建一个空账本,检验不能替 flow 建。 */
|
||||
export function readLedger(studyDir: string): LedgerRecord[] {
|
||||
const path = join(studyDir, "ledger.sqlite");
|
||||
if (!existsSync(path)) throw new Error(`${path} 还不存在`);
|
||||
const ledger = Ledger.open(path);
|
||||
try {
|
||||
return ledger.records();
|
||||
} finally {
|
||||
ledger.close();
|
||||
}
|
||||
}
|
||||
|
||||
export type Of<K extends LedgerRecord["kind"]> = Extract<LedgerRecord, { kind: K }>;
|
||||
export function ofKind<K extends LedgerRecord["kind"]>(records: LedgerRecord[], kind: K): Array<Of<K>> {
|
||||
return records.filter((r): r is Of<K> => r.kind === kind);
|
||||
}
|
||||
|
||||
export interface StageInfo {
|
||||
stageId: string;
|
||||
stage: string;
|
||||
round: number;
|
||||
roster: string[];
|
||||
closed: boolean;
|
||||
}
|
||||
|
||||
export function stages(records: LedgerRecord[]): StageInfo[] {
|
||||
const closed = new Set(ofKind(records, "stageClose").map((r) => r.content.stageId));
|
||||
return ofKind(records, "stageOpen").map(({ content: c }) => ({ ...c, closed: closed.has(c.stageId) }));
|
||||
}
|
||||
|
||||
/** 每份提交(阶段、作者、种类、对象、编号)的最新一版,按这一版的序号排。 */
|
||||
export function finalSubmissions(records: LedgerRecord[]): Array<Of<"submission">> {
|
||||
const latest = new Map<string, Of<"submission">>();
|
||||
for (const r of ofKind(records, "submission")) {
|
||||
const c = r.content;
|
||||
const slot = JSON.stringify([c.stageId, c.author, c.kind, c.target, c.item]);
|
||||
const seen = latest.get(slot);
|
||||
if (seen === undefined || seen.content.version < c.version) latest.set(slot, r);
|
||||
}
|
||||
return [...latest.values()].sort((a, b) => a.seq - b.seq);
|
||||
}
|
||||
|
||||
export interface Entry {
|
||||
id: string;
|
||||
author: string;
|
||||
name: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 只看给定阶段里的提交,各人最后一版入口清单里的入口(方法.md 4.2:讨论时整份替换的新清单接着往后编号)。
|
||||
* 给第零轮全部阶段就是分配时的"入口全体";只给写入口阶段就是闭门写完时的入口全体。
|
||||
*/
|
||||
export function entriesIn(records: LedgerRecord[], stageIds: ReadonlySet<string>): Entry[] {
|
||||
const lists = new Map<string, Entry[]>();
|
||||
const used = new Map<string, number>();
|
||||
for (const s of finalSubmissions(records)) {
|
||||
const c = s.content;
|
||||
if (!stageIds.has(c.stageId) || (c.kind !== "entry" && c.kind !== "discussion")) continue;
|
||||
const entries = (c.content as { entries?: Array<{ name: string; body: string }> }).entries;
|
||||
if (entries === undefined) continue;
|
||||
const n = used.get(c.author) ?? 0;
|
||||
lists.set(c.author, entries.map((e, i) => ({ id: entryId(c.author, n + i + 1), author: c.author, ...e })));
|
||||
used.set(c.author, n + entries.length);
|
||||
}
|
||||
return [...lists.values()].flat();
|
||||
}
|
||||
|
||||
export type Assignment = DecisionShapes["entries"]["output"][number];
|
||||
|
||||
/** 第 0 轮的入口分配决定:输入(全部志愿和入口)和输出(分配结果);没有就是 undefined。 */
|
||||
export function entryDecision(records: LedgerRecord[]): { input: { entries: Array<{ id: string; author: string }> }; output: Assignment[] } | undefined {
|
||||
const d = ofKind(records, "decision").find((r) => r.content.decision === "entries" && r.content.round === 0);
|
||||
return d === undefined ? undefined : (d.content as unknown as { input: { entries: Array<{ id: string; author: string }> }; output: Assignment[] });
|
||||
}
|
||||
|
||||
/** 两份分配是否相同:每人分到的入口和第几志愿都一样,不看先后和对象里键的顺序。 */
|
||||
export function sameAssignment(a: Assignment[], b: Assignment[]): boolean {
|
||||
const key = (xs: Assignment[]) => xs.map((x) => `${x.researcher} ${x.entry} ${x.rank}`).sort().join("\n");
|
||||
return key(a) === key(b);
|
||||
}
|
||||
Reference in New Issue
Block a user