feat: 账本 ledger 模块(任务书 03)

一张只追加的表(序号、种类、内容、哈希),禁改禁删两条触发器。
有自己编号、只发生一次的 8 种记录加部分唯一索引,重复就报错:阶段开、阶段关、运行登记、
确认启动(确认没启动的结果也算这一条,防止同一次运行既扣次又退还)、运行结果、模型调用开始、结束、消融结算。
写入时只查四种拒收:只收开着的阶段的提交;替换必须接本人同一阶段、同一对象、同一种提交的最新一版,实现与核验类不能替换;
结果先于确认启动(没启动除外);本轮消融已有格启动后不能关解释阶段。
写提案时作者自评拆成封存。提供打开(核对表结构版本,非账本库不改动)、追加、同步事务、读取、未完成的动作。
第一版的可见性、截止序号、通用唯一键、jev 记录和多余的前提检查已按新版任务书清掉。
三轮全新上下文审阅;flow 传错参数一类的意见按 CLAUDE.md"罕见情况怎么办"第 2 条只记录不加代码。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 06:37:47 -04:00
parent 493ea6d89d
commit 716f4b88a5
11 changed files with 1236 additions and 1 deletions
+57
View File
@@ -0,0 +1,57 @@
import assert from "node:assert/strict";
import { join } from "node:path";
import { test } from "node:test";
import { Ledger } from "../src/ledger/index.ts";
import { callStart, exited, freshLedger, notStarted, planRun, tempDir, usage } from "./ledger-helpers.ts";
// 未完成的动作(方法.md 第九部分"中断与恢复时的记账含义":先登记、再执行、再登记结果):
// 登记了但没有结果的运行、开始了但没有结束的模型调用能被找出来;写了结果之后就不再出现。
test("运行:登记了没结果的被找出来,带着有没有确认启动;写了结果就不再出现", (t) => {
const { ledger } = freshLedger(t);
planRun(ledger, "base", "baseline", 0);
planRun(ledger, "run1", "forward", 1);
planRun(ledger, "run2", "forward", 1);
ledger.append("runStarted", { run: "base" });
ledger.append("runStarted", { run: "run1" });
ledger.append("runResult", { run: "base", process: exited, judged: null });
assert.deepEqual(
ledger.pending().runs.map((r) => [r.plan.content.run, r.started]),
[
["run1", true],
["run2", false],
],
);
ledger.append("runResult", { run: "run2", process: notStarted, judged: null });
assert.deepEqual(ledger.pending().runs.map((r) => r.plan.content.run), ["run1"]);
ledger.append("runResult", { run: "run1", process: exited, judged: null });
assert.deepEqual(ledger.pending().runs, []);
});
test("模型调用:开始了没结束的被找出来,带着这次任务的全部材料;有用量也还算没结束;结束后不再出现", (t) => {
const { ledger } = freshLedger(t);
ledger.append("callStarted", callStart("k1", "r1", "s1"));
ledger.append("callStarted", callStart("k2", "e1"));
ledger.append("callUsage", { call: "k1", reply: 1, usage });
ledger.append("callEnded", { call: "k2", ending: "failed", reason: "接口超时" });
const calls = ledger.pending().calls;
assert.deepEqual(calls.map((c) => c.content.call), ["k1"]);
assert.deepEqual(calls[0]!.content.materials, callStart("k1").materials);
ledger.append("callEnded", { call: "k1", ending: "completed" });
assert.deepEqual(ledger.pending(), { runs: [], calls: [] });
});
test("关掉再打开:未完成的动作照样找得出来", (t) => {
const path = join(tempDir(t), "ledger.db");
const ledger = Ledger.open(path);
planRun(ledger, "run1", "forward", 1);
ledger.append("runStarted", { run: "run1" });
ledger.append("callStarted", callStart("k1"));
const before = ledger.pending();
ledger.close();
const reopened = Ledger.open(path);
t.after(() => reopened.close());
assert.deepEqual(reopened.pending(), before);
assert.equal(before.runs.length, 1);
assert.equal(before.calls.length, 1);
});