Files
iomgaa 493ea6d89d docs: 写明"边界"指什么;唯一约束改为有编号、只发生一次的八种记录
用户 2026-10-02 同意:CLAUDE.md"罕见情况怎么办"第 2 条写明边界是模型的提交、用户的配置、外部服务的返回,模块之间的调用不算边界;哪些记录加唯一约束属于账本设计,写在结构.md 第四部分(阶段开关、运行登记、确认启动、运行结果、模型调用开始结束、消融结算),前后顺序由 flow 的同一入口保证。任务书 03 同步。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 06:25:16 -04:00

13 KiB
Raw Permalink Blame History

任务书 03:ledger(账本)

写 ledger 模块:研究的全部记录怎么存、怎么保证不能改、写入时拒收什么、花钱的动作怎么先登记。

本任务书已由用户和监督者在 2026-10-02 一起确定,同一天改过一版(见"这一版改了什么")。下面说的"监督者",是在参考项目目录里和用户讨论设计的另一个会话。

按 CLAUDE.md,这一块是"账本"标准,写完要另派一个全新上下文的子 Agent 审阅。

这一版改了什么

第一版要求 ledger 做三件事:判断"谁在什么时候能看到什么"(快照加截止序号),给每条记录配唯一键,同样内容重写时当作已经写过。用户指出这三件都是过度设计:

  • "这个任务能看到哪些材料"由 materials 按任务定。第一条消息里直接给,还是等模型用查询工具去读,是同一份材料的两种给法。ledger 不判断谁能看什么,所以也没有截止序号。
  • 恢复时只核对、不重做写入(CLAUDE.md"中断后怎么恢复"),所以记录照写就是,不需要通用的去重。只给有自己编号、按定义只发生一次的记录加唯一约束。
  • Jev 是一个判别器,和程序检查地位相同,不单独成一种记录。

已经写好的代码里,存储、禁改触发器、提交内容检查、作者自评拆成封存,都还用得上。

先清理第一版留下的东西

第一版的设计已经作废,按它写出来的代码、测试、注释、文档都要清掉,不留"以后也许用得上"的部分。改完要读起来像一开始就按这一版写的(CLAUDE.md"怎么改一份文件"):

  • src/ledger/visibility.ts 和 tests/ledger-visibility.test.ts:整个删掉。
  • snapshot、Viewer、截止序号:从代码、导出和测试里删掉。
  • 通用唯一键(keyOf、joinKey 一类)、"同样内容重写当作已经写过"的处理和它的测试:删掉,只留第一部分列出的唯一约束。
  • jev 这种记录:删掉,改为写进"退回"和"决定"的内容里。
  • 第一版之后加出来、不在第三部分四种里的写入前提检查:删掉,测试一并删掉。
  • 注释里提到快照、截止序号、可见性、"同内容重写"的地方:按这一版改写或删掉。
  • docs/结构.md 第八部分你写的 ledger 那一段:按这一版重写。

清完以后,在 src/ledger/、tests/、docs/ 里搜 snapshot、cutoff、Viewer、visib、keyOf、"快照"、"截止"、"当作已经写过",除了这份任务书和结构.md 开头的来源说明,应该一处都搜不到。把搜索命令和结果写进报告。

开工前先读

  • CLAUDE.md:全部,尤其是"罕见情况怎么办"和"中断后怎么恢复"。
  • docs/结构.md:
    • 第二部分(ledger 和 materials 的边界);
    • 第四部分全文,这是本任务的主要依据(提交 0b0cbb0 改过);
    • 第六部分(只有 ledger 能用 SQLite);
    • 第八部分(vocab、rules 已导出的东西)。
  • docs/方法.md:
    • 第五部分开头的总原则(替换提交,实现和核验阶段不能替换);
    • 4.3 ⑭(解释冻结早于任何一格消融启动)、⑱(本轮全部结果在同一个事务里算完并公开);
    • 第七部分"什么时候扣次数";
    • 第九部分"中断与恢复时的记账含义"。
  • WORK_LOG.md、src/vocab/。
  • 参考项目(只读,不要改):/home/iomgaa/Projects/RivalBits/src/board/
    • schema.ts:触发器禁改禁删的写法;
    • ledger.ts:node:sqlite 的用法;
    • board.ts 里运行(runLedger 一带)和模型调用(openModelCalls 一带)分步登记的顺序。

