Files
InfoRSI/tests/flow-round-steps.test.ts
T
iomgaa f98d664a16 feat: flow 第 2 部分,第 r 轮开轮到选批(任务书 14 第二块)
- next-step 按"第 r 轮"列出提案、第一次预测、挑刺、回应与统一、第二次预测的任务
  (一人对一个对象),判重、冻结协议、选批三步,走到 implementation 停下(任务书 15)
- steps/dispatch.ts:同阶段一人任务先后跑、不同人同时跑;提案、挑刺按 proposalId、critiqueId 分份
- steps/round.ts:判重两两问 Jev、连通成组、并进留池候选;冻结协议在回应阶段关闭后一个事务写全;
  选批照 rules 的 BatchInput 拼,种子用 decisionSeed 版本 1(选格预留版本 2)
- materials 导出 Board(构造改收记录数组,给纯函数用);讨论原文里已作废的入口编号就地标"(已作废)"
- 删 SOURCES.md 里参考项目实测事实那一段(用户 2026-10-03 同意)
- 测试:下一步做什么 21 条、判重连组 4 条、冻结与选批 2 条、作废标注 1 条;npm test 437 通过

全新上下文审阅一轮(只看三个程序步骤与恢复):没有会污染账本的问题。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:59:54 -04:00

132 lines
6.4 KiB
TypeScript

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<string, unknown>;
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 });
});