Files
InfoRSI/任务书/03-ledger.md
T
2026-10-02 02:36:47 -04:00

186 lines
13 KiB
Markdown
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.
# 任务书 03:ledger(账本)
写 ledger 模块:研究的全部记录怎么存、怎么保证不能改、写入时拒收什么、谁在什么时候能看到什么、花钱的动作怎么先登记。
本任务书的内容已由用户和监督者在 2026-10-02 一起确定。下面说的"监督者",是在参考项目目录里和用户讨论设计的另一个会话。
按 CLAUDE.md,这一块是"账本"标准,写完要另派一个全新上下文的子 Agent 审阅。
## 开工前先读
- `CLAUDE.md`:全部。2026-10-02 改过两处:
- "罕见情况怎么办"一节;
- 账本全部只追加。
- `docs/结构.md`:
- 第二部分(ledger 的边界,"几条边界"里的前两条);
- **第四部分全文,这是本任务的主要依据**;
- 第六部分(只有 ledger 能用 SQLite);
- 第八部分(vocab、rules 已导出的东西)。
- `docs/方法.md`:
- 第三部分"谁在什么时候能看到什么";
- 第五部分开头的总原则(替换提交、实现和核验阶段不能替换);
- 4.3 ⑭(解释冻结早于任何一格消融启动)、⑱(本轮全部结果在同一个事务里算完并公开);
- 第七部分"什么时候扣次数";
- 第九部分"中断与恢复时的记账含义"。
- `WORK_LOG.md`、`src/vocab/`。
- 参考项目(只读,不要改):`/home/iomgaa/Projects/RivalBits/src/board/`
- `schema.ts`:触发器禁改禁删的写法(59–70 行一带);
- `ledger.ts`:`node:sqlite` 的用法;
- `board.ts` 里运行(`runLedger` 一带)和模型调用(`openModelCalls` 一带)分步登记的顺序;
- `visibility.ts`:旧的可见性判断,注意它有绕弯的地方,见下面"删除"。
## 一、怎么存(结构.md 第四部分)
- 用 Node 自带的 `node:sqlite`,不另装依赖。一份研究一个数据库文件。
- **只有一张表**,每一行是:
- 序号:自动递增,就是时间先后;
- 种类;
- 唯一键;
- 内容:JSON;
- 内容的哈希:用 vocab 的 `canonical` 和 `hash`。
- **整张表禁止修改、禁止删除**,用两条触发器实现。
- **"种类 + 唯一键"不能重复**,用数据库的唯一约束实现。
- **重复写入的规则**:
- 同一个种类和键,内容哈希相同,当作已经写过,返回原来那条,不报错;
- 内容哈希不同,报错,写清种类、键和两份哈希。
- **唯一键怎么拼,只在 ledger 里定义**,调用方不自己拼键。键拼错会让去重悄悄失效,所以每种记录的键由一个函数从内容里取,别处不另写一份。
- 打开数据库时:
- 新文件就建表;
- 旧文件核对一个表结构版本号,对不上就报错。
不逐字核对建表语句。
## 二、记录的种类
结构.md 第四部分列了 12 种记录。ledger 按需要把它们细分成具体种类,每种有自己的内容定义(TypeBox)和唯一键。下面是监督者整理的参考,具体名字和细分由你来定;和结构.md、方法.md 对不上时以文档为准,并在报告里写出来。
| 结构.md 的记录 | 细分种类 | 唯一键 | 内容 |
|---|---|---|---|
| 1 研究配置 | 配置 | 固定一个 | 先存 Json;config 模块写好后再收紧 |
| 2 阶段 | 开、关 | 阶段编号 | 开:阶段名(vocab `STAGES`)、轮次、这个阶段的应交名单;关:无 |
| 3 提交 | 提交的每一版 | 阶段编号 + 作者 + 对象 + 版本 | 提交种类(vocab `SubmissionKind`)、作者、针对谁(候选、格,没有就空)、版本号、要替换的上一版、内容(用 vocab `SUBMISSION_SCHEMAS` 检查) |
| 3 提交 | 私人笔记的每一版 | 作者 + 版本 | 正文 |
| 4 冻结协议 | 冻结协议 | 候选编号 | 先存 Json 加必要的标识 |
| 5 作者自评 | 封存、解封 | 封存:提案的那一版;解封:候选编号 | 封存:三个数;解封:放出哪几份封存(列明) |
| 6 冻结代码 | 冻结代码 | 候选或消融格编号 | 版本号、因子位置表 |
| 7 运行 | 登记要跑、确认启动、结果 | 都是运行编号 | 登记:用途(基线 / 正向 / 消融)、轮次、对象、运行定义(vocab `RunDefinition`);结果:vocab `ProcessResult`,以及有的话 `JudgedResult` |
| 8 决定 | 决定 | 决定种类 + 轮次 + 对象 | 规则名、规则版本、方法参数、种子、输入、输出(输入输出存 Json,ledger 不认识 rules) |
| 9 结算 | 结算 | 消融编号 + 格 | 存 Json,同上 |
| 10 Jev 判别 | Jev 判别 | 自动 | 问了什么、原始概率、结论 |
| 11 用量 | 模型调用开始、每次回复的用量、结束 | 调用编号(用量另加回复序号) | 用量用 vocab `ModelUsage` |
| 12 事件 | 事件 | 自动 | 事件种类、原因、相关对象 |
几点说明:
- **作者自评怎么封存。** 提案的内容里有作者自评(`authorEstimate`)。写一版提案时,ledger 在同一次写入里把作者自评拆出来存成一条"封存"记录,提案记录本身不带它。这样可见性只需要判断"这条封存有没有被解封",不用在给别人看之前改写提案内容。解封记录列明它放出了哪几条封存。合并而成的候选有几位作者,就有几条封存。
- **能直接用现有定义的就用现有的**:提交内容、运行定义、运行结果、模型用量,vocab 里都有。内容由还没写的模块(config、rules 的决定和结算)产生的,先存 Json 加最少的标识字段,不要替那些模块猜字段。
- 没有"动手杀之前"那条记录:被取消也是一种结果。
## 三、写入时拒绝的情况
只有下面这些,写在 ledger 的写入函数里,在同一个事务中查:
1. 阶段已经关了,还要往这个阶段写提交。
2. 替换提交时,要替换的那一份不是本人在这个阶段、对同一个对象的最新一版。实现、修复、核验、消融实现、消融核验这几种提交不能替换(方法.md 第五部分)。
3. 运行结果写在"确认启动"之前。例外是"没启动"这种结果,它本来就不需要启动记录(方法.md 第七部分:确认没启动的不扣次数)。
4. 某一轮消融已经有格确认启动了,才来关这一轮的解释阶段(方法.md 4.3 ⑭:冻结必须早于任何一格的启动)。
别的不在 ledger 里查。比如提交内容的字段对不对、三个数是否严格递增,归 submissions;该不该开下一个阶段,归 flow。
## 四、谁能看到什么
只用**一个函数**回答"某位查看者在某个序号时,能不能看到这条记录"。查看者有两种:
- **程序**:看全部。
- **某位研究者**:按下表。
| 记录 | 本人 | 其他研究者 |
|---|---|---|
| 研究者阶段的提交 | 交上就能看 | 这个阶段关闭后能看 |
| 作者自评的封存 | 能看 | 有列明它的解封记录后能看 |
| 第一次预测 | 能看 | 永远不能看(只进导出) |
| 私人笔记 | 能看 | 永远不能看 |
| 冻结协议、冻结代码、运行、决定、结算、阶段 | 写入后都能看 | 写入后都能看 |
| Jev 判别、用量、事件 | 不能看 | 不能看 |
后三种不给研究者看,原因有两个:研究者用不到;Jev 判别可能在阶段还开着时就问到了某份提交的内容,给了别人就是提前泄露。
另外两条:
- **截止序号**:研究者在一个阶段里看到的东西,截止到这个阶段开始的那一刻(方法.md 第三部分、结构.md 第二部分)。快照函数接收一个截止序号,序号之后的记录一律不给。
- **实验员和核验员**:不单独设查看者。给他们的材料由以后的 materials 从程序的全集里挑。
只用"给不给"这一种手段,不在给出之前改写记录的内容。
## 五、对外提供什么
经 `src/ledger/index.ts` 导出:
1. **打开**一份研究的账本:新建或续用;表结构版本不对就报错。
2. **追加**一条记录:检查内容(TypeBox),取唯一键,做第三部分的拒收检查,处理重复写入。
3. **事务**:几次追加要么全部写进去,要么一条都不写(方法.md 4.3 ⑱)。
4. **读取**:
- 快照:给定查看者和截止序号,返回能看到的记录。读出时按种类用 TypeBox 校验;
- 程序自己要的几个常用查询,比如某个阶段开着没有、某份提交的最新一版。只写现在能说出谁会用的,不预先写一大套查询。
5. **未完成的动作**:登记了但没有结果的运行,开始了但没有结束的模型调用。恢复时 flow 用它逐个核对,怎么核对不归 ledger。
## 六、参考项目的保留与删除
| 处理 | 内容 |
|---|---|
| 借鉴 | 触发器禁改禁删的写法;运行和模型调用分步登记的顺序;"同样内容重写当作已写过、不同内容报错";写入先后的检查 |
| 删除 | 34 张表,改为一张表 |
| 删除 | 审计表 `audit`、请求表 `requests`、公开表 `publications`、通用事实表 `facts`:它们的作用都变成了一张表本身的性质(序号、唯一键、阶段关闭记录) |
| 删除 | `studies` 表里的租约列(owner、epoch、lease_until),以及每个写入函数里"研究是否在进行中"的判断。同一时刻只有一个进程,靠的是文件锁,文件锁归 plugin |
| 删除 | 本轮重来、模板替换、撤回提交(`round-restart`、`template-replacement`、`withdrawn-submission`)以及可见性里排除它们的三处判断 |
| 删除 | 命令审查 `shell-review`、违规暂停 `integrity-incident`/`integrity-disposition` |
| 删除 | 收尾补测 `fillin_plans`、`ablation_final_settlements`,最后一轮预留 `reserves`,消融的份额、照测、欠测 |
| 删除 | `blindProtocol`(改写冻结协议后再给第一次预测看):第一次预测在挑刺之前、看的是原始提案,不需要它 |
| 删除 | `run_kills`(动手杀之前那条) |
| 删除 | 打开时逐字核对全部建表语句 |
| 挪走 | `code-store.ts` 归 workspace(账本只存版本号);`artifacts.ts` 归 runner 和 tasks(账本只存路径和指纹);阶段视图里顺带算的剩余预算归 rules(`runBalance`) |
| 不搬 | 私人笔记原来是账本外的文件,整份覆盖写;现在进账本,每次修改追加一版 |
## 七、测试
用真实的数据库文件,放在临时目录里,每个测试自己建、自己删。重点:
- 用 SQL 直接执行 UPDATE、DELETE,被触发器拒绝。
- 同种类、同键、同内容写两次只有一条;同键不同内容报错。
- 第三部分的四种拒收各有一条测试;同时测对应的正常情况能写进去。
- 第四部分可见性表里的每一格;截止序号之后的记录看不到;提案里的作者自评不出现在别人能看到的提案记录里。
- 事务中途出错,一条都没写进去。
- 关掉再打开,读出的记录和快照不变;表结构版本不对时打开报错。
- 未完成的动作:有登记没结果的运行、有开始没结束的调用能被找出来,写了结果之后就不再出现。
账本这部分没有公式,预期值就是"应该写进去什么、应该被拒绝什么",照方法.md 和结构.md 的文字写,不从实现里推。
## 八、独立审阅
写完、测试通过后,另派一个全新上下文的子 Agent 审阅。它只拿到结构.md 第四部分、方法.md 第三、七、九部分和 `src/ledger/`、相关测试,不给它看你的思路。
- 让它找:会让记录被改掉、被重复记账、被提前泄露、恢复后对不上的具体场景。
- 它提出的每条问题,先问"不写会怎样":
- 答案是"会报错停下"或"出错后马上就能看出来",就不加代码,在报告里列出来;
- 会悄悄写错账或泄露的,才修,修了附测试。
- 它不写 md 文件,结论直接回复给你。
## 九、规模与文档
- 预期 900–1400 行(不含测试)。超过 1400 行就停下来检查:是需求本身复杂,还是专门处理罕见情况的分支在变多。
- 完成后更新:
- `docs/结构.md` 第八部分:补 ledger 对外的函数和类型,每项一句话;
- `WORK_LOG.md`。
- 不新建其他 md 文件。
## 十、验收与报告
做完后本地提交,然后在回复里交一份报告,监督者会对着实际文件逐项核对:
1. 提交号,新增文件列表,每个文件的行数,ledger 的总行数。
2. `npm run check` 和 `npm test` 的完整输出。
3. 最终的记录种类表:每种的唯一键、内容定义、谁会写它。
4. 独立审阅提出的问题,以及每条怎么处理的(修了的附测试名,没修的写"不写会怎样"的答案)。
5. 你和这份任务书、结构.md、方法.md 不一致的地方,以及为什么。
6. **规则不清楚的地方**:读 CLAUDE.md、结构.md、方法.md 时,哪里让你犹豫、哪里两份文件说法不一致、哪里你是靠猜的。没有就写"没有"。