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>
This commit is contained in:
2026-10-03 13:59:54 -04:00
parent 2c6fdce1ca
commit f98d664a16
17 changed files with 835 additions and 133 deletions
+66
View File
@@ -0,0 +1,66 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { groupSameChange, type SamePair } from "../src/flow/steps/round.ts";
// 判重合并怎么连组(任务书 14 第 2.4 节):判为相同的连成一组,组里有留池候选就并进它,
// 新候选按组里编号最小的原提案编号。预期值按规则手写。
const pair = (a: string, b: string, yes: boolean): SamePair => ({
a,
b,
judgment: { question: "same_change", questionHash: "h", threshold: 0.5, probability: yes ? 0.9 : 0.1, yes, requestId: "req" },
});
test("A 与 B 相同、B 与 C 相同:A、B、C 连成一个候选,即使 A 与 C 判为不同;新候选按组里最小的原提案编号", () => {
const ps = ["r1-researcher-1-1", "r1-researcher-1-2", "r1-researcher-2-1", "r1-researcher-3-1"];
const pairs = [
pair("r1-researcher-1-1", "r1-researcher-1-2", false),
pair("r1-researcher-1-1", "r1-researcher-2-1", false),
pair("r1-researcher-1-1", "r1-researcher-3-1", false),
pair("r1-researcher-1-2", "r1-researcher-2-1", false),
pair("r1-researcher-1-2", "r1-researcher-3-1", true),
pair("r1-researcher-2-1", "r1-researcher-3-1", true),
];
assert.deepEqual(groupSameChange(1, ps, [], pairs), {
fresh: [
{ candidate: "r1-c1", proposals: ["r1-researcher-1-1"] },
{ candidate: "r1-c2", proposals: ["r1-researcher-1-2", "r1-researcher-2-1", "r1-researcher-3-1"] },
],
carried: [],
});
});
test("编号按数字大小比:researcher-10 排在 researcher-2 后面", () => {
const ps = ["r1-researcher-10-1", "r1-researcher-2-1"];
assert.deepEqual(groupSameChange(1, ps, [], [pair(ps[0]!, ps[1]!, false)]).fresh, [
{ candidate: "r1-c1", proposals: ["r1-researcher-2-1"] },
{ candidate: "r1-c2", proposals: ["r1-researcher-10-1"] },
]);
});
test("和留池候选相同的提案并进它,连带和这条提案相同的别的提案;没并进提案的留池候选列空数组", () => {
const ps = ["r2-researcher-1-1", "r2-researcher-2-1", "r2-researcher-3-1"];
const pairs = [
pair("r2-researcher-1-1", "r2-researcher-2-1", true),
pair("r2-researcher-1-1", "r2-researcher-3-1", false),
pair("r2-researcher-2-1", "r2-researcher-3-1", false),
pair("r2-researcher-1-1", "r1-c1", false),
pair("r2-researcher-1-1", "r1-c3", false),
pair("r2-researcher-2-1", "r1-c1", false),
pair("r2-researcher-2-1", "r1-c3", true),
pair("r2-researcher-3-1", "r1-c1", false),
pair("r2-researcher-3-1", "r1-c3", false),
];
assert.deepEqual(groupSameChange(2, ps, ["r1-c3", "r1-c1"], pairs), {
fresh: [{ candidate: "r2-c1", proposals: ["r2-researcher-3-1"] }],
carried: [
{ candidate: "r1-c1", proposals: [] },
{ candidate: "r1-c3", proposals: ["r2-researcher-1-1", "r2-researcher-2-1"] },
],
});
});
test("一组连到两个留池候选:按内部错误停下", () => {
const pairs = [pair("r2-researcher-1-1", "r1-c1", true), pair("r2-researcher-1-1", "r1-c2", true)];
assert.throws(() => groupSameChange(2, ["r2-researcher-1-1"], ["r1-c1", "r1-c2"], pairs), /留池候选 r1-c1、r1-c2 连成了一组/);
});