feat: 任务书 08,jev 模块与账本记 Jev 判断

src/jev 3 个文件 515 行:判重、查冻结条款、查代码改动三个函数,九问问法和阈值照参考项目,只改说法和字段名,阈值注明未在新项目材料上校准,等真实数据。
新增 npm run test:jev(真实 Jev 三处各问一次)。vocab 加 JevJudgment。
ledger 表结构版本 3:callEnded 四种结局对上 agents,墙钟时间用 Measured(恢复时补写出错、原因"进程中断"、测不到);退回、提交记录都带 jev。
config 加必填的 models.jev。

用户原话:"这次就给提交记录加一个和退回记录同样的 Jev 判断字段";"墙钟时间改用 Measured,恢复时写出错加测不到(原因'进程中断')"。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 02:10:55 -04:00
parent 68e49fd01a
commit 0ac1e01613
18 changed files with 1228 additions and 57 deletions
+184 -17
View File
@@ -3,7 +3,7 @@ import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { test } from "node:test";
import { Ledger, type LedgerRecord } from "../src/ledger/index.ts";
import { hash } from "../src/vocab/index.ts";
import { hash, type JevJudgment } from "../src/vocab/index.ts";
import {
callStart,
closeStage,
@@ -111,8 +111,11 @@ test("唯一约束:同一次模型调用开始两次报错", (t) => {
test("唯一约束:同一次模型调用结束两次报错", (t) => {
const { ledger, path } = freshLedger(t);
ledger.append("callStarted", callStart("k1"));
ledger.append("callEnded", { call: "k1", ending: "completed" });
assert.throws(() => ledger.append("callEnded", { call: "k1", ending: "failed", reason: "又报了一次" }), /call_ended_once/);
ledger.append("callEnded", { call: "k1", wallSeconds: { status: "measured", value: 12.5 }, status: "submitted" });
assert.throws(
() => ledger.append("callEnded", { call: "k1", wallSeconds: { status: "measured", value: 3 }, status: "failed", reason: "又报了一次" }),
/call_ended_once/,
);
assert.equal(countRows(path), 2);
});
@@ -162,19 +165,19 @@ test("内容不符合定义时不写入:多出的字段、提交内容不合
openStage(ledger, "s1", "wrapUp", 1);
assert.throws(() => ledger.append("runStarted", { run: "r1", extra: 1 } as never), /runStarted/);
assert.throws(
() => ledger.append("submission", { stageId: "s1", author: "r1", target: null, item: null, version: 1, kind: "wrapUp", content: { body: "" } }),
() => ledger.append("submission", { stageId: "s1", author: "r1", target: null, item: null, version: 1, jev: null, kind: "wrapUp", content: { body: "" } }),
/wrapUp/,
);
// 提案、挑刺一人能交好几份,必须带编号;实现一人对一个候选只交一份,编号必须为空,
// 否则另起一个编号就能绕开"实现不能替换"。
openStage(ledger, "prop", "proposal", 1);
assert.throws(
() => ledger.append("submission", { stageId: "prop", author: "r1", target: null, item: null as never, version: 1, kind: "proposal", content: proposal(range(1, 2, 3)) }),
() => ledger.append("submission", { stageId: "prop", author: "r1", target: null, item: null as never, version: 1, jev: null, kind: "proposal", content: proposal(range(1, 2, 3)) }),
/proposal/,
);
openStage(ledger, "impl", "implementation", 1, ["e1"]);
assert.throws(
() => ledger.append("submission", { stageId: "impl", author: "e1", target: "c1", item: "x" as never, version: 1, kind: "implementation", content: implementation() }),
() => ledger.append("submission", { stageId: "impl", author: "e1", target: "c1", item: "x" as never, version: 1, jev: null, kind: "implementation", content: implementation() }),
/implementation/,
);
assert.equal(countRows(path), 3); // 三条开阶段
@@ -184,7 +187,7 @@ test("写提案:作者自评拆成一条封存,提案记录里不带它", (t
const { ledger } = freshLedger(t);
openStage(ledger, "s1", "proposal", 1);
const record = ledger.append("submission", {
stageId: "s1", author: "r1", target: null, item: "r1-1", version: 1, kind: "proposal", content: proposal(range(1, 2, 3)),
stageId: "s1", author: "r1", target: null, item: "r1-1", version: 1, kind: "proposal", content: proposal(range(1, 2, 3)), jev: null,
});
const all = ledger.records();
const stored = all.find((r) => r.kind === "submission")!;
@@ -197,17 +200,174 @@ test("写提案:作者自评拆成一条封存,提案记录里不带它", (t
assert.equal(seal.seq, record.seq + 1);
});
test("退回:被退回的内容原样存,带退回理由;来自 Jev 的带原始概率和结论", (t) => {
const { ledger } = freshLedger(t);
// Jev 判断的样子照 src/vocab/jev.ts 的 JevJudgment 手写:问题名、问题全文哈希(64 位十六进制)、阈值、原始概率、结论、请求编号。
const judgment = (question: string, probability: number, threshold: number, requestId: string): JevJudgment => ({
question,
questionHash: "ab".repeat(32),
threshold,
probability,
yes: probability > threshold,
requestId,
});
test("退回:被退回的内容原样存;程序检查退回的 jev 为 null,Jev 退回的原样存它用到的几条判断", (t) => {
const path = join(tempDir(t), "ledger.db");
const ledger = Ledger.open(path);
const malformed = { factors: "不是数组" };
// 冻结条款一次问三问,退回时三条判断都存,包括判"否"的两条。
const clauses = [
judgment("changes_measurement", 0.97, 0.5, "req-1"),
judgment("self_contradictory", 0.02, 0.6, "req-2"),
judgment("too_vague", 0.1, 0.7, "req-3"),
];
ledger.append("rejection", { stageId: "s1", author: "r1", kind: "proposal", submitted: malformed, reason: "因子清单格式不对", jev: null });
ledger.append("rejection", {
stageId: "s6", author: "r2", kind: "response", submitted: { body: "改了评测" }, reason: "冻结条款改动了评测",
jev: { probabilities: { yes: 0.97, no: 0.03 }, conclusion: "changesEvaluation" },
stageId: "s6", author: "r2", kind: "response", submitted: { body: "改了评测" }, reason: "冻结条款改动了评测", jev: clauses,
});
const rejections = ledger.records().filter((r) => r.kind === "rejection");
ledger.close();
const reopened = Ledger.open(path);
t.after(() => reopened.close());
const rejections = reopened.records().filter((r) => r.kind === "rejection");
assert.deepEqual(rejections.map((r) => r.content.submitted), [malformed, { body: "改了评测" }]);
assert.deepEqual(rejections[1]!.content.jev, { probabilities: { yes: 0.97, no: 0.03 }, conclusion: "changesEvaluation" });
assert.equal(rejections[0]!.content.jev, null);
assert.deepEqual(rejections[1]!.content.jev, [
{ question: "changes_measurement", questionHash: "ab".repeat(32), threshold: 0.5, probability: 0.97, yes: true, requestId: "req-1" },
{ question: "self_contradictory", questionHash: "ab".repeat(32), threshold: 0.6, probability: 0.02, yes: false, requestId: "req-2" },
{ question: "too_vague", questionHash: "ab".repeat(32), threshold: 0.7, probability: 0.1, yes: false, requestId: "req-3" },
]);
});
test("退回:jev 不合 JevJudgment 的拒收,一条都不写", (t) => {
const { ledger, path } = freshLedger(t);
const good = judgment("same_change", 0.9, 0.5, "req-1");
const { requestId: _, ...withoutRequest } = good;
const bad: unknown[] = [
[], // 空数组:Jev 退回至少有一条判断,程序退回写 null
{ probabilities: { yes: 0.97, no: 0.03 }, conclusion: "changesEvaluation" }, // 旧写法:不是数组
good, // 一条判断也要放进数组
[{ ...good, probability: 1.2 }], // 概率超出 0 到 1
[{ ...good, threshold: -0.1 }], // 阈值超出 0 到 1
[withoutRequest], // 缺请求编号
[{ ...good, requestId: "" }], // 请求编号为空
[{ ...good, questionHash: "abc" }], // 哈希不是 64 位十六进制
[{ ...good, question: "Same Change" }], // 问题名格式不对
[{ ...good, yes: "true" }], // 结论不是布尔值
[{ ...good, note: "多写一项" }], // 不认识的字段
];
for (const jev of bad) {
assert.throws(
() => ledger.append("rejection", { stageId: "s1", author: "r1", kind: "proposal", submitted: {}, reason: "判为同一个改法", jev } as never),
/账本记录"rejection"的内容不符合定义/,
JSON.stringify(jev),
);
}
assert.equal(countRows(path), 0);
});
test("模型调用结束:四种结局各存一条,墙钟时间(秒)测到带值、测不到带原因,关掉再打开读出不变", (t) => {
const path = join(tempDir(t), "ledger.db");
const ledger = Ledger.open(path);
const endings = [
{ call: "k1", wallSeconds: { status: "measured", value: 812.25 }, status: "submitted" },
{ call: "k2", wallSeconds: { status: "measured", value: 95 }, status: "notSubmitted", reason: "模型自己停下了,一份都没交" },
{ call: "k3", wallSeconds: { status: "unmeasured", reason: "进程中断" }, status: "failed", reason: "进程中断,恢复时补写" },
{ call: "k4", wallSeconds: { status: "measured", value: 0 }, status: "stopped" },
] as const;
for (const e of endings) {
ledger.append("callStarted", callStart(e.call));
ledger.append("callEnded", e);
}
ledger.close();
const reopened = Ledger.open(path);
t.after(() => reopened.close());
const ended = reopened.records().filter((r) => r.kind === "callEnded").map((r) => r.content);
assert.deepEqual(ended, [
{ call: "k1", wallSeconds: { status: "measured", value: 812.25 }, status: "submitted" },
{ call: "k2", wallSeconds: { status: "measured", value: 95 }, status: "notSubmitted", reason: "模型自己停下了,一份都没交" },
{ call: "k3", wallSeconds: { status: "unmeasured", reason: "进程中断" }, status: "failed", reason: "进程中断,恢复时补写" },
{ call: "k4", wallSeconds: { status: "measured", value: 0 }, status: "stopped" },
]);
assert.deepEqual(reopened.pending().calls, []);
});
test("模型调用结束:结局和原因对不上、墙钟时间缺失或不合 Measured、测到的值为负,都拒收", (t) => {
const { ledger, path } = freshLedger(t);
const s3 = { status: "measured", value: 3 };
const bad: unknown[] = [
{ call: "k1", wallSeconds: s3, status: "notSubmitted" }, // 没交要带原因
{ call: "k1", wallSeconds: s3, status: "failed" }, // 出错要带原因
{ call: "k1", wallSeconds: s3, status: "failed", reason: "" }, // 原因不能为空
{ call: "k1", wallSeconds: s3, status: "submitted", reason: "交了" }, // 交了不带原因
{ call: "k1", wallSeconds: s3, status: "stopped", reason: "用户停下" }, // 被停下不带原因
{ call: "k1", wallSeconds: s3, status: "completed" }, // 旧写法
{ call: "k1", status: "submitted" }, // 缺墙钟时间
{ call: "k1", wallSeconds: 3, status: "submitted" }, // 裸数字,不是 Measured
{ call: "k1", wallSeconds: null, status: "submitted" }, // 测不到不能写 null
{ call: "k1", wallSeconds: { status: "measured", value: "3" }, status: "submitted" }, // 值不是数
{ call: "k1", wallSeconds: { status: "measured" }, status: "submitted" }, // 测到却没带值
{ call: "k1", wallSeconds: { status: "unmeasured" }, status: "failed", reason: "进程中断" }, // 测不到却没带原因
{ call: "k1", wallSeconds: { status: "unmeasured", reason: "" }, status: "failed", reason: "进程中断" }, // 原因为空
{ call: "k1", wallSeconds: { status: "unmeasured", reason: "进程中断", value: 0 }, status: "failed", reason: "进程中断" }, // 测不到又带值
];
for (const content of bad) {
assert.throws(() => ledger.append("callEnded", content as never), /账本记录"callEnded"的内容不符合定义/, JSON.stringify(content));
}
assert.equal(countRows(path), 0);
});
test("提交:jev 在外层与 content 并列,null 或 JevJudgment 数组都能存能读;提案拆封存不受影响", (t) => {
const path = join(tempDir(t), "ledger.db");
const ledger = Ledger.open(path);
openStage(ledger, "s1", "proposal", 1);
openStage(ledger, "s9", "implementation", 1);
// 代码改动五问都判"否"才收下,五条判断连同原始概率都存。
const checks = [
judgment("unseeded_randomness", 0.01, 0.5, "req-1"),
judgment("reads_clock", 0.02, 0.5, "req-2"),
judgment("touches_measurement", 0.2, 0.6, "req-3"),
judgment("early_exit", 0.05, 0.5, "req-4"),
judgment("zero_for_unmeasured", 0.3, 0.7, "req-5"),
];
const p = ledger.append("submission", { stageId: "s1", author: "r1", target: null, item: "r1-1", version: 1, kind: "proposal", content: proposal(range(1, 2, 3)), jev: null });
ledger.append("submission", { stageId: "s9", author: "e1", target: "c1", item: null, version: 1, kind: "implementation", content: implementation(), jev: checks });
ledger.close();
const reopened = Ledger.open(path);
t.after(() => reopened.close());
const all = reopened.records();
const subs = all.filter((r) => r.kind === "submission");
assert.deepEqual(subs.map((r) => r.content.jev), [
null,
[
{ question: "unseeded_randomness", questionHash: "ab".repeat(32), threshold: 0.5, probability: 0.01, yes: false, requestId: "req-1" },
{ question: "reads_clock", questionHash: "ab".repeat(32), threshold: 0.5, probability: 0.02, yes: false, requestId: "req-2" },
{ question: "touches_measurement", questionHash: "ab".repeat(32), threshold: 0.6, probability: 0.2, yes: false, requestId: "req-3" },
{ question: "early_exit", questionHash: "ab".repeat(32), threshold: 0.5, probability: 0.05, yes: false, requestId: "req-4" },
{ question: "zero_for_unmeasured", questionHash: "ab".repeat(32), threshold: 0.7, probability: 0.3, yes: false, requestId: "req-5" },
],
]);
assert.equal("jev" in (subs[1]!.content.content as object), false);
const seal = all.find((r) => r.kind === "seal")!;
assert.deepEqual(seal.content, { proposal: p.seq, range: range(1, 2, 3) });
assert.equal(seal.seq, p.seq + 1);
});
test("提交:缺 jev、jev 不合 JevJudgment 数组、jev 写进 content 里,都拒收", (t) => {
const { ledger, path } = freshLedger(t);
openStage(ledger, "s1", "wrapUp", 1);
const good = judgment("same_change", 0.1, 0.5, "req-1");
const base = { stageId: "s1", author: "r1", target: null, item: null, version: 1, kind: "wrapUp", content: { body: "收尾" } };
const bad: unknown[] = [
base, // 缺 jev:没用到 Jev 要写 null,不靠省略
{ ...base, jev: [] }, // 空数组
{ ...base, jev: good }, // 一条也要放进数组
{ ...base, jev: [{ ...good, probability: -0.1 }] }, // 概率超出 0 到 1
{ ...base, jev: [{ ...good, extra: 1 }] }, // 不认识的字段
{ ...base, jev: null, content: { body: "收尾", jev: null } }, // jev 不进 content
];
for (const content of bad) {
assert.throws(() => ledger.append("submission", content as never), /账本记录"submission(提交种类 wrapUp)"的内容不符合定义/, JSON.stringify(content));
}
assert.equal(countRows(path), 1);
});
test("事务:中途出错,一条都没写进去;中途被拒收,前面写的也撤销;正常结束则全部写进去", (t) => {
@@ -253,7 +413,7 @@ test("关掉再打开:读出的记录不变,序号接着往下排", (t) => {
const ledger = Ledger.open(path);
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("submission", { stageId: "s1", author: "r1", target: null, item: "r1-1", version: 1, jev: null, kind: "proposal", content: proposal(range(1, 2, 3)) });
ledger.append("note", { author: "r2", body: "笔记" });
closeStage(ledger, "s1");
planRun(ledger, "run1", "forward", 1);
@@ -276,6 +436,13 @@ test("打开:表结构版本不对报错;不是空文件也不是账本的
db.close();
assert.throws(() => Ledger.open(wrongVersion), /版本是 99/);
// 版本 2 是"模型调用结束"还没有墙钟时间、退回还没有 Jev 判断数组时的账本,不迁移,打开报错。
const previous = join(dir, "v2.db");
const v2 = new DatabaseSync(previous);
v2.exec("PRAGMA user_version = 2");
v2.close();
assert.throws(() => Ledger.open(previous), /版本是 2.*只认版本 3/s);
const foreign = join(dir, "foreign.db");
const other = new DatabaseSync(foreign);
other.exec("CREATE TABLE t (x INTEGER)");
@@ -311,9 +478,9 @@ test("查询:阶段关了没有、某份提交的最新一版(同一人对
assert.equal(ledger.stageClosed("s1"), false);
const slot = { stageId: "s1", author: "r1", kind: "critique" as const, target: "c1", item: "k1" };
assert.equal(ledger.latestSubmission(slot), undefined);
ledger.append("submission", { ...slot, version: 1, content: { target: "whole", body: "第一版" } });
const v2 = ledger.append("submission", { ...slot, version: 2, content: { target: "whole", body: "第二版" } });
ledger.append("submission", { ...slot, item: "k2", version: 1, content: { target: "p1", body: "另一份" } });
ledger.append("submission", { ...slot, version: 1, jev: null, content: { target: "whole", body: "第一版" } });
const v2 = ledger.append("submission", { ...slot, version: 2, jev: null, content: { target: "whole", body: "第二版" } });
ledger.append("submission", { ...slot, item: "k2", version: 1, jev: null, content: { target: "p1", body: "另一份" } });
assert.deepEqual(ledger.latestSubmission(slot), v2);
closeStage(ledger, "s1");
assert.equal(ledger.stageClosed("s1"), true);