75d7cffd27
按任务书 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>
90 lines
4.0 KiB
TypeScript
90 lines
4.0 KiB
TypeScript
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);
|
|
}
|