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