Files
InfoRSI/tests/flow-ablation-steps.test.ts
T
iomgaa 144b88480b flow: 消融到收尾、轮末解封、一轮接一轮与研究结束(任务书 17 第二块)
- ⑬–⑱:listCells 从采纳决定和冻结协议现算(不写记录);explanation 有消融才开
  阶段;selectCells 照 CellRuleInput 拼输入(余额当时现算、decisionSeed 版本 2),
  写种类 cells 的决定,没选中的格不另写事件;消融实现/核验按第几次编号
  (r<轮>-ablationImplementation-1、verification-1、-2……从账本数),重做从上一次
  这一格代码接着改,次数用完写 cellNotRunEvent 不跑不扣次;cannotRun 传 null 不核验
  不运行不扣次;alsoOff 用 sharedSettings 沿用同设置的第一格;消融运行一个接一个跑,
  用途 ablation,登记/扣次/恢复与正向同一入口,恢复时指标缺失策略覆盖为只记缺失
  (方法.md 第五部分);settle 拼 SettleInput 调 settleAblation,结算与 chooseBet 的
  bet 决定写在同一事务;wrapUp 阶段关闭后,本轮选中的每个候选在同一个事务里解封
  作者自评(取最后一版提案的封存)。
- 一轮接一轮:每轮开头用 runBalance 现算余额,余额为 0 不开新轮;走到 export 停下,
  原因"导出还没写"。去掉 roundsNext 里 step.id !== "listCells" 的临时写法。
- 消融中发现由 materials 的 Board.baselineOf 从结算输出读出,flow 不另写采纳记录
  (已核实 materials 已实现)。
- 测试 +14:下一步分支 8 条(含收尾关闭后才解封、余额 0 走到导出停、关掉消融时
  解释照方法停下)、消融步骤 6 条(选格输入逐字段手拼对账、结算四种结局、同设置
  沿用、解封取最后一版封存);npm run check 通过,npm test 472 条通过。
- 全新上下文审阅一轮(只看消融重做循环、登记扣次恢复、选格/结算/押的因子输入、
  解封时机、下一轮与结束条件):没有会污染账本的问题;采纳一条小修(删掉未用的
  导入),其余笔记照列不改。

flow 1302 → 1662 行(上限 3500),materials 未动(1930 行)。
2026-10-04 14:04:55 -04:00

