feat: 配置 config 模块(任务书 04)

- src/config:research.json 的字段定义(TypeBox,全部必填、不认识的字段报错,每个字段带中文说明和类别)、一次列出全部错误的校验、步骤清单纯函数。
- 流程写成完整流程 / 朴素循环两选一,第零轮、消融写成开 / 关两选一,冲突组合写不出来,不另写检查。
- 规则名取自 rules 登记表,config 改为依赖 rules(结构.md 第二部分、.dependency-cruiser.cjs)。
- ledger 顺手两处(结构.md 第四部分):消融结算按轮次唯一、去掉消融编号;私人笔记去掉版本号;表结构版本号升到 2。
- 结构.md 第八部分补 config 对外内容、更新 ledger 一节;WORK_LOG 更新。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 08:08:18 -04:00
parent 4dc7ac9d62
commit d2bfb971ab
13 changed files with 1045 additions and 20 deletions
+28 -7
View File
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { test } from "node:test";
import { Ledger } from "../src/ledger/index.ts";
import { Ledger, type LedgerRecord } from "../src/ledger/index.ts";
import { hash } from "../src/vocab/index.ts";
import {
callStart,
@@ -116,12 +116,33 @@ test("唯一约束:同一次模型调用结束两次报错", (t) => {
assert.equal(countRows(path), 2);
});
test("唯一约束:同一次消融结算两次报错", (t) => {
test("唯一约束:同一轮结算两次报错,不同轮各一次照写;结算不带消融编号", (t) => {
// 每轮最多采纳一个改法,所以最多一次消融(docs/结构.md 第四部分):结算按轮次只发生一次。
const { ledger, path } = freshLedger(t);
ledger.append("settlement", { ablation: "a1", result: { cells: { p1: "valid" } } });
assert.throws(() => ledger.append("settlement", { ablation: "a1", result: { cells: { p1: "valid" } } }), /settlement_once/);
ledger.append("settlement", { ablation: "a2", result: { cells: { p1: "insufficient" } } });
assert.equal(countRows(path), 2);
ledger.append("settlement", { round: 1, result: { cells: { p1: "valid" } } });
assert.throws(() => ledger.append("settlement", { round: 1, result: { cells: { p1: "insufficient" } } }), /settlement_once/);
ledger.append("settlement", { round: 2, result: { cells: { p1: "insufficient" } } });
ledger.append("settlement", { round: 3, result: { cells: { p1: "valid" } } });
assert.throws(() => ledger.append("settlement", { ablation: "a1", result: {} } as never), /settlement/);
assert.throws(() => ledger.append("settlement", { round: 4, ablation: "a4", result: {} } as never), /settlement/);
assert.equal(countRows(path), 3);
});
test("私人笔记:同一人写两条都存下,以后写的(序号大的)为准;笔记不带版本号", (t) => {
// 笔记不属于任何阶段,以最后写入的一条为准,序号就是先后(docs/结构.md 第四部分)。
// 后写的正文按字典序排在前面,中间夹着别人的笔记:取"序号最大的那条"才得到后写的。
const { ledger, path } = freshLedger(t);
ledger.append("note", { author: "r1", body: "z 先写的" });
ledger.append("note", { author: "r2", body: "别人的笔记" });
ledger.append("note", { author: "r1", body: "a 后写的" });
const mine = ledger
.records()
.filter((r): r is LedgerRecord<"note"> => r.kind === "note")
.filter((r) => r.content.author === "r1");
assert.deepEqual(mine.map((r) => [r.seq, r.content.body]), [[1, "z 先写的"], [3, "a 后写的"]]);
assert.equal(mine.at(-1)!.content.body, "a 后写的");
assert.throws(() => ledger.append("note", { author: "r1", version: 2, body: "带版本号" } as never), /note/);
assert.equal(countRows(path), 3);
});
test("没有唯一约束的记录照写:同样的事件、决定写两次就是两条", (t) => {
@@ -233,7 +254,7 @@ test("关掉再打开:读出的记录不变,序号接着往下排", (t) => {
ledger.append("config", { researchers: ["r1", "r2"] });
openStage(ledger, "s1", "proposal", 1);
ledger.append("submission", { stageId: "s1", author: "r1", target: null, item: "r1-1", version: 1, kind: "proposal", content: proposal(range(1, 2, 3)) });
ledger.append("note", { author: "r2", version: 1, body: "笔记" });
ledger.append("note", { author: "r2", body: "笔记" });
closeStage(ledger, "s1");
planRun(ledger, "run1", "forward", 1);
ledger.append("callStarted", callStart("k1"));