一、怎么存

  • 用 Node 自带的 node:sqlite。一份研究一个数据库文件。

  • 只有一张表,每一行是:

    • 序号:自动递增,就是时间先后;
    • 种类;
    • 内容:JSON;
    • 内容的哈希:用 vocab 的 canonical 和 hash。
  • 整张表禁止修改、禁止删除,用两条触发器实现。

  • 唯一约束:有自己编号、按定义只发生一次的记录,按编号加数据库唯一约束,重复了就报错,不当作已经写过(结构.md 第四部分):阶段开、阶段关、运行登记、确认启动、运行结果、模型调用开始、模型调用结束、消融结算。

    其余记录照写,不设唯一键。前后顺序(启动前要先登记、结束前要先开始)由 flow 的同一个入口保证,ledger 不查。

  • 打开数据库时:

    • 新文件就建表;
    • 旧文件核对一个表结构版本号,对不上就报错。

二、记录的种类

按结构.md 第四部分的 11 种记录细分。下面是参考,具体名字由你来定。

结构.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 事件 事件 事件种类、原因、相关对象

说明:

  • 作者自评:写一版提案时,ledger 在同一次写入里把作者自评拆出来,存成一条封存,提案记录本身不带它。以后 materials 给别人看提案时,不用去掉什么。
  • 内容由还没写的模块产生的(配置、冻结协议、决定、结算),先存 Json 加最少的标识字段,不替那些模块猜字段。
  • 运行重试:确认没启动、退还名额以后要重试,由 flow 起一个新运行编号,ledger 不需要知道这是重试。

三、写入时拒绝的情况

只有下面四种,在写入函数里、同一个事务中查:

  1. 阶段已经关了,还要往这个阶段写提交。
  2. 替换提交时,要替换的那一份不是本人在这个阶段、对同一个对象的最新一版。实现、修复、核验、消融实现、消融核验这几种提交不能替换(方法.md 第五部分)。
  3. 运行结果写在"确认启动"之前。"确认没启动"这种结果例外,它本来就不需要启动记录。
  4. 某一轮消融已经有格确认启动了,才来关这一轮的解释阶段(方法.md 4.3 ⑭)。

不在这四种里的检查不写。比如:阶段有没有开过、提交种类和阶段对不对得上、解封列出的封存存不存在、用量是不是在调用开始之后。这些都是 flow 自己写错才会发生的。按 CLAUDE.md"罕见情况怎么办",不写的代价是出错时报错停下,或者马上就能看出来。

四、不归 ledger 的

  • 谁能看到什么:归 materials。ledger 只把记录交给程序,不提供按查看者过滤的快照,也没有截止序号。
  • 当前进行到哪一步、余额、信念本:从记录现算,分别归 flow、rules、materials。
  • 文件锁:归 plugin。
  • 代码文件、运行产物:分别归 workspace、runner 和 tasks,账本只存版本号、路径和指纹。

五、对外提供什么

经 src/ledger/index.ts 导出:

  1. 打开一份研究的账本:新建或续用;表结构版本不对就报错。

  2. 追加一条记录:按种类检查内容(TypeBox),做第三部分的拒收检查。

  3. 事务:几次追加要么全部写进去,要么一条都不写(方法.md 4.3 ⑱)。

  4. 读取:

    • 全部记录,按序号排,读出时按种类检查内容;
    • 第三部分拒收检查本来就要用的几个查询,比如某个阶段关了没有、某份提交的最新一版。

    不预先写一大套查询,以后哪个模块需要,再由那个模块的任务书加。

  5. 未完成的动作:登记了但没有结果的运行(带着有没有确认启动),开始了但没有结束的模型调用。恢复时 flow 用它逐个核对,怎么核对不归 ledger。

