import { copyFileSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { DatabaseSync } from "node:sqlite"; import { entryId } from "../../src/materials/index.ts"; import type { ReplayItem, ReplayScript } from "../lib/replay.ts"; // 从参考项目 16-real-1 的账本取第零轮的提交,按任务书 12 第 5.3 节转成新项目的格式,写成回放脚本。 // 原账本只读:先复制到临时目录,再以只读方式打开复制件,原目录里不写任何东西。 // node eval/round-zero/extract-16.ts const SOURCE = "/home/iomgaa/rb-acceptance/16-real-1/board.sqlite"; export const REPLAY_FILE = "eval/round-zero/replay-16-real-1.json"; const PHASE_STAGE: Record = { "entry-draft": "entryDraft", "entry-discuss": "discussion", "entry-claim": "claim" }; const copy = join(mkdtempSync(join(tmpdir(), "inforsi-rb16-")), "board.sqlite"); copyFileSync(SOURCE, copy); const db = new DatabaseSync(copy, { readOnly: true }); type OldEntry = { name: string; startFrom: string; whyWorth: string }; const rows = db .prepare( `SELECT st.phase, st.id AS stage, s.actor, s.version, s.created_seq AS seq, s.payload FROM submissions s JOIN stages st ON st.id = s.stage_id WHERE st.phase IN ('entry-draft', 'entry-discuss', 'entry-claim') ORDER BY s.created_seq`, ) .all() as Array<{ phase: string; stage: string; actor: string; version: number; seq: number; payload: string }>; const fact = db.prepare("SELECT payload FROM facts WHERE kind = 'entry-assignment'").all() as Array<{ payload: string }>; db.close(); if (fact.length !== 1) throw new Error(`entry-assignment 应有 1 条,实有 ${fact.length} 条`); /** 当年入口 {name, startFrom, whyWorth} → 新项目 {name, body}。 */ const entry = (e: OldEntry) => ({ name: e.name, body: `从哪里开始看:${e.startFrom}\n\n为什么值得走:${e.whyWorth}` }); /** 当年入口编号 researcher-1-e4 → 新项目 e-researcher-1-4。 */ function newId(old: string): string { const m = /^(researcher-\d+)-e(\d+)$/.exec(old); if (m === null) throw new Error(`入口编号 ${old} 不是 researcher--e 的写法`); return entryId(m[1]!, Number(m[2])); } const models: Record = {}; for (const r of rows) { const p = JSON.parse(r.payload) as { entries?: OldEntry[]; body?: string; choices?: string[] }; const payload = r.phase === "entry-draft" ? { entries: p.entries!.map(entry) } : r.phase === "entry-discuss" ? { body: p.body!, ...(p.entries === undefined ? {} : { entries: p.entries.map(entry) }) } : { choices: p.choices!.map(newId) }; (models[r.actor] ??= []).push({ from: `${r.stage} ${PHASE_STAGE[r.phase]} seq ${r.seq} v${r.version}`, arguments: { payload } }); } const old = JSON.parse(fact[0]!.payload) as { assignments: Array<{ researcher: string; entryId: string; rank: number | null }>; entries: Array<{ entryId: string; author: string }> }; const expected = old.assignments.map((a) => ({ researcher: a.researcher, entry: newId(a.entryId), rank: a.rank })); const script: ReplayScript & { expected: typeof expected; expectedEntries: string[] } = { source: `${SOURCE}(第零轮 submissions,created_seq ${rows[0]!.seq}–${rows.at(-1)!.seq})`, models, expected, expectedEntries: old.entries.map((e) => newId(e.entryId)), }; writeFileSync(REPLAY_FILE, `${JSON.stringify(script, null, 2)}\n`); // 核对:三阶段各几份、作者、版本,以及当年的分配。 console.log(`共 ${rows.length} 份提交,序号 ${rows[0]!.seq}–${rows.at(-1)!.seq}`); for (const phase of Object.keys(PHASE_STAGE)) { const these = rows.filter((r) => r.phase === phase); console.log(`${phase}:${these.length} 份;${these.map((r) => `${r.actor}@${r.seq}(v${r.version}${JSON.parse(r.payload).entries ? `,${JSON.parse(r.payload).entries.length} 个入口` : ""})`).join("、")}`); } console.log(`当年分配:${expected.map((a) => `${a.researcher} → ${a.entry}(第 ${a.rank} 志愿)`).join(";")}`); console.log(`当年最终入口:${script.expectedEntries.join("、")}`); console.log(`已写 ${REPLAY_FILE}`);