import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { test, type TestContext } from "node:test"; import { stepList } from "../src/config/index.ts"; import { fullConfig as checkedFull } from "../src/flow/frozen.ts"; import { freezeProtocols, selectBatch } from "../src/flow/steps/round.ts"; import type { Study } from "../src/flow/study.ts"; import { Ledger, type LedgerRecord } from "../src/ledger/index.ts"; import { decisionSeed, runBatchRule } from "../src/rules/index.ts"; import { checkTaskFacts } from "../src/tasks/index.ts"; import { fullConfig } from "./config-helpers.ts"; import { tempDir } from "./ledger-helpers.ts"; import { judged, P_A1, P_A2, r, Writer } from "./materials-helpers.ts"; // 冻结协议和选批两步(任务书 14 第 2.4 节):在真实账本里写到回应阶段关闭、第二次预测关闭,跑这一步,看写下的记录。 // 预期值按条文手写:协议主体取回应叠原提案、指标改全名、基线和合并的原提案;选批输入照 rules 的 BatchInput 逐项手拼。 // 这两步只读账本和冻结的配置,Study 里别的几样(Docker、提示词)用不到。 const config = fullConfig(); const facts = checkTaskFacts(JSON.parse(readFileSync(join(import.meta.dirname, "..", "tasks", "circle_packing", "task.json"), "utf8"))); const CRITIQUE = "r1-a1-1.a2.1"; function studyAfterResponse(t: TestContext): Study { const ledger = Ledger.open(join(tempDir(t), "ledger.db")); t.after(() => ledger.close()); const w = new Writer(ledger); ledger.append("code", { object: "base", version: "v-base", locations: null }); w.run("base-1", "baseline", 0, null, judged(0.96, "main")); w.open("r1-proposal", "proposal", 1, ["a1", "a2"]); w.sub("r1-proposal", "a1", "proposal", null, "r1-a1-1", P_A1); w.sub("r1-proposal", "a2", "proposal", null, "r1-a2-1", P_A2); w.close("r1-proposal"); w.decision("dedup", 1, null, { fresh: [{ candidate: "r1-c1", proposals: ["r1-a1-1"] }, { candidate: "r1-c2", proposals: ["r1-a2-1"] }], carried: [] }); w.open("r1-critique", "critique", 1, ["a1", "a2"]); w.sub("r1-critique", "a2", "critique", "r1-a1-1", CRITIQUE, { target: "p2", body: "乙的挑刺" }); w.close("r1-critique"); w.open("r1-response", "response", 1, ["a1", "a2"]); // a1 第一版只答复,第二版改了正文:冻结取最后一版。 w.sub("r1-response", "a1", "response", "r1-c1", null, { replies: [{ critique: CRITIQUE, reply: "保留" }], changes: {}, clauses: ["旧条款"], body: "无" }); ledger.append("submission", { stageId: "r1-response", author: "a1", kind: "response", target: "r1-c1", item: null, version: 2, content: { replies: [{ critique: CRITIQUE, reply: "改了正文" }], changes: { body: "甲改过的正文" }, clauses: ["加多起点", "加扰动"], body: "答复正文" }, jev: null, }); w.sub("r1-response", "a2", "response", "r1-c2", null, { replies: [], changes: {}, clauses: ["加修复"], body: "无" }); w.close("r1-response"); const frozen = { config: checkedFull(config), steps: stepList(config), facts, prompts: null, image: "", seed: 1 }; return { ledger, frozen } as unknown as Study; } const protocols = (records: readonly LedgerRecord[]) => records.filter((x) => x.kind === "protocol").map((x) => x.content); test("冻结协议:回应阶段关闭后,每个新候选取最后一版回应叠在原提案上,指标改全名,写明基线和合并的原提案;同一个事务", (t) => { const s = studyAfterResponse(t); freezeProtocols(s, 1); assert.deepEqual(protocols(s.ledger.records()), [ { candidate: "r1-c1", protocol: { factors: P_A1.factors, safeguards: ["guard"], metrics: { defined: [{ ...P_A1.metrics.defined[0]!, id: "r1-c1.restart-count" }], reused: [] }, body: "甲改过的正文", clauses: ["加多起点", "加扰动"], round: 1, baseline: "base", proposals: ["r1-a1-1"], }, }, { candidate: "r1-c2", protocol: { factors: P_A2.factors, safeguards: [], metrics: { defined: [{ ...P_A2.metrics.defined[0]!, id: "r1-c2.restart-count" }], reused: [] }, body: "乙的提案正文", clauses: ["加修复"], round: 1, baseline: "base", proposals: ["r1-a2-1"], }, }, ]); }); test("选批:输入照 BatchInput 拼(各人最后一版第二次预测、本批基线分数、余额从运行记录算、种子用选批的版本),规则取步骤清单", (t) => { const s = studyAfterResponse(t); freezeProtocols(s, 1); const w = new Writer(s.ledger); w.open("r1-secondPrediction", "secondPrediction", 1, ["a1", "a2"]); w.sub("r1-secondPrediction", "a1", "secondPrediction", "r1-c1", null, { range: r(1, 1.1, 1.2), body: "第一版" }); s.ledger.append("submission", { stageId: "r1-secondPrediction", author: "a1", kind: "secondPrediction", target: "r1-c1", item: null, version: 2, content: { range: r(1.05, 1.15, 1.25), body: "改过" }, jev: null, }); w.sub("r1-secondPrediction", "a2", "secondPrediction", "r1-c1", null, { range: r(1, 1.1, 1.2), body: "理由" }); w.sub("r1-secondPrediction", "a1", "secondPrediction", "r1-c2", null, { range: r(0.9, 0.97, 1), body: "理由" }); w.sub("r1-secondPrediction", "a2", "secondPrediction", "r1-c2", null, { range: r(0.9, 0.96, 1), body: "理由" }); w.close("r1-secondPrediction"); selectBatch(s, 1); const decision = s.ledger.records().find((x) => x.kind === "decision" && x.content.decision === "batch")!.content as Record; const seed = decisionSeed({ studyId: "circle-full-1", round: 1, version: 1 }); const input = { candidates: [ { id: "r1-c1", ranges: [{ researcher: "a1", range: r(1.05, 1.15, 1.25) }, { researcher: "a2", range: r(1, 1.1, 1.2) }] }, { id: "r1-c2", ranges: [{ researcher: "a1", range: r(0.9, 0.97, 1) }, { researcher: "a2", range: r(0.9, 0.96, 1) }] }, ], baseline: 0.96, direction: "maximize", epsilon: facts.epsilon, lambda: 0.1, batchCap: 2, // 13 次里基线运行确认启动、扣了 1 次。 balance: 12, seed, } as const; const { rule: _rule, ...output } = runBatchRule("bestExpected", input); assert.deepEqual(decision, { decision: "batch", round: 1, rule: "bestExpected", ruleVersion: 1, params: { epsilon: facts.epsilon, lambda: 0.1, batchCap: 2, balance: 12 }, seed, input, output }); });