六、参考项目的保留与删除

处理 内容
借鉴 触发器禁改禁删的写法;运行和模型调用分步登记的顺序
删除 34 张表,改为一张表
删除 审计表 audit、请求表 requests(重复写入去重)、公开表 publications、通用事实表 facts
删除 visibility.ts、stage-view.ts:可见性归 materials,而且按任务定材料,不按记录逐条判断
删除 租约列和"研究是否在进行中"的判断(同一时刻一个进程靠文件锁,归 plugin)
删除 本轮重来、模板替换、撤回提交、命令审查、违规暂停、收尾补测、最后一轮预留、消融份额和照测欠测
删除 blindProtocol、run_kills
删除 打开时逐字核对全部建表语句
挪走 code-store.ts 归 workspace;artifacts.ts 归 runner 和 tasks;阶段视图里顺带算的剩余预算归 rules

七、测试

用真实的数据库文件,放在临时目录里,每个测试自己建、自己删。重点:

  • 用 SQL 直接执行 UPDATE、DELETE,被触发器拒绝。
  • 八处唯一约束各有一条测试:同一个编号写第二次就报错。
  • 第三部分的四种拒收各有一条测试;同时测对应的正常情况能写进去。
  • 写提案时作者自评拆成封存,提案记录里不带它。
  • 事务中途出错,一条都没写进去。
  • 关掉再打开,读出的记录不变;表结构版本不对时打开报错。
  • 未完成的动作:有登记没结果的运行、有开始没结束的调用能被找出来,写了结果之后就不再出现。

预期值就是"应该写进去什么、应该被拒绝什么",照方法.md 和结构.md 的文字写,不从实现里推。

八、独立审阅

写完、测试通过后,另派一个全新上下文的子 Agent 审阅。它只拿到结构.md 第四部分、方法.md 第七、九部分和 src/ledger/、相关测试,不给它看你的思路。

  • 让它找:会让记录被改掉、被重复扣次或重复结算、恢复后对不上的具体场景。
  • 它提出的每条问题,先问"不写会怎样":
    • 答案是"会报错停下"或"出错后马上就能看出来",就不加代码,在报告里列出来;
    • 会悄悄写错账的,才修,修了附测试。
  • 它不写 md 文件,结论直接回复给你。

九、规模与文档

  • 预期 500–900 行(不含测试)。超过 900 行就停下来检查:是需求本身复杂,还是专门处理罕见情况的分支在变多。
  • 完成后更新:
    • docs/结构.md 第八部分:补 ledger 对外的函数和类型,每项一句话。第一版写的那一段按这一版重写;
    • WORK_LOG.md。
  • 不新建其他 md 文件。

十、验收与报告

做完后本地提交,然后在回复里交一份报告,监督者会对着实际文件逐项核对:

  1. 提交号,新增文件列表,每个文件的行数,ledger 的总行数。
  2. npm run check 和 npm test 的完整输出。
  3. 最终的记录种类表:每种存什么,谁会写它。
  4. 按这一版删掉了第一版里的哪些东西,以及"先清理第一版留下的东西"那一节最后的搜索命令和结果。
  5. 独立审阅提出的问题,以及每条怎么处理的(修了的附测试名,没修的写"不写会怎样"的答案)。
  6. 你和这份任务书、结构.md、方法.md 不一致的地方,以及为什么。
  7. 规则不清楚的地方:读 CLAUDE.md、结构.md、方法.md 时,哪里让你犹豫、哪里两份文件说法不一致、哪里你是靠猜的。没有就写"没有"。