208 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { selectCells, settle, unsealChosen } from "../src/flow/steps/ablation.ts";
import type { Study } from "../src/flow/study.ts";
import { Ledger } from "../src/ledger/index.ts";
import { Board } from "../src/materials/index.ts";
import { chooseBet, decisionSeed, runCellRule, settleAblation, type AblationModelInput, type CellRuleInput, type SettleInput } from "../src/rules/index.ts";
import { checkTaskFacts } from "../src/tasks/index.ts";
import { fullConfig } from "./config-helpers.ts";
import { definition, exited, proposal, tempDir } from "./ledger-helpers.ts";
import { judged, r, Writer } from "./materials-helpers.ts";
// 选格、结算与押的因子、轮末解封(任务书 17 第 2.2 节):在真实账本里写到上一步做完,跑这一步,看写下的记录。
// 预期值按条文手拼:选格输入照 rules 的 CellRuleInput 逐项拼,结算的各格结局照方法.md 6.6 的情形拼;
// 输出用 rules 的函数当对照(同选批测试的写法,rules 自己有独立预期值的测试)。
const config = fullConfig();
const facts = checkTaskFacts(JSON.parse(readFileSync(join(import.meta.dirname, "..", "tasks", "circle_packing", "task.json"), "utf8")));
function study(t: TestContext): { s: Study; w: Writer } {
const ledger = Ledger.open(join(tempDir(t), "ledger.db"));
t.after(() => ledger.close());
const frozen = { config: checkedFull(config), steps: stepList(config), facts, prompts: null, image: "", seed: 1 };
return { s: { ledger, frozen } as unknown as Study, w: new Writer(ledger) };
}
/** 基线 0.96、被采纳候选 r1-c1(两个因子)完整代码 1.25。 */
function adoptedBase(w: Writer): void {
w.l.append("code", { object: "base", version: "v-base", locations: null });
w.run("base-1", "baseline", 0, null, judged(0.96, "main"));
w.l.append("code", { object: "r1-c1", version: "v-full", locations: null });
w.run("r1-forward-r1-c1-1", "forward", 1, "r1-c1", judged(1.25, "main"));
w.protocol("r1-c1", { factors: [{ name: "因子一" }, { name: "因子二" }], safeguards: [], metrics: { defined: [], reused: [] }, body: "正文", clauses: [], round: 1, baseline: "base", proposals: ["r1-a1-1"] } as never);
w.decision("adopt", 1, null, { adopted: "r1-c1" });
}
const EXPLANATIONS = [
{ id: "r1-a1-x1", author: "a1", cells: [{ factor: "p1", range: r(1.0, 1.1, 1.2) }, { factor: "p2", range: r(1.15, 1.2, 1.24) }] },
{ id: "r1-a2-x1", author: "a2", cells: [{ factor: "p1", range: r(0.9, 1.05, 1.2) }, { factor: "p2", range: r(1.1, 1.18, 1.25) }] },
] as const;
/** 解释阶段关闭:a1、a2 各交一条,对两格都给了三个数。 */
function explain(w: Writer): void {
w.open("r1-explanation", "explanation", 1, ["a1", "a2"]);
for (const e of EXPLANATIONS) {
w.sub("r1-explanation", e.author, "explanation", null, null, { explanations: [{ body: `机制(${e.author})`, cells: e.cells.map((c) => ({ factor: c.factor, range: c.range })) }] });
}
w.close("r1-explanation");
}
/** 成分模型的输入:选格、结算和押的因子共用的那一份。 */
function modelInput(): AblationModelInput {
return { baseline: 0.96, full: 1.25, direction: "maximize", knownBest: facts.knownBest, epsilon: facts.epsilon, lambda: 0.1, factors: ["p1", "p2"], explanations: EXPLANATIONS.map((e) => ({ id: e.id, author: e.author, cells: e.cells.map((c) => ({ factor: c.factor, range: c.range })) })) };
}
test("选格:输入照 CellRuleInput 拼(基线、完整分数、各条解释、余额从运行记录算、种子用选格的版本 2),规则取步骤清单", (t) => {
const { s, w } = study(t);
adoptedBase(w);
explain(w);
selectCells(s, 1);
const record = s.ledger.records().find((x) => x.kind === "decision" && x.content.decision === "cells")!;
const seed = decisionSeed({ studyId: "circle-full-1", round: 1, version: 2 });
const input: CellRuleInput = {
...modelInput(),
// 13 次里基线和正向各确认启动 1 次,余额 11。
balance: 11,
batchCap: 2,
u: 0.5,
decay: 0.5,
seed,
};
const { rule: used, ...output } = runCellRule("valueMinusCost", input);
assert.deepEqual(record.content, { decision: "cells", round: 1, rule: used.name, ruleVersion: used.version, params: output.params, seed, input, output });
});
/** 一次消融实现阶段:subs 是 [格, cannotRun, alsoOff];cannotRun 为 null 表示交了代码。 */
function impl(w: Writer, n: number, subs: Array<[string, string | null, string[]?]>): void {
w.open(`r1-ablationImplementation-${n}`, "ablationImplementation", 1, subs.map(([cell]) => `experimenter-${cell}`));
for (const [cell, cannotRun, alsoOff] of subs) {
w.sub(`r1-ablationImplementation-${n}`, `experimenter-${cell}`, "ablationImplementation", cell, null, { alsoOff: alsoOff ?? [], cannotRun, concerns: "无" });
}
w.close(`r1-ablationImplementation-${n}`);
}
/** 结算一次:拼出 SettleInput,跑 settle,核对 settlement 和 bet 两条记录。 */
function settled(t: TestContext, prepare: (w: Writer) => void, cells: SettleInput["cells"]): void {
const { s, w } = study(t);
adoptedBase(w);
explain(w);
w.decision("cells", 1, null, { chosen: ["p1", "p2"] });
prepare(w);
settle(s, 1);
const input: SettleInput = { ...modelInput(), cells };
const betInput = { ...modelInput(), cells };
const settlement = s.ledger.records().find((x) => x.kind === "settlement")!;
assert.deepEqual(settlement.content, { round: 1, result: { candidate: "r1-c1", input, output: settleAblation(input) } });
const bet = s.ledger.records().find((x) => x.kind === "decision" && x.content.decision === "bet")!;
assert.deepEqual(bet.content, {
decision: "bet",
round: 1,
rule: "chooseBet",
ruleVersion: 1,
params: { direction: "maximize", epsilon: facts.epsilon, knownBest: facts.knownBest, lambda: 0.1 },
seed: null,
input: betInput,
output: chooseBet(betInput),
});
}
test("结算:有效分数、没测、跑不起来、超时记证据不足,各按 6.6 的结局拼", (t) => {
settled(
t,
(w) => {
impl(w, 1, [["r1-c1-p1", null], ["r1-c1-p2", null]]);
w.run("r1-ablation-r1-c1-p1-1", "ablation", 1, "r1-c1-p1", judged(1.1, "main"));
// p2 启动了但跑崩: judged 为 null,记证据不足(扣了次数)。
w.l.append("runPlanned", { run: "r1-ablation-r1-c1-p2-1", purpose: "ablation", round: 1, object: "r1-c1-p2", definition });
w.l.append("runStarted", { run: "r1-ablation-r1-c1-p2-1" });
w.l.append("runResult", { run: "r1-ablation-r1-c1-p2-1", process: exited, judged: null });
},
[
{ factor: "p1", outcome: { kind: "valid", score: 1.1 } },
{ factor: "p2", outcome: { kind: "insufficient" } },
],
);
});
test("结算:没选中的格记没测;交了跑不起来的记跑不起来", (t) => {
const { s, w } = study(t);
adoptedBase(w);
explain(w);
w.decision("cells", 1, null, { chosen: ["p1"] });
impl(w, 1, [["r1-c1-p1", "离了 p1 跑不起来"]]);
settle(s, 1);
const input: SettleInput = {
...modelInput(),
cells: [
{ factor: "p1", outcome: { kind: "cannotRun" } },
{ factor: "p2", outcome: { kind: "untested" } },
],
};
const settlement = s.ledger.records().find((x) => x.kind === "settlement")!;
assert.deepEqual(settlement.content, { round: 1, result: { candidate: "r1-c1", input, output: settleAblation(input) } });
});
test("结算:核验不通过、次数用完有事件的记证据不足;设置相同的两格都记一起关,沿用第一格的分数", (t) => {
settled(
t,
(w) => {
impl(w, 1, [["r1-c1-p1", null, ["p2"]]]);
w.run("r1-ablation-r1-c1-p1-1", "ablation", 1, "r1-c1-p1", judged(1.05, "main"));
impl(w, 2, [["r1-c1-p2", null]]);
w.l.append("event", { event: "cellNotRun", reason: "第 2 次核验仍不通过,修复次数上限 1 已用完", objects: ["r1-c1-p2"] });
},
[
{ factor: "p1", outcome: { kind: "joint", off: ["p1", "p2"], score: 1.05 } },
{ factor: "p2", outcome: { kind: "insufficient" } },
],
);
});
test("结算:设置相同的两格都记一起关,沿用第一格的分数", (t) => {
settled(
t,
(w) => {
impl(w, 1, [["r1-c1-p1", null, ["p2"]]]);
w.run("r1-ablation-r1-c1-p1-1", "ablation", 1, "r1-c1-p1", judged(1.05, "main"));
impl(w, 2, [["r1-c1-p2", null, ["p1"]]]);
},
[
{ factor: "p1", outcome: { kind: "joint", off: ["p1", "p2"], score: 1.05 } },
{ factor: "p2", outcome: { kind: "joint", off: ["p2", "p1"], score: 1.05 } },
],
);
});
test("轮末解封:解封本轮选中的候选的全部作者自评(取最后一版的封存),没选中的继续封存", (t) => {
const { s, w } = study(t);
w.open("r1-proposal", "proposal", 1, ["a1", "a2"]);
w.sub("r1-proposal", "a1", "proposal", null, "r1-a1-1", proposal(r(1.0, 1.1, 1.2), "第一版"));
const v2 = s.ledger.append("submission", {
stageId: "r1-proposal",
author: "a1",
kind: "proposal",
target: null,
item: "r1-a1-1",
version: 2,
content: proposal(r(1.05, 1.15, 1.25), "第二版"),
jev: null,
} as never);
w.sub("r1-proposal", "a2", "proposal", null, "r1-a2-1", proposal(r(0.9, 1.0, 1.1)));
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.decision("batch", 1, null, { chosen: ["r1-c1"] });
unsealChosen(s, 1);
const unseals = s.ledger.records().filter((x) => x.kind === "unseal");
assert.equal(unseals.length, 1);
assert.equal(unseals[0]!.content.candidate, "r1-c1");
// 解封的是最后一版(v2)的封存。
const seal = s.ledger.records().find((x) => x.kind === "seal" && x.content.proposal === v2.seq)!;
assert.deepEqual(unseals[0]!.content.seals, [seal.seq]);
assert.deepEqual(new Board(s.ledger.records()).unsealed(), [{ candidate: "r1-c1", author: "a1", range: r(1.05, 1.15, 1.25) }]);
});