Files
InfoRSI/docs/结构.md
T
iomgaa a42349717c feat: 研究编号只许小写、配置路径必须绝对;检验判重两个顺序取平均
任务书 13 第一部分,用户 2026-10-03 同意。
- config:studyId 限定小写字母、数字和 ._-;task、promptDir 必须以 / 开头。
- eval:research.json 路径改绝对,writeConfig 不再转换;quality.ts 第 2、6 问每对 A、B 与 B、A 各问一次取平均,两次原始概率都存。
- tests:大写编号、相对路径各加测试。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:42:11 -04:00

614 lines
74 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.
# InfoRSI 代码结构
这份文件说明代码分成哪些模块、模块之间怎么依赖、流程按什么步骤推进、账本记哪些记录。研究方法本身以 `docs/方法.md` 为准;这里只说代码怎么组织来实现它。
来源:
- 用户 2026-10-01 认可"按负责一件事来切"、下面这套模块与可替换点,以及步骤清单和账本记录种类。
- 用户 2026-10-01 同意:提交的字段定义放 vocab,submissions 只判收不收;vocab 可放规范序列化和哈希两个函数,上限调到 600 行;执行后实际 1030 行,用户同意上限改为 1100 行;任务书 09 加对号清单时用户 2026-10-03 同意改为 1150 行,vocab 只放对号清单,任务材料的其余类型放 materials。
- 用户 2026-10-02 同意:"全部只追加":账本用一张只追加的表,全部记录都不能改。
- 用户 2026-10-02 指出可见性、截止序号、通用唯一键都是过度设计:"每次给到派的材料有哪些"和"一开始灌进去还是让它去查"是分开的两件事;Jev 本质上是一种可以代替程序的判别器。据此:谁能看什么归 materials(按任务定材料),ledger 只存;只在方法要求"只能一次"处加唯一约束;Jev 不单独成记录。ledger 预期 500–900 行。
- 用户 2026-10-02 同意:依赖跟着任务走(一份研究不能自己改依赖),凡是执行任务代码的事都由 runner 在任务镜像里跑,runner 与 tasks 的行数按第七部分。
- 用户 2026-10-02 同意:workspace 用 git,按独立分析给出的窄用法(版本号用目录内容编号、自己列文件存、不读用户配置);实验员目录里除了任务给的"不存"清单,其余都算候选。
- 依据是两次开发调研。一次是模块切分,参考了 Parnas 1972 与 1979、Ousterhout、Cockburn 和同类项目的源码;一次是同类研究的实验设计。
---
## 一、切分原则
不按研究步骤切,按"以后可能会改的设计决定"切,一个模块藏住一个决定。换掉某个决定时,只改这一个模块。
判断一刀切得对不对,问这 7 个问题:
1. 这个模块藏住的是哪一个决定?说不出来,或者说出来的是"第几步",就是切错了。
2. 换掉这个决定,要改几个模块?应该只改 1 个。
3. 对外露出的函数和类型,是否比内部实现简单得多?
4. 依赖有没有绕成圈?下层能不能离开上层单独测试?
5. 纯计算的模块里,有没有读写文件、数据库、网络或模型?
6. 每样外部的东西,是不是只有一个模块直接碰它?
7. 同一份知识(阶段名单、谁能看见什么、某种提交有哪些字段)是不是只在一处定义?
---
## 二、模块
| 层 | 模块 | 藏住的决定 | 依赖 |
|---|---|---|---|
| 入口 | plugin(插件入口) | 用户在 Pi 里怎么操作:命令、状态显示、配置对话;文件锁;把各模块装配起来 | 装配时用到各模块 |
| 组织 | flow(流程) | 按步骤清单推进研究;每次启动走同一条恢复路径 | ledger、rules、materials、submissions、agents、runner、tasks、workspace、config、jev |
| 组织 | export(导出) | 导出给人看的格式 | ledger、rules、materials |
| 组织 | config(配置) | research.json 的字段和校验(每个字段必填,不补默认值);把配置翻译成步骤清单 | rules |
| 组织 | materials(材料) | 每个任务能看到哪些材料(方法.md 第三部分);第一条消息放哪些、什么顺序、怎么写;在这些材料范围内回答模型的查询 | ledger、rules |
| 组织 | submissions(收件) | 一份提交收下还是退回:先程序检查,再问 Jev(字段定义在 vocab) | jev |
| 外部 | ledger(账本) | 记录怎么存、怎么保证不可改、怎么从记录算出当前状态;花钱动作的登记 | 无 |
| 外部 | agents(角色会话) | Pi 的接口:开子会话、发工具和提示词、收提交、记用量 | 无 |
| 外部 | runner(运行器) | Docker 的用法:建镜像、限时、限核、断网、记资源、查一次运行的状态 | 无 |
| 外部 | tasks(任务) | 任务目录里的事实和文件(指标、ε、时限、核数、镜像的 Dockerfile 与依赖清单);一个任务怎么跑、怎么从原始产物独立算分(填圆的合法化也在这里) | 无 |
| 外部 | workspace(代码版本) | git 的用法:一份研究一个裸仓库,版本号是目录内容的编号;存版本(自己列文件,不用 `git add`)、取出、比较两个版本;不读用户的 git 配置 | 无 |
| 外部 | jev(Jev 判别) | 问法、阈值(暂用参考项目的值,等真实研究的数据再校准) | 无 |
| 纯计算 | rules(研究规则) | 全部公式和常数;信念本、战绩、余额的计算 | 无 |
另有一个 **vocab**(共用词汇),所有模块都可以引用,上限 1150 行(多出来的主要是发给模型的字段说明和对号清单)。一样东西要有两个互相不能引用的模块都用到,才放进 vocab;只有一个模块用的,放在那个模块里。它放:
- 共用的类型和名单:角色、研究者阶段、提交种类、指标定义、运行定义与运行结果、模型用量;
- 每种提交的字段定义(用 TypeBox 写,同一份定义既是类型,也用来检查收到的数据,也是发给模型的提交工具参数);
- 只有两个函数:规范序列化和哈希,保证"内容相同就得到同一个指纹"只有一种算法。
依赖只能往下,图中同一行的模块之间不互相依赖。谁具体能依赖谁,以上面的模块表为准,`npm run check` 按那张表检查。
```
入口 plugin
组织 flow
export
materials submissions config
外部 ledger agents runner tasks workspace jev
纯计算 rules vocab(谁都能用)
```
### 几条边界
- **给模型看什么只归 materials。** 每个任务开始时,materials 按方法.md 第三部分定下这个任务能看到的材料。哪些放进第一条消息、哪些等模型用查询工具去读,是同一份材料的两种给法,查询工具查不到这份材料以外的东西。ledger 不判断谁能看什么。
- **收不收一份提交,只在 submissions 一处判。** 跟状态有关的两条由 ledger 在写入时拒绝:这个阶段还开着吗;要替换的那一份是不是本人刚交的。
- **信念本**:数字由 rules 算,写成文字由 materials 负责,不单独存,每次现算。
- **runner 只管怎么跑,tasks 只管怎么判。** 两者互不认识,由 flow 串起来:先向 tasks 要运行定义,交给 runner 去跑,再把产物交回 tasks 算分。凡是要执行任务代码的事(正式运行、判分时跑校验器、轻量检查)都由 runner 在任务镜像里跑,tasks 自己不起进程;只有正式运行登记、扣次数。
- **依赖跟着任务走。** 镜像由任务目录里的 Dockerfile 和依赖清单建出来,研究开始时建一次,镜像编号冻结进账本的研究配置记录;恢复时只核对,本机没有这个镜像就报错停下,不重建。一份研究不能自己改依赖。
- **"下一步做什么"只写在 flow 的一个纯函数里。** 每个步骤一个小文件。步骤文件里如果出现公式、字段名单或可见性判断,说明知识漏进来了,要挪回下层。
---
## 三、步骤清单
研究开始时,config 根据配置生成这份研究的步骤清单,连同每个决策点用哪条规则,一起冻结进账本。flow、导出、恢复都只读这份清单,不再读配置。按配置不做的步骤(第零轮只讨论 1 次时的第 2 次讨论、只有一位研究者时的讨论)不执行,账本里记一条"按配置跳过"。
**整份研究**
```
1. 冻结配置
2. 基线运行
3. 第零轮:写入口 → 讨论(1 或 2 次)→ 认领 → 分配
4. 一轮接一轮,直到余额用完
5. 导出
```
**每一轮**
```
1. 开轮:放信念本,处理留池候选
2. 提案
3. 判重合并
4. 第一次预测(看原始提案)
5. 挑刺
6. 回应与统一 → 协议冻结
7. 第二次预测(看冻结协议)
8. 选批 〔规则:选批规则〕
9. 对每个入选候选:实现 → 核验 →(不通过:修复 → 再核验)→ 正式运行
10. 整批公开,记分数账和信息账
11. 采纳
12. 若采纳的改法有 2 个以上因子:
列格 → 解释 → 选格 → 对每个选中的格:消融实现 → 核验 → 运行 → 结算
〔规则:选格规则〕
13. 收尾,更新信念本
```
### flow 分四部分写,各自验证
用户 2026-10-03 定:参考项目的 flow 太长、很难验证(用户原话:"我觉得我们之前其实很多问题都是 flow 没有做好"),所以拆成四部分,一部分验证通过后再写下一部分,每部分一份任务书。
| 部分 | 管哪些步骤 | 自己的验证目标 | 拿哪份真实记录对照 |
|---|---|---|---|
| 1. 骨架、研究开始、第零轮 | 从账本算出现在到哪;"下一步做什么"的纯函数(步骤先后只写这一处);主循环;恢复入口;派一个模型任务、跑一次正式运行两个共用动作;冻结配置、建镜像、基线;第零轮 | 第零轮的研究质量六问(任务书 12):入口里有没有关键想法;分到的入口分没分开;好方向分出去没有;入口是不是改代码的路线;入口有没有写答案;讨论有没有用。另杀进程再启动,不重跑、不重扣、不漏写 | 关键想法和死路清单取自同类系统与参考项目高分运行的研究路径;16-real-1 第零轮回放,分配和参考项目相同,并用同样六问打分当对照 |
| 2. 开轮到冻结 | 4.3 ①–⑦ | 每次模型调用开始时存下的材料里,没有当时还没公开的东西 | 16-real-1 三轮提案到第二次预测回放;15-tight-2 抄错编号不再卡住 |
| 3. 选批到采纳 | ⑧–⑫ | 扣掉的次数等于确认启动的运行数;修复用完不跑、不扣、记事件 | 选批记录的输入和 16-real-1、15-real-1 存下的比;15-tight-2 第 1 轮的修复 |
| 4. 消融到收尾 | ⑬–⑱ 与研究结束 | 解释冻结早于任何一格启动;每格只结算一次 | 选格输入和 15-real-1、16-real-1 比;最后整份回放 16-real-1,当四部分合起来的验收 |
**怎么验证**(用户 2026-10-03 定):一律用真 Jev、真 Docker,不做假的 Jev、Docker、任务,flow 不为此开替换口子。同样的输入真实跑几次,结果都符合要求就算通过;真实运行里没出现过的情况不专门造出来测。用户原话:"我建议用一律用真 Jev、真 Docker。因为其实我们现在是在做科学研究,不是在开发程序……我们只需要重复运行多次"。模型一侧两种都做:
- 回放:用本机假模型服务,把参考项目真实记录里模型交的东西按原顺序转成新格式再交一遍,走真实的 Pi。新 Jev 的判断可能和当时不同,两边走向分开以后不再对照,按方法判断分开的地方谁对。
- 真模型:用配置里的模型真实跑几次。
**参考项目的记录不是标准答案**:对照前先问"要证明哪一句"。故意和参考项目不同的地方不对齐:第一次预测在挑刺之前、修复用完不暂停、Jev 等于阈值时收下。真实记录里只有 16-real-1 从头跑到尾没暂停;判重合并、留池、一人多条提案、修复用完在全部记录里都没出现过。
---
## 四、账本记录
账本只存发生过的事。信念本、战绩、余额、"现在进行到哪一步"都不存,每次从记录现算。
**全部只追加。** 所有记录放在同一张表里,每一行是:序号(自动递增,就是时间先后)、种类、内容(JSON)、内容的哈希。整张表禁止修改、禁止删除,由数据库触发器保证。要更正就追加一条新记录;"开"和"关"、"替换"也都写成新记录。
恢复时只核对、不重做写入(CLAUDE.md"中断后怎么恢复"),所以记录照写就是,不需要通用的去重。有自己编号、按定义只发生一次的记录,按编号加数据库唯一约束,重复了就报错:阶段开、阶段关、运行登记、确认启动、运行结果、模型调用开始、模型调用结束、消融结算(按轮次:每轮最多采纳一个改法,所以最多一次消融)。没有这种编号的记录(提交的各个版本、事件、用量)照写。前后顺序(比如启动前要先登记)由 flow 的同一个入口保证,ledger 不查。运行编号由 flow 起;确认没启动、退还名额后要重试,就起一个新编号。
| # | 记录 | 内容 | 谁写入 |
|---|---|---|---|
| 1 | 研究配置 | 冻结的配置、步骤清单、每个决策点用哪条规则、方法参数、提示词哈希 | flow(内容由 config 生成) |
| 2 | 阶段 | 每个阶段开一条、关一条;关闭即公开 | flow |
| 3 | 提交 | 各种提交的每一版:入口、讨论、志愿、提案、两次预测、挑刺、回应、解释、收尾、实现、核验:阶段关闭后以最后一版为准,此后由 ledger 拒绝再写。私人笔记的每一条:不属于任何阶段,以最后写入的一条为准。每一版带收下时用到的 Jev 判断(含原始概率和结论,没用到 Jev 的为空)。被退回的提交也记,附对象(同提交的 target:候选、原提案或消融格,没有对象的为空)、退回理由和用到的 Jev 判断 | flow(经 submissions 判定后) |
| 4 | 冻结协议 | 每个候选的冻结版本:方法.md 4.3 ⑥ 列的因子清单、保障清单、指标登记、正文、实现条款,以及轮次、出发的基线、合并了哪些原提案。答复不在其中,留在回应或统一的提交记录里(用户 2026-10-03 定) | flow(主体由 submissions 拼) |
| 5 | 作者自评 | 封存时的三个数;解封时追加一条解封记录 | flow |
| 6 | 冻结代码 | 每份候选代码和消融格代码的版本号,以及因子位置表 | flow(版本号由 workspace 给出) |
| 7 | 运行 | 每次正式运行三条:登记要跑(占名额);确认启动(扣次);结果(分数、合法化、指标、答案路径、资源) | flow(结果由 runner 与 tasks 给出) |
| 8 | 决定 | 程序做的每个决定:选批、采纳、选格、押的因子、入口分配、判重合并。每条存规则名、规则版本、方法参数、种子、全部输入、输出;用到 Jev 的(判重合并),输入里带 Jev 的原始概率 | flow(由 rules 计算) |
| 9 | 结算 | 每格的结局、撑几成、配合关系标签、每条解释的确认或否定、世界比特、惊讶 | flow(由 rules 计算) |
| 10 | 用量 | 模型调用三条:开始(附这次任务的全部材料)、每次回复的用量、结束(带这次调用的墙钟时间;中断后恢复时补写的结束记录,结局记出错、原因写"进程中断",墙钟时间记测不到)。正式运行的资源用量记在运行结果里。只记录 | flow(用量由 agents 交出) |
| 11 | 事件 | 违规记录、"按配置跳过"、"没测"及原因、停下的原因 | flow |
Jev 是一个判别器,和程序检查地位相同,不单独成一种记录:它的原始概率和结论,记在它判断的那件事里(第 3 种的提交和退回、第 8 种的判重合并)。
**写入时由 ledger 拒绝的几种情况**:阶段已关闭还要写提交;要替换的那一份不是本人在这个阶段刚交的;没有"确认启动"就写运行结果("确认没启动"这种结果除外);这次消融已经有格启动了才冻结解释。
---
## 五、可替换点
论文实验要换掉的部件,按种类用最轻的办法换:
| 种类 | 例子 | 换法 |
|---|---|---|
| 外部服务 | Pi 会话、Docker 运行器、任务、Jev | 接口 |
| 决策规则 | 选批:批内最好期望、均匀随机。选格:信息价值减成本、全测、随机同格数、都不测。以后可能加合并方式 | 纯函数,登记在 rules 里的"名字 → 函数"表里,配置写名字 |
| 方法参数 | N、M、批量、运行次数、因子数上限、u、衰减、开放预留 | 配置值,不写成常数,也不用环境变量,每个都必须写在配置文件里。分两层:用户配置(配置对话逐项问);方法参数(做实验时才改,配置对话按方法.md 6.11 的现行值写进文件、给用户看过) |
| 模型与提示词 | 每个角色、每位研究者用哪个模型;提示词目录 | 配置值;账本记下每份提示词文件的哈希 |
步骤开关和对照组"朴素循环":config 里的字段和步骤清单保留(任务书 04 已写好),后面会用到;flow、materials 先只实现完整流程全开,跑稳之后再补对照组(方法.md 第十一部分)。
为了让换规则的实验做得对,结构上守住下面几条:
1. **配置只在一处被翻译**:就是第三部分的步骤清单,flow 不直接读配置里的规则名。
2. **决策函数是纯函数,输入全部从账本读。** 每条决策记录存下规则名、规则版本、方法参数、随机种子、全部输入和输出。种子由研究编号、轮次、版本算出,不用全局随机数。平票按固定规则处理。决策函数要导出,重放脚本和核对脚本直接调用它,不另抄一份公式。这样同一批记录换一条规则,就能离线重算。
3. **衡量好坏的尺子只有一把。** 比特、惊讶、覆盖率永远用同一个固定算法,从每个人的原始范围算出来,不跟着决策规则一起变。
4. **一份研究的配置在开始时冻结进账本**,恢复时以冻结的那份为准。
5. **只提供跑通过的组合。** 每条可换的规则至少有一条小规模的跑通测试。
---
## 六、模块化程度与检查
- 一个 npm 包,每个模块一个目录。别的模块只能通过目录下的 `index.ts` 引用它。
- 用 dependency-cruiser 在 `npm run check` 里强制以下规则:
- 不许循环引用;
- 每个模块只能依赖第二部分表里列出的模块;
- 不许绕过 `index.ts` 直接引用模块内部的文件;
- rules 和 vocab 不许引用文件、数据库、网络、子进程和 Pi;
- 只有 ledger 能用 SQLite,只有 agents 和 plugin 能用 Pi,只有 runner 和 workspace 能起子进程;
- tasks 不许引用 runner。
- 不拆成多个包,也不做 TypeScript 分项目编译。
---
## 七、规模
下表是粗估。动手写某个模块之前,由用户逐个确认;写的过程中超过预期一倍,就停下来检查。
| 模块 | 预期行数 |
|---|---|
| plugin | 600–1000 |
| config | 300–600 |
| flow | 2000–3500 |
| ledger | 500–900 |
| rules | 1500–2500 |
| submissions | 400–600 |
| jev | 400–600 |
| materials | 1200–1900(多出的部分是用户 2026-10-03 让 materials 定义的账本内容形状,以及冻结时给指标名加前缀) |
| agents | 600–700 |
| workspace | 200–300 |
| runner | 250–450 |
| tasks | 公共部分与填圆合计 400–700,另有任务目录里的 Python 文件 |
| export | 500–900 |
| vocab | 上限 1150 |
---
## 八、各模块对外的函数与类型
写到哪个模块时,再把它对外的函数和类型补在这里。
### vocab
下面是 `src/vocab/index.ts` 导出的全部内容。标"字段定义"的,是一份 TypeBox 定义,同名的 TypeScript 类型也一起导出:同一份东西既当类型用,也能在运行时检查数据(`Value.Check`),还能直接当发给模型的工具参数。
**两个函数**
- `canonical(value)`:规范序列化。对象的键排序后再拼成字符串,数字必须是有限数,只接受纯 JSON;内容相同就一定得到同一个字符串。
- `hash(value)`:对规范序列化的结果算 sha256,得到 64 位十六进制的指纹。
**基础**
- `Json`(类型):任意纯 JSON 值。
- `Id`(字段定义):编号的格式,只许字母、数字和 `._-`,首字符是字母或数字,最长 128 个字符,因为编号会被拼进文件路径和命令行。
- `DIRECTIONS`、`Direction`(字段定义):指标方向,只有越大越好(maximize)和越小越好(minimize)两种。
- `Measured`(字段定义):测到 / 测不到。测到带值,测不到带原因,所有可能测不到的量都用它,不拿 0 或 null 冒充。
**名单**
- `ROLES`、`Role`:三个角色,研究者、实验员、核验员。
- `STAGES`、`Stage`:有人交提交的各个阶段,按流程先后排;程序自己做的步骤不在里面。
- `SUBMISSION_KINDS`、`SubmissionKind`:每种提交属于哪个阶段、由哪个角色交,各模块都从这张表取。
**指标**
- `METRIC_LAYERS`、`MetricLayer`:指标的三个层次,最终、常规辅助、假设专用。
- `MISSING_POLICIES`、`MissingPolicy`:测不到时怎么办的两档,缺了只记缺失、缺了整次作废。
- `MISSING_POLICY_EFFECTS`:两档各自后果的白话说明,字段说明和给模型的材料共用这一份。
- `HypothesisMetric`(字段定义):研究者在提案、回应、统一里登记的一条假设专用指标。编号是短名:字母、数字和 `_-`,不能含 `.`,最长 64 个字符。冻结时改为全名"候选编号.短名"(materials 的 `freezeProtocol`,用户 2026-10-03 定),冻结协议和 tasks 收的指标沿用这个类型,编号是全名;短名不带 `.`、全名都带,两者不会重名。
- `MetricDefinition`(字段定义):一条完整的指标定义,带编号和层次。
**运行**
- `RunDefinition`(字段定义):tasks 交给 runner 的运行定义,写明命令、工作目录、按 sha256 钉死的镜像和运行时、时限、核数和环境变量;候选进程不继承宿主的环境变量。
- `ProcessResult`(字段定义):runner 交出的进程层面结果,分没启动、正常退出、超时、从外面被杀四种;启动过的带资源用量,从外面被杀的三项资源都记测不到。
- `VERDICTS`、`Verdict`:一次运行的判定,算数、不算数二选一。
- `ANSWER_PATH_KEY`、`SAFEGUARD_TRIGGERS_KEY`:候选在指标文件里多写的两个保留键名,`answer-path` 和 `safeguard-triggers`。
- `AnswerPath`(字段定义):读到的答案路径原样字符串,或读不到及原因;在不在位置表里由拿得到位置表的模块现算。
- `SafeguardTriggers`(字段定义):读到的"保障短名 → 动手次数"原样内容,或读不到及原因。
- `JudgedResult`(字段定义):tasks 从原始产物独立判分的结果,包括判定和理由、分数、各指标取值、答案路径、保障动手次数和任务自己的附加记录。
- `ModelUsage`(字段定义):一次模型调用的用量,四种 token 数和价格,每一项都可能测不到。
**提交**
- `Range`(字段定义):三个数 q1、q5、q9,即一个分数有一成、一半、九成可能低于多少。
- `SUBMISSION_SCHEMAS`:每种提交的字段定义,键与 `SUBMISSION_KINDS` 相同;同一份定义是类型、收件格式检查和提交工具参数。
- `Submission<K>`(类型):某种提交的内容类型,例如 `Submission<"proposal">`。
**Jev**
- `JevJudgment`(字段定义):Jev 对一个问题的一次判断:问题名、问题全文的哈希(措辞、示例、阈值一起算)、阈值、原始概率、结论(概率是否超过阈值)、Jev 返回的请求编号。jev 交出它,ledger 在提交和退回记录里原样存它,两个模块互不引用,所以放在这里。
- `Checklist`、`ChecklistOf<K>`(类型):对号清单,materials 为一次任务算出、submissions 拿来判收不收;按提交种类区分,每种只放它要用的(入口和讨论的入口个数范围;认领的入口编号和至少排几个;提案的 M、因子数上限、能延续的因子、能引用的假设专用指标(全名)、已占用的指标编号(任务的指标和两个保留键);两次预测的候选;挑刺的原提案和它的因子;回应、统一的挑刺编号(统一另有被合并的原提案);实现、修复、核验的条款、因子、保障(核验另有没有归属的改动);解释的格和 M;消融实现的格、要关的因子、全部因子;消融核验的格和全部因子;收尾无)。用户 2026-10-03 决定:vocab 只放这一个,任务材料的其余类型放 materials。
### rules
下面是 `src/rules/index.ts` 导出的全部内容。rules 是纯计算:输入数字,输出数字,不碰文件、数据库和模型,也不用全局随机数。λ、ε、已知最好、每批上限、余额这些参数都由调用方传进来,rules 里只写死方法.md 6.11 标"固定"的几个常数。三个数一律用 vocab 的 `Range`。编号(候选、研究者、入口、因子、解释)一律按自然序比较:数字部分按数值大小比,所以 c2 排在 c10 前面。
几个反复出现的词:**折线**是把一个人的三个数画成"分数低于 x 的把握"的分段直线(6.2);**合并分布**是几条折线按权重加起来的结果(6.3);**改进量**是分数比基线好了多少,正数表示变好,越小越好的任务也换算成正数为好(6.1)。决定类函数的返回值里带着写决定记录要用的中间数,不只返回"选了谁"。
**常数与基础**
- `INTERACTION_CUT`:配合关系标签里相对交互量的界限 0.2(6.6"配合关系标签")。
- `INDISPENSABLE_SUPPORT`:判"缺一不可"的撑几成界限 0.9(6.6)。
- `NET_TOLERANCE`:选格时比较净值的容差 1e-12,差距在它以内的组合当作一样好(6.7"选格")。
- `GRID_COMBINATION_CAP`:只测一部分格时,网格组合总数的上限 4,000,000(6.7"信息价值")。
- `GRID_SUBDIVISIONS`:网格里每段最多再等分 20 份(6.7"信息价值")。
- `epsilonFloor(ε)`:算 ε/100;采纳线下限、剩余空间下限、比特格宽三处共用这一个函数,保证末位一致(6.11"跟随 ε")。
- `improvement(from, to, direction)`:从 from 到 to 按指标方向好了多少,越大越好时是 to − from,越小越好时是 from − to(6.1)。
- `compareIds(a, b)`:按自然序比较两个编号,供所有排序和"编号最小"的判断使用;p01 和 p1 这类逐段相同的编号再按整串定先后,不会判成相等。
- `combinations(n, k)`:列出从 0..n−1 里选 k 个的全部组合,按字典序排,选批和选格列组合时用。
**范围与合并分布(6.1–6.3)**
- `Point`、`Knots`(类型):折线上的一个点(横坐标,低于它的把握),以及按横坐标严格递增排列的一串点。
- `Component`、`Mixture`(类型):合并分布的一个成分(一条折线加它的权重),以及成分的列表,权重加起来是 1。
- `personKnots(range)`:把一个人的三个数变成五个点的折线,两端各补一段与相邻半宽相同的尾巴(6.2)。
- `below(knots, x)`:读折线在 x 处"低于 x 的把握"。
- `knotsMean(knots)`:折线所代表的分数的平均值(期望)。
- `density(knots, x)`:折线在 x 处的斜率,也就是把握在这一点有多集中(密度);押的因子更新权重时用(6.8)。
- `mapKnots(knots, f, decreasing)`:把折线的横坐标换到另一把刻度上;换算是反向时点序倒过来、把握换成 1 减原把握。
- `reserveKnots(lines)`:开放预留,即给"大家都没想到的结果"留的那一份:取所有折线的最低点到最高点,左右各再放宽一半,在上面均匀铺开(6.3)。
- `authorShares(authors)`:每条解释的占比,研究者之间等权,同一人的几条平分(6.3"合并"、6.7"成分与权重")。
- `mixtureBelow(m, x)`:合并分布在 x 处"低于 x 的把握"。
- `breakpoints(m)`:合并分布全部折点的横坐标,去重、升序。
- `mapMixture(m, f, decreasing)`:把合并分布的每个成分都换到另一把刻度上,权重不变。
- `forwardMixtures(candidates, λ)`:正向每个候选的合并分布,(1 − λ) × 各人折线平均 + λ × 开放预留,开放预留由这次选批的全部候选、全部人共用(6.3"正向")。
- `ablationCellMixture(entries, λ)`:消融一格的合并分布,各条解释按 `authorShares` 分 1 − λ,这一格自己的开放预留占 λ(6.3"消融")。
- `toImprovement(m, b, direction)`:把合并分布从分数原值换算成相对基线 b 的改进量;越小越好时刻度反向,一成和九成随之对调(6.1)。
- `mixtureQuantile(m, p)`:合并分布的分位点,即"有 p 的把握低于多少";只给人看,不参与决定。
- `mixtureRange(m)`:合并分布的一成、一半、九成,按分数原值,消融格显示"大家合起来的范围"用。
- `forwardMergedRange(imp, b, direction)`:正向候选"大家合起来的范围",在改进量上读三个数再换回分数原值。
- `integrateUpperTail(ds, from)`:按段精确算 ∫[1 − ∏F] dt,正向的批内最好期望和消融全测时的信息价值共用这一个积分。
**正向:入池、选批、采纳(6.4、6.5,4.3 ⑫)**
- `lossProbability(imp, ε)`:候选"比基线低 ε 以上"的把握;严格小于一半才入池(6.4"入池")。
- `batchValue(imps)`:一组候选的批内最好期望 V(S),即这批里最好的那个预计比基线好多少,低于基线按 0 算(6.4)。
- `BatchInput`(类型):两条选批规则共用的输入:候选及各人的三个数、基线、方向、ε、λ、每批上限、余额、种子。
- `BatchDecision`(类型):选批结果,含 6.4"选批记录写入"的全部内容:参数、批量 k、每个候选的合并范围和是否入池、各组合的 V、选中的组合及其 V、候选池的 V、分数尺读数。
- `selectBestExpected(input)`:选批规则"批内最好期望",在候选池里列出全部组合取 V 最大的,V 完全相等时取编号小的组合(6.4)。
- `selectUniformRandom(input)`:选批规则"均匀随机",用种子从全部候选里抽,不看入池门槛;入池、合并范围、分数尺读数照样算出来记下(第十一部分)。
- `AdoptionLineInput`(类型):算采纳线要的基线 B、已知最好 K\*(没有时为 null)、ε 和方向。
- `adoptionLine(input)`:采纳线(6.5),返回线的值和 (K\* − B) ÷ 4 的原值;没有已知最好时是 ε,结果不低于 ε/100。
- `adopt(input)`:采纳判断,只看有有效分数的候选,最好的那个比基线高出采纳线以上才采纳;返回采纳线、最好的候选及其改进量和采纳结果,够写采纳记录。
**消融结算(6.6,4.3 ⑬ ⑰)**
- `CellOutcome`(类型):一格的五种结局:有效分数、证据不足、没测、跑不起来、一起关(带实际关掉的因子和可能的分数)。
- `AblationCell`(类型):一格,即一个因子和它的结局。
- `AblationExplanation`(类型):一条解释,带作者和它对每一格给的三个数。
- `listCells(factors)`:列格,被采纳改法的每个因子各一格,按因子编号排(4.3 ⑬)。
- `totalGain(baseline, full, direction)`:总提升 G = f(完整) − f(基线);G ≤ 0 说明上游选错了改法,直接报错(6.6"撑几成")。
- `supportOf(baseline, full, off, direction)`:某个因子撑几成,即关掉它后损失的分数占总提升的比例,不截断(6.6)。
- `SynergyLabel`(类型):配合关系标签的代号,六种:关掉反而更好、不全、缺一不可、互补、替代、各管各;中文说明以后由 materials 写。
- `synergyLabel(supports)`:按 6.6 那张表从上往下判出标签,同时返回各格撑几成之和与相对交互量(有格没测出有效分数时这两个数为 null)。
- `sharedSettings(cells)`:判断哪些"一起关"的格其实是同一种设置(关的因子集合相同),给出该沿用哪一格的结果(6.6)。
- `ExplanationVerdict`(类型):一条解释在一格上的判定,确认或否定。
- `judgeExplanations(explanations, cells)`:每条解释在每个有效格上,实测落在它的一成到九成之间(含两端)记确认,否则记否定(6.6)。
- `inseparableGroups(judgments)`:找出在所有有效格上确认与否定完全相同的解释,归成"现有测法分不开"的组(6.6)。
- `ablationFinding(input)`:消融中发现,找出比当前基线高出采纳线以上的格,几格都过线时取分数最好的、同分取因子编号小的;返回采纳线、(K\* − B) ÷ 4、全部过线的格和选中的格(4.3 ⑰)。
- `SettleInput`(类型):一次消融结算的输入:基线、完整分数、方向、ε、已知最好、λ、被采纳改法的因子清单、各格结局、各条解释。清单里每个因子都要有一格,缺了直接报错。
- `settleAblation(input)`:一次消融的完整结算,给出 G、每格的撑几成和合并范围、有效格的世界比特、惊讶和分歧量、配合关系标签、确认与否定、分不开的组、消融中发现(4.3 ⑰、6.6、6.9);押的因子另由 `chooseBet` 算。
**信息价值与选格(6.7、6.8)**
- `OPEN_RESERVE`:成分名单里开放预留的名字,排在最后。
- `AblationModelInput`(类型):建成分模型要的输入:基线、完整分数、方向、已知最好、ε、λ、因子编号、各条解释。
- `AblationModel`(类型):成分模型:因子、成分(各条解释加开放预留)、起始权重、G、剩余空间 h、每格每成分的折线(分数原值和撑几成两种刻度)及期望、每格的事前期望撑几成。
- `remainingSpace(full, knownBest, ε, direction)`:剩余空间 h,有已知最好时是 max(K\* − f(完整), ε/100),没有时是 1(6.7"方向收益")。
- `ablationModel(input)`:建成分模型,每条解释一个成分、开放预留一个成分,解释合起来占 1 − λ(研究者等权、同一人平分),开放预留占 λ(6.7"成分与权重")。
- `gridSize(k)`:测 k 格时每格的小段数 m,即满足 m^k ≤ 4,000,000 的最大整数(6.7"信息价值")。
- `cellGrid(knots, k)`:把一格按折点切成小段,返回各小段中点和每个成分在各小段上的把握;小段数的规则按 6.7 原文。
- `measuredGrid(model, c, k)`:模型里第 c 格的网格,在分数原值上切段,再把中点换算成撑几成。
- `informationValueExact(model)`:全部格都测时的信息价值,逐段精确积分。
- `informationValueGrid(model, S)`:用网格算的信息价值,不论测几格都走网格,主要用来和精确算法互相对照。
- `informationValue(model, S)`:信息价值 VOI(S),即测了 S 这几格之后,挑出最该押的因子能比现在多挣多少(6.7);全测走精确积分,只测一部分走网格,空集是 0。
- `ComboParams`(类型):算价值与成本要的余额、每批上限、知识被用上的概率 u、衰减 r。
- `ComboValue`(类型):一个测格组合的 VOI、R̂、m′、价值、成本、净值(6.7"价值减成本")。
- `evaluateCombos(model, params)`:列出格数不超过"余额 − 1"的全部非空组合并算价值、成本、净值,另给 ĝ₀ 的原值。
- `CellRuleInput`(类型):四条选格规则共用的输入,即成分模型的输入加 `ComboParams` 和种子。
- `UntestedReason`(类型):一格没测的原因:剩余次数不够、信息价值不足以抵过成本、随机没抽中、规则是"都不测"。
- `CellDecision`(类型):选格结果,含 6.7 要求附上的全部数字:参数、种子、G、h、成分与起始权重、各因子的事前期望收益、ĝ₀、全部组合、选中的格及其净值,以及每一格测不测、没测的原因和含它的净值最大组合。
- `topCombo(combos)`:在给定的组合里挑净值最大的,差在 1e-12 以内算一样好,再先取格数少的、后按因子编号比(6.7"选格"第 2 条)。它不管净值是否大于 1e-12,所以也用来挑"含这一格的最好组合"。
- `pickByNet(combos)`:按净值挑要测的组合。先只留净值大于 1e-12 的组合,一个都没有就一格都不测;再在留下的组合里用 `topCombo` 挑。
- `selectByValue(input)`:选格规则"信息价值减成本"(6.7"选格")。
- `selectAll(input)`:选格规则"全测",按因子编号测前"余额 − 1"格(第十一部分)。
- `selectRandomSameCount(input)`:选格规则"随机抽同样多的格",格数和"信息价值减成本"会选的一样,用种子抽(第十一部分)。
- `selectNone(input)`:选格规则"都不测"(第十一部分)。
- `BetResult`(类型):押的因子的结果:更新后的成分权重、权重是否无从更新、各因子的方向收益(实测换算的或估计的)、挑出的因子及其预测收益、押或不押的两种情况、关掉反而更好的格。
- `chooseBet(input)`:押的因子,用有效格的实测更新各成分的权重,再挑方向收益最大的因子;有格关掉反而更好或估计收益不为正时不押,实测落在所有成分之外时保持起始权重(6.8)。
**信息账(6.9)**
这一组是方法里"唯一的尺子",不随选批、选格规则变,一律在分数原值上从本批基线 b 起按格宽切小格。
- `bitWidth(ε)`:格宽 Δ = ε/100。
- `entropyBits(ps)`:一组把握的拿不准程度 −Σ p log₂ p,单位是比特。
- `worldBits(m, origin, width)`:世界比特,即合并分布在宽 Δ 的小格上有多拿不准。
- `cellIndex(origin, width, v)`:实测 v 落在第几个小格,和切格用同一套边界。
- `surprise(m, origin, width, value)`:惊讶 = −log₂(事前给实测所在小格的把握),返回格号、事前把握和惊讶值;事前把握为 0 时惊讶记"测不到"并写明原因。
- `disagreement(entries, origin, width)`:分歧量,大家平均后的世界比特减去各人世界比特的平均,量的是人与人之间的差别,不掺开放预留,只记录。
- `Placement`(类型):落点四段:低于一成、一成到一半、一半到九成、高于九成。
- `placement(range, v)`:实测落在一个人范围的哪一段。
- `covers(range, v)`:实测是否落在一成到九成之间(含两端),也是 6.6 里"确认"的判据。
- `coverage(samples)`:落点四段的计数和覆盖率;一条样本都没有时覆盖率记"测不到",不记 0。
**余额与战绩(第七部分、6.10 第 4 条)**
- `RunState`(类型):一次正式运行在账上的三种状态:已预留还没核对出结果、确认启动、确认没启动。
- `runBalance(total, runs)`:从运行记录数出已扣次数、占用名额和余额;确认启动的扣一次,已预留的先占着名额,确认没启动的退还。只数运行次数,不算计算时间。
- `forwardBatchSize(batchCap, balance)`:本轮正向批量 k = min(每批上限, 余额)。
- `ablationCellLimit(balance)`:消融最多测"余额 − 1"格,余额为 0 或 1 时是 0。
- `Phase`(类型):正向或消融。
- `Tally`(类型):一个人的战绩:总数、中位数偏高几次、偏低几次、被穿过上沿几次、下沿几次、包住几次、落点四段计数。
- `trackRecord(samples)`:按研究者、按正向与消融分开数战绩,只用来显示,不改任何权重(6.9、6.10)。
- `authorVsMerged(items, direction)`:"作者自评 vs 大家合并",给出作者比大家乐观多少,以及实测落在两个范围的哪一段;只收已经跑过、有有效成绩的候选(6.10 第 4 条)。
**入口分配、种子与规则登记表(4.2、第九部分、第十一部分)**
- `EntryAssignment`(类型):一个研究者分到的入口,以及它是第几志愿(按剩余入口分到的为 null)。
- `assignEntries(input)`:第零轮的入口分配,按志愿一轮一轮分,争同一个入口时先给作者、再给编号最小的人,志愿用完的人按编号拿剩下的入口;结果里带志愿序号,够写分配记录(4.2"分配规则")。
- `decisionSeed(input)`:决定的种子,由研究编号、轮次、版本经 vocab 的 `hash` 算出(第九部分)。
- `seededRandom(seed)`:从种子确定地展开一串随机整数,同一个种子永远得到同一串数,不用 `Math.random`。
- `seededSample(items, size, seed)`:用种子从一组编号里不放回地均匀抽 size 个,结果只取决于种子和编号集合,与传入次序无关;候选和因子都这样抽。
- `BATCH_RULES`、`BatchRuleName`:选批规则登记表,名字 → 函数和版本号,两条:批内最好期望、均匀随机。
- `CELL_RULES`、`CellRuleName`:选格规则登记表,四条:信息价值减成本、全测、随机抽同样多的格、都不测。
- `runBatchRule(name, input)`:按名字跑一条选批规则,结果带上规则名和版本,flow 写决定记录时从这里取。
- `runCellRule(name, input)`:按名字跑一条选格规则,结果带上规则名和版本。
### ledger
下面是 `src/ledger/index.ts` 导出的全部内容。账本是一个 SQLite 文件里的一张只追加的表;外面拿不到数据库连接,只能经下面这些函数读写。ledger 不判断谁能看到哪些记录,那归 materials。
**打开与写入**
- `Ledger.open(path)`:打开一份研究的账本,新文件就建表,旧文件核对表结构版本号(现为 4),对不上就报错。
- `ledger.close()`:关闭数据库。
- `ledger.append(kind, content)`:追加一条记录并返回它。先按种类检查内容,再做第四部分说的四种拒收检查(第 1 条是只收开着的阶段的提交,即有开阶段记录、没有关阶段记录),都在同一个事务里。有自己编号、按定义只发生一次的记录按编号加了唯一约束,重复写就报错:阶段开、阶段关、运行登记、确认启动("结果是没启动"也算在这一条里,一次运行两样只能有一样)、运行结果、模型调用开始、模型调用结束、消融结算(按轮次,一轮一条);其余记录照写。记录之间的先后(例如结果前要先有登记)不查。写提案时,作者自评从提案里拆出来,在同一个事务里另存成一条封存。
- `ledger.transaction(fn)`:里面的几次追加要么全部写进去,要么一条都不写;fn 必须是同步函数,返回值类型排除了 Promise,异步函数在类型检查时就被挡住;不能嵌套。
**读取**
- `ledger.records()`:全部记录,按序号排,读出时按种类重新检查内容。
- `ledger.stageClosed(stageId)`:这个阶段关了没有。
- `ledger.latestSubmission(slot)`:某份提交(阶段、作者、对象、编号)的最新一版。
- `ledger.pending()`:未完成的动作,即登记了还没有结果的运行(带着有没有确认启动)和开始了还没有结束的模型调用,恢复时由 flow 逐个核对。
- `Pending`(类型):`pending()` 的返回值。
**类型**
- `Kind`:记录的种类,共 19 种:`config`(研究配置)、`stageOpen` / `stageClose`(阶段开、关)、`submission`(提交的每一版,外层带收下时用到的 `JevJudgment` 数组,没用到 Jev 的为 null)、`rejection`(退回,带对象 `target`(与提交的 target 同义,没有对象的写 null;同一人在同一阶段可能对几个对象各做一次同种任务,materials 按它找本人这个对象的上一次退回)、原样内容和理由;Jev 退回的,原样存这次用到的一条或几条 `JevJudgment`,程序检查退回的这一项写 null)、`note`(私人笔记,每次修改追加一条,以最后写入的一条为准,不带版本号)、`protocol`(冻结协议)、`seal` / `unseal`(作者自评的封存、解封,都用记录序号指明是哪一条)、`code`(冻结代码)、`runPlanned` / `runStarted` / `runResult`(运行的登记要跑、确认启动、结果)、`decision`(决定)、`settlement`(一轮的消融结算,按轮次唯一)、`callStarted` / `callUsage` / `callEnded`(模型调用的开始,附这次任务的全部材料;每次回复的用量;结束,带墙钟时间(秒,`Measured`:中断后恢复时补写的结束记录,结局记出错、原因写"进程中断",墙钟时间记测不到),结局与 agents 的会话结局同名:交了、没交(带原因)、出错(带原因)、被停下)、`event`(事件)。
- `AppendKind`:调用方能追加的种类,除了 `seal`(封存只由 ledger 在写提案时一并写)。
- `Content<K>`:某种记录存下的内容。
- `Input<K>`:追加时交的内容;只有提交和存下的不同:提案带着作者自评。
- `LedgerRecord<K>`:一条记录:序号、种类、内容的哈希、内容。
- `SubmissionSlot`(类型):一份提交不论第几版:阶段、作者、提交种类(版本号只在同一种提交里往下接)、针对谁(候选或格,没有就空)、这一份的编号(只有提案、挑刺这两种一人能交好几份的才有,其余必须为空)。
- `StoredSubmission`、`SubmissionInput`(类型):存下的提交和追加时交的提交,按提交种类区分内容类型;版本号从 1 起,替换时加 1,被替换的就是上一版;`jev` 和 `content` 并列,存这一版收下时用到的 Jev 判断,不进 `content`。
### config
下面是 `src/config/index.ts` 导出的全部内容。config 依赖 rules,只为从规则登记表取规则名和版本。账本里"研究配置"那条记录只存 Json(ledger 在更下层,不能引用 config),flow 从账本读回配置时,再用这里的 `checkConfig` 检查一遍。
**读取与校验**
- `checkConfig(value)`:按字段定义检查一份已经解析好的配置,合格就原样返回带类型的配置;不合格就报错,一次列出全部错误,每条写成"字段路径:原因"。读 research.json 后、从账本读回冻结的配置时都用它。
- `loadConfig(path)`:读 research.json,解析 JSON(不是合法 JSON 时报错带文件路径),再交给 `checkConfig`。
**字段定义**
- `ResearchConfig`(字段定义):research.json 的全部字段,全部必填,不认识的字段报错。每个字段带中文说明(`description`)和类别(`category`:用户配置或方法参数),配置对话照着它问,不另写一份。`models` 里除了各角色的模型,还有 Jev 用的模型名(`models.jev`,用户配置);Jev 的密钥不进配置,从环境变量读。流程是两选一:完整流程写四个开关、选批规则、它才用得到的 λ、u、衰减;朴素循环只写 `preset`,别的都写不进去。
- 研究编号 `studyId` 只许小写字母、数字和 `._-`,首字符是小写字母或数字:它会进 Docker 镜像名,镜像名只认小写。(来源:用户 2026-10-03 同意,任务书 13)
- `task`、`promptDir` 必须是绝对路径:从别的目录接着跑一份研究时,相对路径会读到别处,而任务目录没有哈希核对挡着。(来源:用户 2026-10-03 同意,任务书 13)
- `FIELD_CATEGORIES`、`FieldCategory`:字段的两种类别,用户配置、方法参数。
- `THINKING_LEVELS`、`ThinkingLevel`:Pi 0.85.1 接受的七个思考档位,出处写在代码注释里;config 不能引用 Pi,所以照抄一份。
**步骤清单**
- `stepList(config)`:纯函数,把校验过的配置翻译成整份研究和每一轮的步骤清单。每一步标"执行"或"按配置跳过"(带原因),被跳过的也列出;选批、选格两步带规则名和版本。
- `StepList`(类型):`preset`(完整流程还是朴素循环,materials 据此决定给什么材料)、整份研究的步骤、每一轮的步骤。
- `Step<S>`(类型):一步:编号、规则(只有选批、选格有,其余为空)、执行或跳过及原因。
- `RuleRef`(类型):决策点用的规则,名字和版本。
- `STUDY_STEPS`、`StudyStepId`:整份研究的步骤,按先后排。
- `ROUND_STEPS`、`RoundStepId`:每一轮的步骤,按先后排;比第三部分拆得更细(例如"回应与统一"和"协议冻结"分成两步,挑刺与回应关掉时协议照样冻结)。
### runner
下面是 `src/runner/index.ts` 导出的全部内容。runner 用 docker 命令行做事,不碰账本,自己也不存状态:依据都在 Docker 里,所以新进程里新建的 `Runner` 能接着等早先启动的容器。Docker 不应答时,查状态答"问不到",其余函数直接报错,都不重试。
- `new Runner(dockerHost)`:Docker 守护进程的地址必须给出;写 null 表示不加 `-H`,由 docker 命令按自己的环境变量和 context 决定。
- `runner.buildImage(contextDir, tag)`:用目录里的 Dockerfile 建镜像,交回镜像编号(`docker image inspect -f '{{.Id}}'` 的值)。
- `runner.hasImage(digest)`:本机有没有这个镜像编号。
- `runner.start(name, definition, runDir)`:起一个容器,运行目录挂到运行定义的工作目录;容器里以 root 运行的计量 shell 把命令降到宿主当前用户再跑,结局、退出码、墙钟、CPU 秒、峰值内存写在容器内只有 root 可写的目录里,命令改不了。`docker run` 失败时按名字再查一次,容器不存在或建了没启动算没启动(建了的删掉),在跑或已结束算启动了。
- `runner.wait(name, logDir)`:等容器结束,交回 `ProcessResult`,把标准输出、标准错误和从容器里拷出的结果文件存成日志目录里的 `stdout.log`、`stderr.log`、`usage`。有结果文件按文件定正常退出或超时;没有就是从外面被杀。
- `runner.status(name)`:没有这个容器、建了没启动、在跑、已结束、问不到,五种之一。
- `runner.remove(name)`:删掉一个已结束或没启动的容器,flow 写完结果后调用。
- `ContainerStatus`、`StartOutcome`(类型):查状态的五种回答;启动了,或没启动和 Docker 的报错原文。
容器名由调用方给,必须是确定的(恢复时按名字去问);runner 只检查它符合 Docker 的命名要求。没有取消函数:方法里没有程序主动取消一次已启动运行的场合。
### tasks
下面是 `src/tasks/index.ts` 导出的全部内容。tasks 不起子进程:凡是要执行任务代码的事(正式运行、判分时跑校验器、轻量检查)都只交出 `RunDefinition`,由调用方交给 runner 在任务镜像里跑,再把产物交回这里解读。任务种类到实现是 `task.ts` 里的一张对照表,现在只有填圆。各目录(运行目录、判分目录、检查目录)都由调用方建好、传空目录进来。
**任务事实**
- `TASK_KINDS`、`TaskKind`:任务种类名单,现在只有 `circle_packing`。
- `TaskFacts`(字段定义):task.json 的字段:任务种类、最终指标的编号和方向、ε、已知最好(没有写 null)、单次时限、核数、Docker 运行时、不许改的文件名单、存代码版本时跳过的名字(`unversionedNames`,只写名字不带 `/`,在任何一层都跳过;填圆是 `__pycache__`、`.git`、`solution.json`、`metrics.json`)。全部必填,不认识的字段报错。
- `checkTaskFacts(value)`:严格检查一份已解析的 task.json,一次列出全部错误,每条写成"字段路径:原因"。
- `Task`(类型):读进来的一个任务:task.json 的内容、任务目录(Dockerfile 所在,给 runner 建镜像)、基线目录、TASK.md 全文。
- `loadTask(taskDir)`:读任务目录,检查 task.json,交回 `Task`;"不存"清单在 `task.facts.unversionedNames` 里,flow 存版本时把它交给 workspace。
- `metricDefinitions(task)`:最终指标和常规辅助指标的定义(墙钟、CPU 秒、峰值内存、最小间隙、原分数、原违反量、扣掉的分);辅助指标的定义写在填圆的代码里。
- `protectedChanges(task, changedFiles)`:改动过的文件里属于"不许改"名单的那些。
**运行**
- `prepareRun(task, candidateDir, runDir, hypothesisMetrics)`:把候选代码、任务的 harness 和这次要测的假设专用指标说明(metrics-spec.json)放进运行目录。
- `formalRun(task, imageDigest, seed)`:正式运行的运行定义;种子由调用方给,同一份研究的全部运行用同一个,经环境变量交给 harness。
- `lightCheck(task, candidateDir, checkDir, imageDigest, seconds)`:把候选代码和检查脚本放进检查目录,交回轻量检查的运行定义;只看语法和入口函数,不执行候选代码,次数上限不归这里管。
- `readLightCheck(task, checkDir, result)`:解读轻量检查:通过,或不通过及原因;检查脚本自己没正常跑完时报错停下。
- `LightCheckOutcome`(类型):轻量检查的结论。
**判分**
- `prepareJudging(task, runDir, judgeDir, imageDigest, formal, hypothesisMetrics)`:判分第一步。正式运行超时、从外面被杀、退出码非 0,或者没有产物,直接交回不算数的判分结果;否则把产物和宿主保管的校验器复制进判分目录,交回校验器的运行定义。
- `judge(task, runDir, judgeDir, formal, validator, hypothesisMetrics)`:判分第二步。读校验器写的判定和候选写的指标文件,加上正式运行的结果,交回 `JudgedResult`:校验器判合法(含合法化后合法)算数;判不合法、或"缺了整次作废"的假设专用指标测不到,不算数。合法化的三个数既进 `extra`,也是常规辅助指标的取值;测不到的指标记测不到并附原因。校验器自己没正常跑完、判定文件认不出、修后分数高于原分数时报错停下,不当成候选的失败。
- `JudgingStep`(类型):判分第一步的结果,已判定或要跑校验器。
### workspace
下面是 `src/workspace/index.ts` 导出的全部内容。一份研究一个裸仓库,位置由调用方给;版本号是目录树编号(tree id),不造提交,每个版本挂一个 `refs/versions/<版本号>` 引用防 `git gc` 清理。"上一版是谁""现在的基线是哪个版本"都由 flow 记进账本,git 里不记。只用 git 的底层命令,每个 git 子进程都不读系统和用户的配置与 attributes 文件,`GIT_DIR` 只放进子进程的环境。git 命令失败直接报错,写明命令和 git 的报错原文,不重试。
- `createRepository(repo)`:在给定位置建一个空的裸仓库。
- `saveVersion(repo, dir, excludedNames)`:把目录现在的内容存成一个版本,交回版本号;自己遍历目录列文件(不用 `git add`,`.gitignore` 不起作用),名字在清单里的文件或目录在任何一层都跳过,空目录不进版本,可执行位记成 100755、其余 100644。遇到符号链接、设备文件这类既不是普通文件也不是目录的东西,交回"存不了"和那个路径。同样的内容永远得到同一个版本号。
- `checkoutVersion(repo, version, dir)`:把一个版本的文件原样写进调用方建好的空目录,还原可执行位;目录里没有 `.git`。
- `compareVersions(repo, from, to)`:比较两个版本,交回改动过的文件(路径;新增、删除还是修改;是否只改了可执行位;是否二进制;非二进制文件每一块改动在新旧两边的行号范围),以及给模型看的带 3 行上下文的 `git diff` 原文,不加颜色。
- `SaveOutcome`(类型):存好了和版本号,或者存不了、路径和原因。
- `LineRange`、`Hunk`(类型):一块改动在一侧是"第几行到第几行";这一侧没有行时写成"在第几行之后"(0 表示文件开头),同 `git diff -U0` 的写法。
- `FileChange`、`Comparison`(类型):一个改动过的文件,二进制文件不拆块;比较的全部结果。块可以为空:空文件的新增、删除和只改可执行位都没有块,按整个文件改了算。
### materials
下面是 `src/materials/index.ts` 导出的全部内容。谁能看什么每种任务一个函数,对着方法.md 第三部分、4.2、4.3 写,不写通用判定(用户 2026-10-02)。materials 只读账本,不写;任务材料由 flow 整份存进"模型调用开始"记录。依赖只有 ledger、rules、vocab;代码比较结果、运行产物、提示词由 flow 传进来。
- `buildMaterial(req)`:生成一份任务材料:给模型看的各节、可查询的记录、拼好的第一条消息、对号清单、能读的代码版本。研究者的材料按"任务说明 → 你自己的 → 公共材料 → 现在交什么"排;实验员、核验员的按"任务说明 → 分给你的材料 → 现在交什么"排,不给信念本和成绩。同一人重新发起同一任务时,最前面放上一次被退回的原因:按阶段、作者、提交种类、对象找本人最后一条退回记录。"现在交什么"由 vocab `SUBMISSION_SCHEMAS` 的字段说明生成。几种任务的取法:
- 第二次预测:冻结协议不带答复,"挑刺与答复"从这个候选已公开的最后一份回应或统一里读(回应另给答复正文);留池候选读它冻结那一轮的(用户 2026-10-03 定)。
- 核验:不放 Jev 对这份实现的判断。能走到核验说明 Jev 已判过,Jev 是判别条件,不是给核验员的提示。
- 消融实现:这一格已有公开的消融核验(核验不通过、重做)时,和修复一样从上一次交的这一格代码接着改(用户 2026-10-03 定):工作目录写"上一次交的这一格代码的副本",能读的版本里加上它,完整代码照旧列出作对照;另放"退回意见":最近一次核验里关错了的因子和核验员写的依据,写法同修复的退回意见。
- 指标:冻结之后一律用全名"候选编号.短名":已登记指标清单、对号清单的 `reusableMetrics`、实验员材料的"要实现的指标定义"(候选代码在结果文件里也用全名当键)。提案、回应、统一的对号清单里,已占用的指标编号只有任务的指标、`answer-path` 和 `safeguard-triggers` 两个保留键(用户 2026-10-03 定);已登记的全名带 `.`,新登记的短名不带,不用列进来。
- `finalEntries(ledger)`:按账本取的最终有效的入口(各人最后一版入口清单里的每个入口,带作者和正文);flow 分配入口时用,和认领的对号清单是同一份(任务书 12 第三部分)。
- `queryMaterial(material, id)`:只在这份材料的记录里按编号找,交回全文;找不到交回 null(agents 的查询回调据此答"看不到")。
- `readPrompts(dir)`:读 `roles/<角色>.md` 和 `tasks/<提交种类>.md`,交回文字和每个文件原始字节的 sha256;研究开始时 flow 把哈希冻结进研究配置记录(方法.md 4.1)。
- `beliefBook(ledger, study, round)`:信念本全文(方法.md 6.10 五条),数字全部用 rules 现算,只写已公开的记录;`round` 是读者所在轮次,导出时写 null。选批历史的每次运行后面原样写出任务的附加记录(判分结果的 `extra`,合法化扣掉的分在里面,字段由任务定)。消融格记"证据不足"时,有运行的原因取判分,没有运行的(核验不通过、重做次数用完)原因取这一格的 `CELL_NOT_RUN` 事件。
- `TaskMaterial`、`MaterialSection`、`MaterialRecord`、`ReadableVersion`(类型):任务材料,以及其中的一节、一条可查询记录(编号、标题、全文)、一个能读的代码版本(版本号、说明、取出目录)。
- `TaskRequest`、`StudyFacts`、`CodeChanges`、`Prompts`、`PromptFile`(类型):flow 传进来的东西,全部必填;`CodeChanges` 与 workspace 的比较结果同形(照 jev `codeHunks` 的先例,不引用 workspace)。
- 账本里只存成 Json 的内容的形状(用户 2026-10-03 决定 2:materials 定义,flow 照着写,ledger 不改):`StudyRecord`(研究配置记录:配置、步骤清单、提示词每个文件的哈希、镜像编号、任务事实、种子;配置和任务事实存 Json,flow 读回时用 `checkConfig`、`checkTaskFacts` 再检查,任务书 12 第三部分)、`FrozenProtocol`(冻结协议:方法.md 4.3 ⑥ 列的因子、保障、指标、正文、实现条款,加轮次、出发的基线、合并了哪些原提案;不带答复,主体由 submissions 的 `protocolBody` 拼,指标编号是全名)、`LocationTable`(代码记录的位置表)、`DecisionKind`、`DECISION_KINDS`、`DecisionShapes`(六种决定:入口分配、判重合并、选批、采纳、选格、押的因子,输入输出用 rules 的类型;没采纳时也写一条采纳决定)、`DedupOutput`(判重合并的输出)、`Settlement`(结算)。
- `freezeProtocol(body, candidate, round, baseline, proposals)`:flow 写冻结协议时调用。输入 submissions 交回的冻结协议主体、候选编号、轮次、基线、合并了哪些原提案,交回 `FrozenProtocol`;新登记的假设专用指标改用全名"候选编号.短名"(例如 `r2-c1.restart-gain`),引用的本来就是全名,照抄。加前缀只在这一处:同一轮的研究者互相看不到提案,会起同一个短名、定义却不同,候选编号判重合并之后才有,所以只能在冻结时加(用户 2026-10-03 定)。
- 事件:`CELL_NOT_RUN`、`cellNotRunEvent(格, 原因)`:一格消融核验不通过、重做次数用完而不跑时,flow 用它写事件记录(`objects` 只有这一格),结算记证据不足(方法.md 4.3 ⑯、6.6)。
- 编号规则:`ORIGINAL_BASELINE`、`researcherId`(第 i 位研究者,从 0 数,编号 `researcher-<i+1>`,任务书 12 第三部分)、`entryId`、`proposalId`、`critiqueId`、`candidateId`、`factorId`、`cellId`、`splitCell`、`explanationId`、`callId`,以及协议内的局部编号 `factorKey`(p1…)、`clauseKey`(c1…)。入口编号按作者一直往后编,讨论时整份替换也不复用旧编号。
### agents
下面是 `src/agents/index.ts` 导出的全部内容。agents 是程序里唯一直接用 Pi 的模块:开一个子会话,按角色发工具,把模型的查询、提交、笔记、轻量检查和每次回复的用量转给调用方的回调,等会话结束交回结局。它不碰账本,不决定给模型看什么,也不判断交上来的东西好不好。Pi 的设置不读用户和工作目录里的 settings.json,用到的几项(重试、压缩上下文、HTTP 超时)写在 `session.ts` 里;模型服务配置照常用本机 Pi 的 models.json 和 auth.json。
- `runSession(task)`:跑一次会话,交回结局。建好会话后先核对实际装了什么(模型和思考档位没被换、没有扩展、技能、提示模板和上下文文件、系统提示词只多了 Pi 自己加的工作目录一行、工具表正好是该给的那几个且是给的实现),不通过就一个请求都不发,交回出错;回调抛错按调用方的内部错误处理,会话立即停下,错误原样抛出。
- `SessionTask`(类型):跑一次会话的输入,按角色区分,全部必填:模型、角色提示词和第一条消息(materials 拼好的文字)、工作目录、会话文件目录、提交种类、查询、提交、用量三个回调和"停下"信号;研究者另有能读的目录和写笔记,核验员另有能读的目录,实验员另有轻量检查;研究者交上一份被收下后会话不结束,实验员、核验员交上就结束。
- `RoleModel`、`ThinkingLevel`(类型):provider、模型名和思考档位,档位必须写明,名单与 config 的 `THINKING_LEVELS` 相同。
- `KindOf<R>`(类型):某个角色交的提交种类,取自 vocab 的 `SUBMISSION_KINDS`。
- `SubmitAttempt<K>`(类型):交给提交回调的一次提交:形状合格的内容和"替换哪一份"(只有研究者的提交能替换),或形状不合格的原样参数和原因(参数原文不是合法 JSON、必填字段交了 null、Pi 的形状校验不通过),后者回调只需记一条退回。
- `SubmitReceipt`(类型):提交回调的回答,收下和编号,或退回和原因,agents 原样回给模型;收下时另回显收到的字数和结尾。
- `SessionEnding`、`SessionOutcome`(类型):交了、没交(附原因,例如最后一条回复被输出上限截断)、出错(附原因)、被停下四种,都带墙钟时间(秒)和会话文件路径(Pi 没写出会话文件时为 null)。
### jev
下面是 `src/jev/index.ts` 导出的全部内容。九问的问法和阈值照参考项目,只改了说法和字段名;阈值没有在新项目的材料上校准过,等真实研究的数据(任务书 08 第三部分)。模型名由调用方从 config 的 `models.jev` 传入,密钥从环境变量 `TYPESAFE_API_KEY` 读进请求头。调不通时在发请求那一处有限次重试,仍不通就报错;回答读不懂报错并带请求编号。
- `judgeSameChange(model, a, b)`:③ 判重,两条提案的因子清单是不是同一个改法,一对一次请求,交回一条 `JevJudgment`。
- `checkProtocol(model, protocol)`:⑥ 查冻结条款,三问(改动了评测、自相矛盾、太含糊)各一次请求,交回 `JevCheck`。
- `checkHunk(model, hunk, protocol)`:⑨ 查一段代码改动,五问(不固定种子的随机数、没登记就读时钟、算登记指标的方式与定义不符、比登记的停止条件更早结束、测不到的量写成 0)同一份材料一次请求,交回 `JevCheck`。对照的指标取自冻结协议,编号是全名。
- `codeHunks(comparison)`:把 workspace 的比较结果(文件清单和差异原文)切成差异块;没有 `@@` 行的改动(二进制、只改可执行位、空文件增删)不出块,由程序检查。
- `DedupFactor`、`FrozenProtocol`、`ProtocolMetric`、`ProtocolRules`、`CodeHunk`(类型):三个函数收的带类型输入,取自 vocab 的字段定义;材料由 jev 自己拼。`DedupFactor` 是因子的名称、做法和关键设定:问题写着"设定不同就不算同一个改法",所以关键设定要给 Jev 看,判重的材料说明里也用一句话讲了 `keySettings` 是什么。
- `JevCheck`(类型):每一问的判断,以及有没有任何一问判"是"。
### submissions
下面是 `src/submissions/index.ts` 导出的全部内容。submissions 判一份提交收下还是退回:先做程序检查(vocab 字段定义管不到的跨字段规则,问题一次列全),有一项不过就退回、不问 Jev;全过了再问 Jev,几问问完、有一问判"是"就退回。它不读也不写账本,只依赖 jev 和 vocab;要 workspace、tasks、materials 算的东西由 flow 算好传进来。Jev 调不通、回答读不懂时 jev 报错,这里不接住,flow 按内部错误停下。
- `receive(incoming)`:判一份提交,交回 `Verdict`。回应、统一先拼好冻结协议主体,程序检查、问 Jev、交回都用这同一份;回应、统一问冻结条款三问,实现、修复对每个差异块问代码改动五问,其余种类不问 Jev。Jev 退回的原因只列判"是"的几问,用 jev 的英文问题名(用户 2026-10-03 定),差异块附原文。
- `protocolBody(from)`:拼冻结协议主体,只在这一处拼。回应:因子、保障、指标、正文四项,`changes` 写了哪项就整项换新,没写的用原提案的,实现条款取回应自己的;统一:直接用统一提交自己的字段。答复不进协议(用户 2026-10-03 定)。
- `verificationOutcome(v)`:一份已收下的核验通过没有:任何条款、因子、保障没落实,或任何没有归属的改动判为越界,就不通过;四类分开列(方法.md 4.3 ⑨)。
- `ablationOutcome(v, off, alsoOff)`:一份已收下的消融核验通过没有:要关的因子和实验员报的一起关的因子都写了"关着"、其余都写了"照留"才通过;分开列该关却照留的和该留却关了的(方法.md 4.3 ⑯)。
- `Incoming`(类型):交给 `receive` 的一份提交,按提交种类区分:内容、这次任务的对号清单、提交之后才知道的东西(`KnownOf`)、Jev 的模型名。
- `KnownOf<K>`(类型):提交之后才知道的东西,没有的种类为 null:提案是本人本阶段已收下几条,回应是作者那条原提案,实现和修复是 `ForwardCode`,消融实现是 `AblationCode`(交了"跑不起来"时为 null)。
- `ForwardCode`(类型):实现、修复的代码:存不成版本时带路径和原因;存成了带改了的不许改的文件、和本批基线比出的差异块、给 Jev 对照的协议条款。
- `AblationCode`(类型):消融实现的代码:存不成版本时带路径和原因;存成了只带改了的不许改的文件。
- `OriginalProposal`(类型):回应拼协议时叠在下面的原提案:因子、保障、指标、正文,作者自评不进协议。
- `ProtocolBody`(类型):冻结协议的主体,即方法.md 4.3 ⑥ 列的因子、保障、指标、正文、实现条款;flow 用 materials 的 `freezeProtocol` 补上轮次、基线、合并了哪些原提案,写成 `FrozenProtocol`。
- `Verdict`(类型):判的结论:收下(带用到的全部 Jev 判断,没问 Jev 为 null;回应、统一另带拼好的协议主体,其余为 null),或退回(带原因和用到的 Jev 判断)。
### flow
下面是 `src/flow/index.ts` 导出的全部内容,以后 plugin 也只用它。flow 只经各模块的 `index.ts` 用它们;正式代码里没有为检验写的参数、环境变量、回放模式或假实现(CLAUDE.md"测试")。这一节对应 flow 第 1 部分(任务书 12):骨架、研究开始、第零轮,走到"开第 1 轮"停下。
- `runStudy(input)`:推进一份研究,直到停下。输入三样,都必填:研究目录;配置(新研究交 `checkConfig` 检查过的配置,接着跑一份已有的研究写 null,以账本里冻结的那份为准;两者对不上就报错);"停下"信号。每次启动走同一条路:打开账本;新研究先冻结配置,本机已有 `inforsi-<研究编号>` 的镜像就说明研究编号用过了,报错停下,要换一个编号(编号重复的新研究会把旧研究遗留的同名容器当成自己的运行);已有研究的,核对冻结的配置、镜像还在、提示词哈希没变(开发入口开着时提示词可以变);给开始了没结束的模型调用补写结束(出错、"进程中断"、墙钟时间测不到);按容器名问 Docker 核对登记了没结果的正式运行(没启动的写"没启动"退还名额,启动了的补写确认启动、等它、判分、写结果,问不到的写"状态未知"事件并停下);然后进主循环:读账本 → 问"下一步做什么" → 做这一步 → 回到开头。主循环里 `docker run` 没启动的,写完"没启动"结果(退还名额、记下 Docker 报错原文)就停下,停下原因带上报错原文,本进程不重跑;下次启动时照旧起新编号重跑。恢复时补写的"没启动"不停下。
- `StudyInput`(类型):上面三样输入。
- `StudyEnding`(类型):两种结局之一:停下(附原因,同时写一条停下事件),或被用户停下(正在跑的模型会话立即停,正在跑的容器不管,下次启动由恢复入口接着等)。"研究结束"第 4 部分才有。
研究目录里放:`ledger.sqlite` 账本;`repo.git` 代码版本的裸仓库;`runs/<运行编号>/` 一次正式运行(取出的代码、运行目录、日志、判分目录、校验器日志);`calls/<调用编号>/` 一次模型调用(工作目录、会话文件、能读的代码版本)。取出代码、建运行目录和判分目录时目标都是新建的空目录。容器名 `inforsi-<研究编号>-<运行编号>`,判分容器另加 `-judge`。同一份研究的全部运行用同一个种子:研究编号经 vocab 的 `hash` 取前 8 位十六进制转成整数,冻结进研究配置记录。
**每一步以哪条记录为准算做完**(任务书 12 第 2.2 节)。步骤的先后只写在 flow 的"下一步做什么"一个纯函数里;后面三部分每加一步,这张表加一行。
| 步骤(config 的 `STUDY_STEPS`) | 看到哪条记录算做完 |
|---|---|
| `freezeConfig` 冻结配置 | 研究配置记录(`config`) |
| `baselineRun` 基线 | 基线代码记录(`code`,对象是 materials 的 `ORIGINAL_BASELINE`),而且最近一次基线运行的结果不是"没启动";"没启动"的下次启动时起新编号(`base-2`……)重跑,判为不算数的停下 |
| `entryDraft`、`discussion1`、`discussion2`、`claim` | 这个阶段的关闭记录(`stageClose`);阶段编号 `r0-<步骤>`,只在名单上的人都有被收下的提交之后才关 |
| `assignEntries` 分配 | 第 0 轮的入口分配决定(`decision`,种类 `entries`) |
| 按配置跳过的步骤 | 一条"按配置跳过"事件(`skippedByConfig`),写明步骤和 config 给的原因 |
研究者阶段里没交上的人,按他在这个阶段最近一次调用怎么结束的来定:没有调用、被用户停下、出错(含"进程中断")都派;没交的,开发入口关着就停下(交不出合格提交按内部错误,方法.md 4.3 ②),开着就再派。一次派发里有人没交上,等这次派发的会话全部结束后停下。
检验命令(`eval/`,真实运行,要 Docker;带 `*` 的会花模型或 Jev 的钱。输出都在仓库外 `/home/iomgaa/inforsi-eval/<日期-名字>/`,名字也当研究编号用,只能小写、不能重复):
| 命令 | 用途 |
|---|---|
| `npm run eval:study -- --config <文件> --dir <目录>`、`-- --resume --dir <目录>` * | 起一份研究或接着跑,Ctrl-C 发停下信号 |
| `npm run eval:r0:run -- <名字>` * | 用 `eval/round-zero/research.json` 真实跑一次第零轮 |
| `npm run eval:r0:quality -- <检验目录>` * | 对第零轮产出问六问(任务书 12 第 5.2 节),原始概率写进 `<检验目录>/quality.json` |
| `npm run eval:r0:extract` | 从参考项目 16-real-1 的账本重新提取第零轮提交,生成回放数据 |
| `npm run eval:r0:replay -- <名字>` | 回放 16-real-1 的第零轮,比对分配结果 |
| `npm run eval:r0:kill -- baseline\|drafting\|stageFull` | 在三处之一杀控制进程、重启跑完,逐条检查(用回放,不花钱) |
| `npm run eval:r0:concurrent` | 两份研究同时跑到基线结束,检查各扣各的、容器不撞名 |