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 = Extract; export function ofKind(records: LedgerRecord[], kind: K): Array> { return records.filter((r): r is Of => 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> { const latest = new Map>(); 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): Entry[] { const lists = new Map(); const used = new Map(); 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); }