feat: 诊断(任务书 18 第一块):诊断数学、四种提交、一次运行跑多个实验、账本与材料、flow 诊断各步

消融默认关,关掉时解释阶段一并跳过;选诊断实验用信息价值减成本。
eval/diagnosis/research.json 为第三块配置。vocab 字段说明的出口文字随提示词一起改,见下一次提交的 prompts/ 与同文件改动(本次含在内)。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 10:36:20 -04:00
parent f39f8410e7
commit 852600df02
69 changed files with 6607 additions and 214 deletions
+7 -5
View File
@@ -17,7 +17,7 @@ import {
type SubmissionKind,
} from "../vocab/index.ts";
// 账本记录的种类(docs/结构.md 第四部分的 11 种记录,细分成下面 19 种),每种一份内容定义(TypeBox)。
// 账本记录的种类(docs/结构.md 第四部分的 11 种记录,细分成下面 20 种),每种一份内容定义(TypeBox)。
// 下面写"只有一条""只一次"的,由 schema.ts 的唯一约束保证。
// 配置、冻结协议、决定、结算的内容由别的模块产生,这里只检查最少的标识字段,其余只检查是 Json。
@@ -131,7 +131,7 @@ export const KINDS = {
submission: Type.Unsafe<StoredSubmission>({}),
/**
* 3 退回:被退回的那份内容原样存(它可能正是因为不合格式才被退回),连同退回理由。
* target 是这次任务的对象,与提交的 target 同义(候选、原提案、消融格,没有对象的写 null):
* target 是这次任务的对象,与提交的 target 同义(候选、原提案、消融格、本轮诊断,没有对象的写 null):
* 同一人在同一阶段可能对几个对象各做一次同种任务,重新发起时按它找本人这个对象的上一次退回。
* jev 是这次退回用到的 Jev 判断(见 JevUsed),判"否"的几问也一起存;没用到 Jev 的写 null。
*/
@@ -161,11 +161,11 @@ export const KINDS = {
/** 6 冻结代码:候选或消融格的代码版本号(由 workspace 给出)和因子位置表。位置表先存 Json。 */
code: Type.Object({ object: Id, version: Text, locations: AnyJson }, CLOSED),
/** 7 运行·登记要跑(占名额):运行编号由 flow 起,一个编号只登记一次;用途、轮次、对象(候选或格;基线没有)、运行定义。 */
/** 7 运行·登记要跑(占名额):运行编号由 flow 起,一个编号只登记一次;用途、轮次、对象(候选、格或本轮诊断;基线没有)、运行定义。 */
runPlanned: Type.Object(
{
run: Id,
purpose: Type.Enum(["baseline", "forward", "ablation"], { type: "string" }),
purpose: Type.Enum(["baseline", "forward", "ablation", "diagnosis"], { type: "string" }),
round: Round,
object: OptionalId,
definition: RunDefinition,
@@ -178,7 +178,7 @@ export const KINDS = {
runResult: Type.Object({ run: Id, process: ProcessResult, judged: Type.Union([JudgedResult, Type.Null()]) }, CLOSED),
/**
* 8 决定:选批、采纳、选格、押的因子、入口分配、判重合并。decision 是决定种类。
* 8 决定:选诊断实验、选批、采纳、选格、押的因子、入口分配、判重合并(提案和诊断实验)。decision 是决定种类。
* 输入输出存 Json:ledger 不认识 rules。判重合并的输入里带 Jev 的原始概率。
*/
decision: Type.Object(
@@ -197,6 +197,8 @@ export const KINDS = {
/** 9 结算:每轮最多采纳一个改法,所以最多一次消融,结算按轮次一轮一条。各格结局、配合关系、解释的确认或否定等先存 Json。 */
settlement: Type.Object({ round: Round, result: AnyJson }, CLOSED),
/** 9 诊断结算:每轮最多一次诊断,结算也按轮次一轮一条(方法.md 6.12)。每个实验的结局与实测、解释的确认或否定等先存 Json。 */
diagnosisSettlement: Type.Object({ round: Round, result: AnyJson }, CLOSED),
/** 10 模型调用开始:先登记再调用(方法.md 第九部分)。附这次任务的全部材料,用来重现模型当时看到了什么。一次调用只有一条。 */
callStarted: Type.Object({ call: Id, actor: Id, stageId: OptionalId, model: Text, materials: AnyJson }, CLOSED),
+13 -5
View File
@@ -83,7 +83,7 @@ export class Ledger {
}
/**
* 追加一条记录,返回存下的那一条。先按种类检查内容,再做写入时的四种拒收检查(rejections.ts)。
* 追加一条记录,返回存下的那一条。先按种类检查内容,再做写入时的五种拒收检查(rejections.ts)。
* 写提案时,作者自评从提案里拆出来,在同一个事务里另存成一条封存,返回的是提案那一条。
*/
append<K extends AppendKind>(kind: K, content: Input<K>): LedgerRecord<K> {
@@ -117,16 +117,24 @@ export class Ledger {
this.#first("stageOpen", "json_extract(content, '$.stageId') = ?", stageId) as LedgerRecord<"stageOpen"> | undefined,
latest: (slot) => this.latestSubmission(slot),
runStarted: (run) => this.#first("runStarted", "json_extract(content, '$.run') = ?", run) !== undefined,
ablationStarted: (round) =>
ablationStarted: (round) => this.#purposeStarted("ablation", round),
diagnosisStarted: (round) => this.#purposeStarted("diagnosis", round),
};
/** 这一轮有没有某种用途的运行已经确认启动。 */
#purposeStarted(purpose: "ablation" | "diagnosis", round: number): boolean {
return (
this.#one(
`SELECT s.seq FROM records s JOIN records p
ON p.kind = 'runPlanned' AND json_extract(p.content, '$.run') = json_extract(s.content, '$.run')
WHERE s.kind = 'runStarted'
AND json_extract(p.content, '$.purpose') = 'ablation'
AND json_extract(p.content, '$.purpose') = ?
AND json_extract(p.content, '$.round') = ?`,
purpose,
round,
) !== undefined,
};
) !== undefined
);
}
// -------------------------------------------------------------------------
// 读取
+17 -5
View File
@@ -1,8 +1,8 @@
import { SUBMISSION_KINDS, type Stage } from "../vocab/index.ts";
import type { Content, Kind, LedgerRecord, SubmissionSlot } from "./kinds.ts";
// 写入时由 ledger 拒绝的四种情况(docs/结构.md 第四部分"写入时由 ledger 拒绝的几种情况"),在写入的同一个事务里查。
// 只有这四种。记录之间别的先后(例如运行结果前要先有登记)和提交种类与阶段对不对得上都不查:
// 写入时由 ledger 拒绝的五种情况(docs/结构.md 第四部分"写入时由 ledger 拒绝的几种情况"),在写入的同一个事务里查。
// 只有这五种。记录之间别的先后(例如运行结果前要先有登记)和提交种类与阶段对不对得上都不查:
// flow 调 ledger 是模块之间的调用,不是系统边界。"只发生一次"由 schema.ts 的唯一约束管。
// 提交内容的字段归 submissions,该不该开下一个阶段归 flow。
@@ -16,14 +16,18 @@ export interface WriteState {
runStarted(run: string): boolean;
/** 这一轮有没有消融格的运行已经确认启动。 */
ablationStarted(round: number): boolean;
/** 这一轮的诊断运行有没有已经确认启动。 */
diagnosisStarted(round: number): boolean;
}
/** 实现和核验阶段的提交不能替换(方法.md 第五部分总原则):实现、修复、核验、消融实现、消融核验。 */
/** 实现和核验阶段的提交不能替换(方法.md 第五部分总原则):实现、修复、核验、消融实现、消融核验、诊断实现(含修复)、诊断核验。 */
const NOT_REPLACEABLE: ReadonlySet<Stage> = new Set([
"implementation",
"verification",
"ablationImplementation",
"ablationVerification",
"diagnosisImplementation",
"diagnosisVerification",
]);
export function checkWrite(kind: Kind, content: unknown, state: WriteState): void {
@@ -35,7 +39,7 @@ export function checkWrite(kind: Kind, content: unknown, state: WriteState): voi
/**
* 第 1 条:只收开着的阶段的提交,即有开阶段记录、没有关阶段记录。
* 第 2 条:新的一版必须正好接在本人在这个阶段、对同一个对象的最新一版后面(第一版是 1);
* 实现、修复、核验、消融实现、消融核验只能交第一版。
* 实现、修复、核验、消融实现、消融核验、诊断实现、诊断修复、诊断核验只能交第一版。
*/
function checkSubmission(s: Content<"submission">, state: WriteState): void {
const where = `提交 ${s.kind}(阶段 ${s.stageId},作者 ${s.author},对象 ${s.target ?? "无"},编号 ${s.item ?? "无"},第 ${s.version} 版)`;
@@ -63,7 +67,10 @@ function checkRunResult(r: Content<"runResult">, state: WriteState): void {
}
}
/** 第 4 条:这一轮已经有消融格确认启动了,就不能再关解释阶段(方法.md 4.3 ⑭:解释冻结必须早于任何一格启动)。 */
/**
* 第 4 条:这一轮已经有消融格确认启动了,就不能再关解释阶段(方法.md 4.3 ⑭:解释冻结必须早于任何一格启动)。
* 第 5 条:这一轮的诊断运行已经确认启动了,就不能再关诊断预测阶段(方法.md 4.3 诊 2:冻结必须早于诊断运行启动,范围才算事前预测)。
*/
function checkStageClose(c: Content<"stageClose">, state: WriteState): void {
const open = state.stageOpen(c.stageId);
if (open?.content.stage === "explanation" && state.ablationStarted(open.content.round)) {
@@ -71,4 +78,9 @@ function checkStageClose(c: Content<"stageClose">, state: WriteState): void {
`不能关解释阶段 ${c.stageId}:第 ${open.content.round} 轮已经有消融格确认启动,解释冻结必须早于任何一格启动(方法.md 4.3 ⑭)`,
);
}
if (open?.content.stage === "diagnosisPrediction" && state.diagnosisStarted(open.content.round)) {
throw new Error(
`不能关诊断预测阶段 ${c.stageId}:第 ${open.content.round} 轮的诊断运行已经确认启动,诊断预测冻结必须早于诊断运行启动(方法.md 4.3 诊 2)`,
);
}
}
+3 -1
View File
@@ -3,7 +3,7 @@ import type { DatabaseSync } from "node:sqlite";
// 账本的表结构(docs/结构.md 第四部分"全部只追加"):只有一张表,每一行是序号、种类、内容、内容的哈希。
// 表结构一改就把版本号加 1;旧文件版本号对不上时打开报错,不迁移,也不逐字核对建表语句。
export const SCHEMA_VERSION = 4;
export const SCHEMA_VERSION = 5;
/** 新文件要执行的建表语句,由 Ledger 在一个事务里执行,最后写入版本号。 */
export const CREATE_SCHEMA = [
@@ -33,6 +33,8 @@ export const CREATE_SCHEMA = [
`CREATE UNIQUE INDEX call_ended_once ON records (json_extract(content, '$.call')) WHERE kind = 'callEnded'`,
// 结算以轮次为编号:每轮最多采纳一个改法,所以最多一次消融。
`CREATE UNIQUE INDEX settlement_once ON records (json_extract(content, '$.round')) WHERE kind = 'settlement'`,
// 诊断结算同样以轮次为编号:每轮最多一次诊断。
`CREATE UNIQUE INDEX diagnosis_settlement_once ON records (json_extract(content, '$.round')) WHERE kind = 'diagnosisSettlement'`,
`PRAGMA user_version=${SCHEMA_VERSION}`,
];