diff --git a/WORK_LOG.md b/WORK_LOG.md index 1f52d01..33986ff 100644 --- a/WORK_LOG.md +++ b/WORK_LOG.md @@ -20,6 +20,7 @@ - 任务书 07(agents):src/agents 4 个文件、681 行(预期按用户决定改为 600–700,超出部分是任务书第四部分要保留的三处提交处理、读代码分段和会话核对,没有新增专门路径),`npm test` 293 条通过(agents 9 条,其中 1 条经真实 Pi 会话连本机假模型服务)。package.json 固定 `@earendil-works/pi-coding-agent`、`pi-ai` 0.85.1 并加 overrides(同参考项目);新增 `npm run test:pi`(真实 glm-5.3,研究者先退回再收下、实验员写文件跑 shell 再交)。实测:glm-5.3 经 iomgaa 服务时,正文里没转义的半角双引号会在服务那一侧把参数截成合法但半截的 JSON,Pi 收到的原文本身合法,JSON 检查查不出(抓包确认);提交工具说明里加了一句"引号用「」",之后 12 份提交没再出现。结构.md 第七、八部分已更新。 - 任务书 08(jev):src/jev 3 个文件、515 行,九问的问法和阈值照参考项目,只改说法和字段名,阈值注明未在新材料上校准;判重一对一次请求、冻结条款三问各一次、代码改动一块一次(五问同材料);`npm run test:jev` 三处各问一次真实 Jev,九问都拿回 0 到 1 的概率和请求编号。vocab 加 `JevJudgment`。ledger 表结构版本升到 3:`callEnded` 四种结局对上 agents、墙钟时间用 `Measured`(中断恢复时补写出错、原因"进程中断"、时间测不到);退回和提交记录都带 `jev`(判断数组或 null)。config 加必填的 `models.jev`。`npm test` 316 条通过。账本改动全新上下文审阅两轮。 - 任务书 09(materials):src/materials 12 个文件、1831 行(预期 1200–1800;超出部分主要是用户决定 2 新加的 shapes.ts 116 行,复审判定没有专门路径增多),vocab 加 `Checklist`(1105 行,上限 1150)。19 份提示词以参考项目为底稿只做必要改动搬进 `prompts/`;TASK.md 补了 `unversionedNames` 不存进版本。`npm test` 356 条通过(materials 39 条)。全新上下文审阅三轮(可见性一轮、规模与正确性一轮、修复复审一轮),materials 内部没有发现泄露;要靠 flow 写入时机保证的几条记进"未完成"的"给 flow"。 +- 任务书 10(submissions):先改 materials 第一部分 7 处,另加两处小改动(jev 判重材料带关键设定、核验 `body` 字段说明补同行提醒)。src/submissions 6 个文件、391 行(预期 400–600);materials 1850 行;vocab 1107 行;ledger 退回记录加对象字段,表结构版本升到 4。`npm test` 400 条通过;`npm run test:jev` 新增三个真实用例:写得好的回应收下(三问 0.06 / 0.04 / 0.04),改评测的回应退回(changes_measurement 0.91,阈值 0.21),实现 3 块 15 条判断全否。全新上下文审阅一轮:采纳消融实现退回文字前后矛盾、两处测试加强;另发现同轮指标重名,用户定的改法见"未完成"。结构.md 第七、八部分已更新。 ## 关键决定(用户原话见各提交说明) @@ -50,47 +51,29 @@ - 中断的模型调用,恢复时补写"出错、原因'进程中断'、墙钟时间测不到"(`callEnded` 的墙钟时间用 `Measured`)。用户原话:"墙钟时间改用 Measured,恢复时写出错加测不到(原因'进程中断')"。 - materials(任务书 09)开工前用户定的四件事(2026-10-03,用户选了推荐项):vocab 上限改 1150,只放对号清单 `Checklist`,任务材料其余类型放 materials;冻结协议、位置表、判重合并输出、决定种类和编号规则由 materials 定义并导出,flow 照着写,ledger 不改;实验员、核验员不给信念本,方法.md 4.3 ① 改为"研究者任务";合并而成的候选按原提案挑刺。 - 核验不通过、修复次数用完时:正向候选不跑(不扣次数,记事件写明原因);消融格和正向一样可以退回实验员重做,次数用同一个修复上限,用完仍不通过这一格不跑、不扣次数、记"证据不足"(方法.md 4.3 ⑨ ⑯、6.6)。用户原话:"同意"。 +- 冻结协议只放方法.md 4.3 ⑥ 列的几项(2026-10-03)。用户原话:"冻结协议只放方法.md 4.3 ⑥ 列的几项,FrozenProtocol 去掉 replies 和 replyBody;第二次预测要的"挑刺与答复"从这个候选已公开的回应或统一提交里读;submissions 交回的冻结协议主体也不带答复。" +- 解释的 `cells` 只按完整流程全开实现,一律要求交(用户选"只按全开实现");做步骤开关时再补"关掉消融时不交"。 +- 提案、回应、统一的已占用指标编号加 `answer-path`、`safeguard-triggers` 两个保留键(用户选"materials 补进清单")。 +- Jev 退回的原因只列判"是"的几问,只用 jev 的英文问题名(用户选"只列判是的+只写英文")。 ## 未完成 -- 任务书 09 核对后要改的 materials,并入任务书 10 先做(监督者核对 446e262 时发现,用户同意): - 1. 挑刺的"现在交什么"落进了研究者的通用一句"调用一次交上就完成了",和任务说明"可以交一份或几份"相反;给挑刺单写一句。 - 2. 退回记录加"对象"字段(同提交记录的 target);materials 按阶段、作者、种类、对象找上一次的退回,删掉按 `callId` 前缀定位的写法和"同一任务不并行"的要求。 - 3. 核验材料里删掉 Jev 预扫的概率和阈值:方法里没有这一项,能到核验说明 Jev 已判过没问题,再给核验员看等于把 Jev 当提示用。 - 4. 信念本选批历史补"合法化扣掉的分"(方法.md 6.10 第 3 条)。 - 5. 删掉"答案出自被关掉的因子,它本该关着"那句:方法里没有,也没有真实证据。 - 6. 消融核验不通过可以重做(用户 2026-10-03 定):重发消融实现时材料里带核验意见;一格不跑而记"证据不足"时,信念本不能再去读它的运行(现在的写法假定证据不足的格一定有运行),rules 里 `insufficient` 的注释跟着改。 - 7. 顺手改三处旧说法:`tasks/circle_packing/TASK.md`"保障写'无'时写 {}";`src/vocab/submissions.ts` 开头注释的 q₁ ≤ q₅ ≤ q₉;读代码工具的 `offset` 参数(提示词里已删)。 - 信念本"按因子归并"(方法.md 6.10 第 1 条"出现在哪些候选里"):现在是每个候选的每个因子一行,用"(延续 X)"连起来,没合成一行。等真实运行后再看要不要合。 -- jev 判重材料加上每个因子的"关键设定"(现在只有名称和做法,而问题写着"设定不同就不算同一个改法"),并入下一份任务书。用户原话:"我同意加上"。 - Jev 校准:新项目跑出真实研究后,从账本取真实的 Jev 判断和材料,由 Claude 或 GPT 打标签作为校准集和留出集,重新校准九问的问法和阈值。不用参考项目 8 个验收数据库(材料格式还会变)。 - `inbox/调研.md`:要迁进 `docs/调研/` 的调研结论。 - `docs/方法.md` 里还有 7 条标〔自定〕的规则,待用户过目。 -- 留给 submissions 的跨字段规则(vocab 字段定义不管): - 1. q1 < q5 < q9(严格递增,方法.md 第五部分,提交 bfb96ba):作者自评、两次预测、解释的每一格。 - 2. 因子数不超过上限,回应改过的因子清单同样适用。 - 3. 同一提案里两个因子不能延续同一个因子;延续的编号要在信念本里看得到。 - 4. 保障短名不能是 `main`,不能写成 `p<数字>`,同一清单里不重复。 - 5. 开着消融时每条解释必须交 `cells`,并恰好覆盖材料列出的格;关掉消融时不交。 - 6. 解释最多 M 条。 - 7. 志愿至少 min(N, 入口总数) 个、不重复、编号都在入口清单里。 - 8. 挑刺针对的因子编号在被挑提案的因子范围内。 - 9. 回应的逐条答复恰好覆盖针对本候选的全部挑刺;统一的 `sources` 恰好覆盖被合并的原提案。 - 10. `reused` 引用的指标存在且是假设专用指标;新登记的指标短名不重复。 - 11. 实现和修复:位置表覆盖每个因子和每条保障;代码位置起始行不大于结束行。 - 12. 核验:条款、因子、保障逐项全覆盖;没有归属的改动与材料列出的逐条对应。 - 13. 消融实现:写了 `cannotRun` 时 `alsoOff` 为空;`alsoOff` 不含本格要关的因子,且都在因子清单里。 - 14. 消融核验覆盖每个因子。 -- 核验提交 `body` 的字段说明要补一句:同一行同时出现在因子和保障里时,在说明里写一句提醒(方法.md 4.3 ⑨)。写 submissions 时一并改。 - 给 flow:同一轮里选批和选格都用随机规则时,传不同的"版本"算种子,否则两次抽取用同一串随机数。 - 给 flow:确认启动前要先登记运行;模型调用要先开始再结束;阶段要先开再关;ledger 都不查。 - 给 flow:步骤清单没有单列"轮末动作"(解封作者自评、入选未跑的候选出池),flow 要做这些。 - 写 plugin 时实测:源码导入路径带 `.ts` 扩展名,Pi 加载插件能否接受。 -- 给 submissions:研究者登记的假设专用指标编号不能和常规辅助指标重名(`metricDefinitions` 里的 7 个),否则候选自报的值会顶替宿主测的值(任务书 05 审阅意见)。 - 给 flow:容器名要在本机所有研究之间唯一(例如带研究编号);`docker run` 因重名失败时 runner 按"启动了"处理。日志目录每次运行一个新的。 - 给 flow:取出时目标目录必须是空的,workspace 不查。 - 给 flow(materials 审阅):冻结协议只在回应阶段关闭后写进账本,阶段内给 Jev 查的协议只放内存;解封只在本轮收尾阶段关闭后写;阶段关闭只在名单上的人交齐后写。materials 把这几条记录的出现当作"已公开",提前写会泄露(审阅者在临时账本里实测过)。 - 给 flow:没采纳时也写一条采纳决定(adopted 为 null),本轮正向运行靠它公开;修复任务要在核验阶段关闭后再发。 +- 同一轮两位研究者可能给假设专用指标起同一个编号、定义不同:两份都会收下,下一轮引用时取第一条定义。审阅时在参考项目真实账本里查到 4 次:11-real-1 第 1 轮 min-radius、15-real-1 第 1 轮 lp-polish-gain、15-tight-2 第 2 轮 final-radius-spread 与 last-improve-elapsed、16-real-1 第 2 轮 restart-gain。用户选的改法:"冻结时加候选前缀":冻结协议里指标全名为"候选编号.短名",可引用和已占用清单都用全名,恢复"短名不能含 ."。涉及 vocab、materials、submissions、tasks 读指标文件,待写进下一份任务书。 +- 给 flow:消融格核验不通过、修复次数用完时写 `cellNotRunEvent(格, 原因)` 事件,结算记 insufficient。消融重做时实验员从哪份代码出发(完整代码副本,还是上一次这一格的提交),方法没写;flow 取代码时要和 materials 的写法对齐。 +- 给 flow:修复的差异块按本批基线算(任务书 10 第二部分第 2 步),切好交给 submissions;形状不合格的提交由 agents 报给提交回调,flow 直接记退回,不经 submissions。 +- 给 flow:消融实现交了 `cannotRun` 时 `known.code` 传 null,否则传存版本的结果。 - 给以后换任务镜像:镜像里必须有 `setpriv`(runner 用它把命令降到当前用户);容器里不要用 `python -I`,它会忽略 `PYTHON_CPU_COUNT`。 ## 工作方式(用户 2026-10-01 定) @@ -104,4 +87,4 @@ ## 下一步 -任务书 10(submissions,`任务书/10-submissions.md`,开头先改 materials 的 7 处)已写好,等用户另开新 Agent 执行;完成后监督者核对,再做 flow。 +任务书 10(submissions)已完成,等监督者核对;之后是指标加候选前缀那件事,再做 flow。 diff --git a/docs/结构.md b/docs/结构.md index 4319a86..cca0cfd 100644 --- a/docs/结构.md +++ b/docs/结构.md @@ -123,8 +123,8 @@ |---|---|---|---| | 1 | 研究配置 | 冻结的配置、步骤清单、每个决策点用哪条规则、方法参数、提示词哈希 | flow(内容由 config 生成) | | 2 | 阶段 | 每个阶段开一条、关一条;关闭即公开 | flow | -| 3 | 提交 | 各种提交的每一版:入口、讨论、志愿、提案、两次预测、挑刺、回应、解释、收尾、实现、核验:阶段关闭后以最后一版为准,此后由 ledger 拒绝再写。私人笔记的每一条:不属于任何阶段,以最后写入的一条为准。每一版带收下时用到的 Jev 判断(含原始概率和结论,没用到 Jev 的为空)。被退回的提交也记,附退回理由和用到的 Jev 判断 | flow(经 submissions 判定后) | -| 4 | 冻结协议 | 每个候选的冻结版本:因子清单、保障清单、实现条款、指标登记 | 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 给出) | @@ -187,7 +187,7 @@ Jev 是一个判别器,和程序检查地位相同,不单独成一种记录 | flow | 2000–3500 | | ledger | 500–900 | | rules | 1500–2500 | -| submissions | 600–1000 | +| submissions | 400–600 | | jev | 400–600 | | materials | 1200–1850(多出的部分是用户 2026-10-03 让 materials 定义的账本内容形状) | | agents | 600–700 | @@ -397,7 +397,7 @@ Jev 是一个判别器,和程序检查地位相同,不单独成一种记录 **打开与写入** -- `Ledger.open(path)`:打开一份研究的账本,新文件就建表,旧文件核对表结构版本号,对不上就报错。 +- `Ledger.open(path)`:打开一份研究的账本,新文件就建表,旧文件核对表结构版本号(现为 4),对不上就报错。 - `ledger.close()`:关闭数据库。 - `ledger.append(kind, content)`:追加一条记录并返回它。先按种类检查内容,再做第四部分说的四种拒收检查(第 1 条是只收开着的阶段的提交,即有开阶段记录、没有关阶段记录),都在同一个事务里。有自己编号、按定义只发生一次的记录按编号加了唯一约束,重复写就报错:阶段开、阶段关、运行登记、确认启动("结果是没启动"也算在这一条里,一次运行两样只能有一样)、运行结果、模型调用开始、模型调用结束、消融结算(按轮次,一轮一条);其余记录照写。记录之间的先后(例如结果前要先有登记)不查。写提案时,作者自评从提案里拆出来,在同一个事务里另存成一条封存。 - `ledger.transaction(fn)`:里面的几次追加要么全部写进去,要么一条都不写;fn 必须是同步函数,返回值类型排除了 Promise,异步函数在类型检查时就被挡住;不能嵌套。 @@ -412,7 +412,7 @@ Jev 是一个判别器,和程序检查地位相同,不单独成一种记录 **类型** -- `Kind`:记录的种类,共 19 种:`config`(研究配置)、`stageOpen` / `stageClose`(阶段开、关)、`submission`(提交的每一版,外层带收下时用到的 `JevJudgment` 数组,没用到 Jev 的为 null)、`rejection`(退回,带原样内容和理由;Jev 退回的,原样存这次用到的一条或几条 `JevJudgment`,程序检查退回的这一项写 null)、`note`(私人笔记,每次修改追加一条,以最后写入的一条为准,不带版本号)、`protocol`(冻结协议)、`seal` / `unseal`(作者自评的封存、解封,都用记录序号指明是哪一条)、`code`(冻结代码)、`runPlanned` / `runStarted` / `runResult`(运行的登记要跑、确认启动、结果)、`decision`(决定)、`settlement`(一轮的消融结算,按轮次唯一)、`callStarted` / `callUsage` / `callEnded`(模型调用的开始,附这次任务的全部材料;每次回复的用量;结束,带墙钟时间(秒,`Measured`:中断后恢复时补写的结束记录,结局记出错、原因写"进程中断",墙钟时间记测不到),结局与 agents 的会话结局同名:交了、没交(带原因)、出错(带原因)、被停下)、`event`(事件)。 +- `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`:某种记录存下的内容。 - `Input`:追加时交的内容;只有提交和存下的不同:提案带着作者自评。 @@ -503,13 +503,18 @@ Jev 是一个判别器,和程序检查地位相同,不单独成一种记录 下面是 `src/materials/index.ts` 导出的全部内容。谁能看什么每种任务一个函数,对着方法.md 第三部分、4.2、4.3 写,不写通用判定(用户 2026-10-02)。materials 只读账本,不写;任务材料由 flow 整份存进"模型调用开始"记录。依赖只有 ledger、rules、vocab;代码比较结果、运行产物、提示词由 flow 传进来。 -- `buildMaterial(req)`:生成一份任务材料:给模型看的各节、可查询的记录、拼好的第一条消息、对号清单、能读的代码版本。研究者的材料按"任务说明 → 你自己的 → 公共材料 → 现在交什么"排;实验员、核验员的按"任务说明 → 分给你的材料 → 现在交什么"排,不给信念本和成绩。同一人在同一任务里重新发起时,最前面放上一次被退回的原因。"现在交什么"由 vocab `SUBMISSION_SCHEMAS` 的字段说明生成。 +- `buildMaterial(req)`:生成一份任务材料:给模型看的各节、可查询的记录、拼好的第一条消息、对号清单、能读的代码版本。研究者的材料按"任务说明 → 你自己的 → 公共材料 → 现在交什么"排;实验员、核验员的按"任务说明 → 分给你的材料 → 现在交什么"排,不给信念本和成绩。同一人重新发起同一任务时,最前面放上一次被退回的原因:按阶段、作者、提交种类、对象找本人最后一条退回记录。"现在交什么"由 vocab `SUBMISSION_SCHEMAS` 的字段说明生成。几种任务的取法: + - 第二次预测:冻结协议不带答复,"挑刺与答复"从这个候选已公开的最后一份回应或统一里读(回应另给答复正文);留池候选读它冻结那一轮的(用户 2026-10-03 定)。 + - 核验:不放 Jev 对这份实现的判断。能走到核验说明 Jev 已判过,Jev 是判别条件,不是给核验员的提示。 + - 消融实现:这一格已有公开的消融核验(核验不通过、重做)时,另放"退回意见":最近一次核验里关错了的因子和核验员写的依据,写法同修复的退回意见。 + - 提案、回应、统一的对号清单里,已占用的指标编号含任务的指标、`answer-path` 和 `safeguard-triggers` 两个保留键、已登记的假设专用指标(用户 2026-10-03 定)。 - `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。 +- `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 不改):`FrozenProtocol`(冻结协议)、`LocationTable`(代码记录的位置表)、`DecisionKind`、`DECISION_KINDS`、`DecisionShapes`(六种决定:入口分配、判重合并、选批、采纳、选格、押的因子,输入输出用 rules 的类型;没采纳时也写一条采纳决定)、`DedupOutput`(判重合并的输出)、`Settlement`(结算)。 +- 账本里只存成 Json 的内容的形状(用户 2026-10-03 决定 2:materials 定义,flow 照着写,ledger 不改):`FrozenProtocol`(冻结协议:方法.md 4.3 ⑥ 列的因子、保障、指标、正文、实现条款,加轮次、出发的基线、合并了哪些原提案;不带答复,主体由 submissions 的 `protocolBody` 拼)、`LocationTable`(代码记录的位置表)、`DecisionKind`、`DECISION_KINDS`、`DecisionShapes`(六种决定:入口分配、判重合并、选批、采纳、选格、押的因子,输入输出用 rules 的类型;没采纳时也写一条采纳决定)、`DedupOutput`(判重合并的输出)、`Settlement`(结算)。 +- 事件:`CELL_NOT_RUN`、`cellNotRunEvent(格, 原因)`:一格消融核验不通过、重做次数用完而不跑时,flow 用它写事件记录(`objects` 只有这一格),结算记证据不足(方法.md 4.3 ⑯、6.6)。 - 编号规则:`ORIGINAL_BASELINE`、`entryId`、`proposalId`、`critiqueId`、`candidateId`、`factorId`、`cellId`、`splitCell`、`explanationId`、`callId`,以及协议内的局部编号 `factorKey`(p1…)、`clauseKey`(c1…)。入口编号按作者一直往后编,讨论时整份替换也不复用旧编号。 ### agents @@ -532,5 +537,21 @@ Jev 是一个判别器,和程序检查地位相同,不单独成一种记录 - `checkProtocol(model, protocol)`:⑥ 查冻结条款,三问(改动了评测、自相矛盾、太含糊)各一次请求,交回 `JevCheck`。 - `checkHunk(model, hunk, protocol)`:⑨ 查一段代码改动,五问(不固定种子的随机数、没登记就读时钟、算登记指标的方式与定义不符、比登记的停止条件更早结束、测不到的量写成 0)同一份材料一次请求,交回 `JevCheck`。 - `codeHunks(comparison)`:把 workspace 的比较结果(文件清单和差异原文)切成差异块;没有 `@@` 行的改动(二进制、只改可执行位、空文件增删)不出块,由程序检查。 -- `DedupFactor`、`FrozenProtocol`、`ProtocolMetric`、`ProtocolRules`、`CodeHunk`(类型):三个函数收的带类型输入,取自 vocab 的字段定义;材料由 jev 自己拼。 +- `DedupFactor`、`FrozenProtocol`、`ProtocolMetric`、`ProtocolRules`、`CodeHunk`(类型):三个函数收的带类型输入,取自 vocab 的字段定义;材料由 jev 自己拼。`DedupFactor` 是因子的名称、做法和关键设定:问题写着"设定不同就不算同一个改法",所以关键设定要给 Jev 看。 - `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`(类型):提交之后才知道的东西,没有的种类为 null:提案是本人本阶段已收下几条,回应是作者那条原提案,实现和修复是 `ForwardCode`,消融实现是 `AblationCode`(交了"跑不起来"时为 null)。 +- `ForwardCode`(类型):实现、修复的代码:存不成版本时带路径和原因;存成了带改了的不许改的文件、和本批基线比出的差异块、给 Jev 对照的协议条款。 +- `AblationCode`(类型):消融实现的代码:存不成版本时带路径和原因;存成了只带改了的不许改的文件。 +- `OriginalProposal`(类型):回应拼协议时叠在下面的原提案:因子、保障、指标、正文,作者自评不进协议。 +- `ProtocolBody`(类型):冻结协议的主体,即方法.md 4.3 ⑥ 列的因子、保障、指标、正文、实现条款;flow 补上轮次、基线、合并了哪些原提案,写成 materials 的 `FrozenProtocol`。 +- `Verdict`(类型):判的结论:收下(带用到的全部 Jev 判断,没问 Jev 为 null;回应、统一另带拼好的协议主体,其余为 null),或退回(带原因和用到的 Jev 判断)。 diff --git a/prompts/tasks/ablationImplementation.md b/prompts/tasks/ablationImplementation.md index f231b1b..dc1372f 100644 --- a/prompts/tasks/ablationImplementation.md +++ b/prompts/tasks/ablationImplementation.md @@ -22,6 +22,8 @@ 材料里没有任何成绩和研究者的判断,这是有意的——你照因子清单做就行,不用知道哪个因子被认为更重要。不要自己跑正式实验去看关掉以后的分数(系统提示词"两条纪律");想确认能被正式实验接上,用 `research_check`。 +材料"本任务要处理的东西"最前面有"退回意见"时,说明上一次交的这一格没通过消融核验:意见列出了关错的因子和核验员看到的位置。照意见把它们改对,仍然从完整代码出发、只关这一格要关的因子;核验员看错了的,在 `concerns` 里写清实际在哪几行。 + 交之前用 `diff -ru <完整代码冻结目录> .` 看一遍:改动应该只落在这个因子(和你写进一起关的因子)的位置上,加上把这个因子的指标改写成 null 的那几行。 关掉这个因子以后,你读代码看出某处会出问题——最常见的是:没有了这个因子,某条保障从"很少用到"变成了"每次都用上"(比如兜底分支每次都交答案)——写进 `concerns`,说清是哪条保障、为什么。这一格量到的可能就是那条保障,研究者需要在读结果时知道。照常只关指定的因子,不因为担心就改别处。没有就写"无"。 diff --git a/prompts/tasks/response.md b/prompts/tasks/response.md index 534c0b6..8fbee67 100644 --- a/prompts/tasks/response.md +++ b/prompts/tasks/response.md @@ -35,4 +35,4 @@ ## 交什么 -逐条答复(材料"本任务要处理的东西"末尾"本任务要答复的挑刺"列出了针对这个候选的每一份挑刺,每份一条答复,`critique` 照那里的挑刺编号逐字写;研究记录里别的评议提交针对的是别的提案,不用答复)、实现条款(`clauses`,以编号 c1、c2……称呼)、需要时的改动(`changes`)、答复正文(`body`:逐条答复之外要补充的话,没有就写"无")。程序把改动叠到原提案上,得到冻结协议;答复原样跟着冻结协议,给之后预测的人对照着挑刺原文读。 +逐条答复(材料"本任务要处理的东西"末尾"本任务要答复的挑刺"列出了针对这个候选的每一份挑刺,每份一条答复,`critique` 照那里的挑刺编号逐字写;研究记录里别的评议提交针对的是别的提案,不用答复)、实现条款(`clauses`,以编号 c1、c2……称呼)、需要时的改动(`changes`)、答复正文(`body`:逐条答复之外要补充的话,没有就写"无")。程序把改动叠到原提案上,得到冻结协议;答复不进冻结协议,原样和它一起给之后预测的人,对照着挑刺原文读。 diff --git a/prompts/tasks/verification.md b/prompts/tasks/verification.md index f5a3f07..109383d 100644 --- a/prompts/tasks/verification.md +++ b/prompts/tasks/verification.md @@ -2,7 +2,7 @@ 分派给你的是一次还没跑的运行:一个候选的冻结协议,和实验员照它写出的代码。你核过以后,程序从你的逐项结果推出通过与否:通过,这次运行才会被放去跑正式实验;不通过,你写的位置和依据原样交给实验员修改。 -材料给了你:冻结协议全文;实验员交的因子位置表和保障位置表;这份代码相对起点代码(本批基线)的改动全文;两份代码的冻结目录(可以用 `read_source` 读);实验员提交时判别模型逐段预扫的结果。只看这份实现新引入的改动,起点代码里原有的写法不算它的问题。 +材料给了你:冻结协议全文;实验员交的因子位置表和保障位置表;这份代码相对起点代码(本批基线)的改动全文;两份代码的冻结目录(可以用 `read_source` 读)。只看这份实现新引入的改动,起点代码里原有的写法不算它的问题。 ### 逐项核 @@ -12,7 +12,7 @@ - **因子**:对着因子位置表,看那几行是不是照这个因子的做法写的;因子的设定值是不是协议写的那个。位置表只是线索,因子的代码不在表上写的地方而在别处,照实写在哪。 - **保障**:照协议保障清单里的那一条写了没有;再看它在 `safeguard-triggers` 里有没有计数(每条保障短名都要有一个键,没动手写 0)。没有计数也记没落实,依据里写清缺的是计数。 -随机性和停止条件按协议正文核:代码里有没有协议没登记的随机数、读时钟、按时间或成绩提前退出。查跨文件的随机性和时间依赖要顺着调用读——预扫是逐段单独判的,看不到跨文件的关系,这部分要你来看。这类问题出在某个因子的代码里,那个因子就记没落实,依据里写清是哪一句协议没登记它;出在没归属的改动里,就在那一处判越界。 +随机性和停止条件按协议正文核:代码里有没有协议没登记的随机数、读时钟、按时间或成绩提前退出。查跨文件的随机性和时间依赖要顺着调用读——程序收件时的检查是逐段单独判的,看不到跨文件的关系,这部分要你来看。这类问题出在某个因子的代码里,那个因子就记没落实,依据里写清是哪一句协议没登记它;出在没归属的改动里,就在那一处判越界。 代码导入的库要在材料"任务与本轮"一节里的"任务要点"列出的库里;不在的,正式实验会直接报错,记在用到它的那一项上。 @@ -30,8 +30,6 @@ 不通过时,实验员只按你写的改,所以每一项的依据要让他能照着改:好——"`solver.py:88-95` 加了 `if best > target: break`,协议的停止条件只有迭代 500 次,这段要删";坏——"实现不够忠实"。找到一处问题后继续核完,一次说全。 -材料里的预扫结果说明这些改动已经逐段通过了判别模型;你可以据此多看几眼,但判断以代码为准。 - ## 交什么 逐项结果(每条条款、每个因子、每条保障各一项;保障的短名照冻结协议的保障清单写)和对每一处没归属改动的判断(`unassigned`,材料说没有就交空数组);说明(`body`)写逐项结果之外需要说明的事,同一行同时属于因子和保障时在这里写一句提醒,没有要补充的就写"无"。 diff --git a/src/agents/tools.ts b/src/agents/tools.ts index 0ba3467..e1518d5 100644 --- a/src/agents/tools.ts +++ b/src/agents/tools.ts @@ -134,7 +134,7 @@ const READ_MAX_BYTES = 50 * 1024; * 读一个文件或列一个目录。路径必须是绝对路径,按真实路径(解析 .. 和符号链接之后)判断在不在可读目录里: * 模型正常探索时就会去 ls 上一层,看到别人还没公开的东西。不合格就抛错,Pi 把错误回给模型。 */ -export function readSource(roots: readonly string[], path: string, offset: number): string { +export function readSource(roots: readonly string[], path: string): string { const allowed = roots.map((r) => realpathSync(r)); const listing = allowed.map((r) => ` ${r}`).join("\n"); if (!isAbsolute(path)) throw new Error(`read_source 只接受绝对路径,收到的是 "${path}"。可读的目录:\n${listing}`); @@ -160,26 +160,23 @@ export function readSource(roots: readonly string[], path: string, offset: numbe lines = text.split("\n"); what = `文件 ${path},共 ${lines.length} 行、${Buffer.byteLength(text)} 字节`; } - if (offset > lines.length) throw new Error(`offset=${offset} 超出范围:${what},offset 取 1 到 ${lines.length}。`); const shown: string[] = []; let bytes = 0; - for (let i = offset - 1; i < lines.length && shown.length < READ_MAX_LINES; i++) { - const line = lines[i] as string; + for (const line of lines) { + if (shown.length === READ_MAX_LINES) break; bytes += Buffer.byteLength(line) + 1; if (bytes > READ_MAX_BYTES) { - // 单独一行就超过上限:只给这一行的开头,并明说。 + // 第一行就超过上限:只给这一行的开头,并明说。 if (shown.length === 0) { const head = Buffer.from(line).subarray(0, READ_MAX_BYTES).toString("utf8"); - return `${head}\n\n[截断:第 ${offset} 行超过 ${READ_MAX_BYTES} 字节,只显示了它的开头。${what}。]`; + return `${head}\n\n[截断:第 1 行超过 ${READ_MAX_BYTES} 字节,只显示了它的开头。${what}。]`; } break; } shown.push(line); } - const last = offset + shown.length - 1; - if (offset === 1 && last === lines.length) return shown.join("\n"); - const next = last < lines.length ? `要读后面,用 offset=${last + 1} 再调一次。` : "已读到结尾。"; - return `${shown.join("\n")}\n\n[截断:本次显示第 ${offset}–${last} 行(单次最多 ${READ_MAX_LINES} 行或 ${READ_MAX_BYTES} 字节)。${what}。${next}]`; + if (shown.length === lines.length) return shown.join("\n"); + return `${shown.join("\n")}\n\n[截断:只显示了前 ${shown.length} 行(单次最多 ${READ_MAX_LINES} 行或 ${READ_MAX_BYTES} 字节)。${what}。]`; } /** @@ -198,8 +195,6 @@ function text(value: string) { return { content: [{ type: "text" as const, text: value }], details: {} }; } -const OFFSET = Type.Optional(Type.Integer({ minimum: 1, description: "从第几行开始读,默认 1" })); - /** 这个任务的自定义工具(实验员还有换过环境的 bash)。Pi 自带的其余几个由 Pi 按白名单自己装。 */ export function buildTools(task: SessionTask, state: SubmitState, host: Host): ToolDefinition[] { const tools: ToolDefinition[] = []; @@ -312,9 +307,9 @@ export function buildTools(task: SessionTask, state: SubmitState, host: Host): T label: "读代码与资料", description: `读取代码或资料文件,或列出一个目录。path 用绝对路径,必须在这些目录里:${roots.join("、")}。` + - `单次最多 ${READ_MAX_LINES} 行或 ${READ_MAX_BYTES} 字节,超出时会告诉你用 offset 从第几行接着读。`, - parameters: Type.Object({ path: Type.String({ description: "绝对路径" }), offset: OFFSET }, { additionalProperties: false }), - execute: async (_id, params) => text(readSource(roots, params.path, params.offset ?? 1)), + `单次最多 ${READ_MAX_LINES} 行或 ${READ_MAX_BYTES} 字节,超出的部分不显示,结尾会写明截断。`, + parameters: Type.Object({ path: Type.String({ description: "绝对路径" }) }, { additionalProperties: false }), + execute: async (_id, params) => text(readSource(roots, params.path)), })); } diff --git a/src/jev/jev.ts b/src/jev/jev.ts index c6f77ca..31d4570 100644 --- a/src/jev/jev.ts +++ b/src/jev/jev.ts @@ -7,8 +7,11 @@ import { questionsOf, taskOf, type Question } from "./questions.ts"; type Factor = Submission<"proposal">["factors"][number]; -/** 判重看的因子:名称和做法。关掉时换成什么、延续哪个因子不放:它们不改变"这条提案改什么"。 */ -export type DedupFactor = Pick; +/** + * 判重看的因子:名称、做法和关键设定。问题写着"设定不同就不算同一个改法",所以关键设定要给 Jev 看。 + * 关掉时换成什么、延续哪个因子不放:它们不改变"这条提案改什么"。 + */ +export type DedupFactor = Pick; /** 一份冻结协议的条款(docs/方法.md 4.3 ⑥"冻结协议包括"),字段与统一提交相同。 */ export type FrozenProtocol = Pick, "factors" | "safeguards" | "metrics" | "body" | "clauses">; @@ -77,7 +80,7 @@ export async function checkHunk(model: string, hunk: CodeHunk, protocol: Protoco } function dedupFactor(f: DedupFactor): { [key: string]: Json } { - return { name: f.name, method: f.method }; + return { name: f.name, method: f.method, keySettings: f.keySettings }; } function check(judgments: JevJudgment[]): JevCheck { diff --git a/src/ledger/kinds.ts b/src/ledger/kinds.ts index 1e7f9ac..39da0f0 100644 --- a/src/ledger/kinds.ts +++ b/src/ledger/kinds.ts @@ -131,6 +131,8 @@ export const KINDS = { submission: Type.Unsafe({}), /** * 3 退回:被退回的那份内容原样存(它可能正是因为不合格式才被退回),连同退回理由。 + * target 是这次任务的对象,与提交的 target 同义(候选、原提案、消融格,没有对象的写 null): + * 同一人在同一阶段可能对几个对象各做一次同种任务,重新发起时按它找本人这个对象的上一次退回。 * jev 是这次退回用到的 Jev 判断(见 JevUsed),判"否"的几问也一起存;没用到 Jev 的写 null。 */ rejection: Type.Object( @@ -138,6 +140,7 @@ export const KINDS = { stageId: Id, author: Id, kind: Type.Enum(SUBMISSION_KIND_NAMES, { type: "string" }), + target: OptionalId, submitted: AnyJson, reason: Text, jev: JevUsed, diff --git a/src/ledger/schema.ts b/src/ledger/schema.ts index f0fbe61..7554687 100644 --- a/src/ledger/schema.ts +++ b/src/ledger/schema.ts @@ -3,7 +3,7 @@ import type { DatabaseSync } from "node:sqlite"; // 账本的表结构(docs/结构.md 第四部分"全部只追加"):只有一张表,每一行是序号、种类、内容、内容的哈希。 // 表结构一改就把版本号加 1;旧文件版本号对不上时打开报错,不迁移,也不逐字核对建表语句。 -export const SCHEMA_VERSION = 3; +export const SCHEMA_VERSION = 4; /** 新文件要执行的建表语句,由 Ledger 在一个事务里执行,最后写入版本号。 */ export const CREATE_SCHEMA = [ diff --git a/src/materials/assigned.ts b/src/materials/assigned.ts index 9f16c51..bbf42c8 100644 --- a/src/materials/assigned.ts +++ b/src/materials/assigned.ts @@ -100,8 +100,8 @@ export function repair(d: Draft): ChecklistOf<"repair"> { } /** - * ⑨ 核验:没归到任何因子或保障的改动排第一(程序按位置表现算);冻结协议、位置表、指标定义、改动全文、 - * Jev 对这份实现的预扫结果。因子与保障同一行的地方只作提醒。 + * ⑨ 核验:没归到任何因子或保障的改动排第一(程序按位置表现算);冻结协议、位置表、指标定义、改动全文。 + * 因子与保障同一行的地方只作提醒。能走到核验的实现已经过了 Jev,Jev 的判断不给核验员看(Jev 是判别条件,不是提示)。 */ export function verification(d: Draft): ChecklistOf<"verification"> { const candidate = d.req.object!; @@ -139,10 +139,6 @@ export function verification(d: Draft): ChecklistOf<"verification"> { "### 改动全文(候选相对起点代码,每段前后各 3 行上下文)", "", fenced(d.req.changes!.diff, "diff"), - "", - "### 预扫结果(收下这份实现时 Jev 逐段判过;概率和阈值原样列出)", - "", - ...(impl.jev === null ? ["(没有)"] : impl.jev.map((j) => `- ${j.question}:概率 ${j.probability},阈值 ${j.threshold}`)), ]); metricSection(d, p); return { kind: "verification", unassigned: found.unassigned.map((u) => u.location), ...items }; @@ -170,17 +166,31 @@ function ablation(d: Draft, role: string, codeNote: string[]): { cell: string; k return { cell, key, factors: items.factors, lines }; } -/** ⑯ 消融实现:从完整代码出发只关指定的因子。 */ +/** + * ⑯ 消融实现:从完整代码出发只关指定的因子。核验不通过重做时(这一格已有公开的消融核验)另放退回意见: + * 最近一次核验里关错了的因子和核验员写的依据,写法同修复的退回意见。 + */ export function ablationImplementation(d: Draft): ChecklistOf<"ablationImplementation"> { const a = ablation(d, "实验员", ["你的工作目录是完整代码的副本,改动都落在工作目录里。"]); - d.section("本任务要处理的东西", a.lines); + const verified = d.board.published("ablationVerification").filter((s) => s.target === a.cell).at(-1); + const opinion: string[] = []; + if (verified !== undefined) { + const off = new Set([a.key, ...lastAblation(d, a.cell).alsoOff]); + const wrong = verified.content.factors.filter((x) => x.off !== off.has(x.factor)); + const text = (x: (typeof wrong)[number]) => `- 因子 ${x.factor} 应当${off.has(x.factor) ? "关着" : "照留"},核验员看到的是${x.off ? "关着" : "照留"}:${x.where}`; + opinion.push("### 退回意见", "", "上一次交的这一格没通过消融核验,下面是关错了的因子:", ...wrong.map(text), ""); + } + d.section("本任务要处理的东西", [...opinion, ...a.lines]); return { kind: "ablationImplementation", cell: a.cell, off: a.key, factors: a.factors }; } +/** 这一格最近一次公开的消融实现提交。 */ +const lastAblation = (d: Draft, cell: string) => d.board.published("ablationImplementation").filter((s) => s.target === cell).at(-1)!.content; + /** ⑯ 消融核验:只查是不是只关了指定的因子(加实验员报的一起关的因子)。不算没有归属的改动。 */ export function ablationVerification(d: Draft): ChecklistOf<"ablationVerification"> { const a = ablation(d, "核验员", []); - const also = d.board.published("ablationImplementation").filter((s) => s.target === a.cell).at(-1)!.content.alsoOff; + const also = lastAblation(d, a.cell).alsoOff; d.section("这一格实际要关的因子", [ [a.key, ...also].join("、") + (also.length === 0 ? "(只关指定的因子)" : "(指定的因子加实验员报的一起关的因子)"), ]); diff --git a/src/materials/belief-book.ts b/src/materials/belief-book.ts index 247ba1a..3a633cc 100644 --- a/src/materials/belief-book.ts +++ b/src/materials/belief-book.ts @@ -16,7 +16,7 @@ import { measured, num, pct, PLACEMENT_TEXT, rangeText, resourcesText, score, SY import type { StudyFacts } from "./inputs.ts"; import { answerPathLines } from "./locations.ts"; import { Board, validScore } from "./read.ts"; -import { cellId, factorId, factorKey, ORIGINAL_BASELINE, type Settlement } from "./shapes.ts"; +import { CELL_NOT_RUN, cellId, factorId, factorKey, ORIGINAL_BASELINE, type Settlement } from "./shapes.ts"; // 信念本(方法.md 6.10):程序从已公开的记录现算,不存。数字全由 rules 的函数算,这里只挑出来写成字。 // 只读这几样:已关阶段的提交、按 Board.runPublic 公开的运行、决定、结算、解封;封存、第一次预测、退回一律不读。 @@ -96,6 +96,8 @@ export function beliefBookOf(board: Board, study: StudyFacts, round: number | nu const others = Object.keys(judged.metrics).filter((k) => k !== study.finalMetric).sort(compareIds); if (others.length > 0) lines.push(` 其余指标:${others.map((k) => `${k} ${measured(judged.metrics[k]!, num)}`).join(";")}`); lines.push(...answerPathLines(judged, board.code(c.id).locations!, null).map((x) => ` ${x}`)); + // 合法化扣掉的分(方法.md 6.10 第 3 条)记在任务自己的附加记录里,字段由任务定,原样写出。 + lines.push(` 任务的附加记录(含合法化,原样):${JSON.stringify(judged.extra)}`); } }); } @@ -186,7 +188,11 @@ function cellText(board: Board, s: { round: number } & Settlement, key: string, } else if (outcome.kind === "joint") { parts.push(`证据不足(测的是 ${outcome.off.join(" + ")} 一起关${outcome.score === null ? "" : `,分数 ${score(outcome.score)}`})`); } else { - parts.push(`证据不足:${board.runOf(cell)!.result!.judged!.verdictReason}`); + // 证据不足有两种来历(方法.md 6.6):启动了但没有有效分数,原因取判分;核验不通过、重做次数用完而不跑, + // 这一格没有运行,原因取 flow 写的事件。 + const judged = board.runOf(cell)?.result!.judged; + const why = judged === undefined ? board.eventReason(CELL_NOT_RUN, cell) : judged === null ? "没有判分" : judged.verdictReason; + parts.push(`证据不足:${why}`); } parts.push(`测前合并范围 ${rangeText(out.merged)}`); if (out.worldBits !== null) parts.push(`世界比特 ${num(out.worldBits)}`); diff --git a/src/materials/index.ts b/src/materials/index.ts index dedcc0e..ce2832c 100644 --- a/src/materials/index.ts +++ b/src/materials/index.ts @@ -8,7 +8,9 @@ export { queryMaterial, readPrompts } from "./prompts.ts"; export { callId, candidateId, + CELL_NOT_RUN, cellId, + cellNotRunEvent, clauseKey, critiqueId, DECISION_KINDS, diff --git a/src/materials/locations.ts b/src/materials/locations.ts index eb0ce13..0021428 100644 --- a/src/materials/locations.ts +++ b/src/materials/locations.ts @@ -105,8 +105,8 @@ export function unassignedChanges( /** * 答案路径和保障动手次数,对照位置表写成文字(方法.md 第五部分"答案路径")。合法的答案路径是 main、 - * 位置表里的因子编号、保障短名。答案出自保障时单独标一行:消融格用方法原句,正向运行换一句; - * 消融格里答案出自被关掉的因子时另写一句。offFactor 是消融格关掉的因子编号,正向运行为 null。 + * 位置表里的因子编号、保障短名。答案出自保障时单独标一行:消融格用方法原句,正向运行换一句。 + * offFactor 是消融格关掉的因子编号,正向运行为 null。 */ export function answerPathLines(judged: JudgedResult, table: LocationTable, offFactor: string | null): string[] { const factors = table.factorLocations.map((f) => f.factor); @@ -125,7 +125,6 @@ export function answerPathLines(judged: JudgedResult, table: LocationTable, offF : `本格答案出自保障 ${a.value},量到的可能是这条保障的效果,而不是关掉这个因子的效果。`, ); } - if (a.value === offFactor) lines.push(`答案出自被关掉的因子 ${a.value}:这一格的代码里它本该关着。`); } const t = judged.safeguardTriggers; if (t.status === "unreadable") lines.push(`保障动手次数读不到:${t.reason}`); diff --git a/src/materials/prompts.ts b/src/materials/prompts.ts index 7f0cbff..d93102a 100644 --- a/src/materials/prompts.ts +++ b/src/materials/prompts.ts @@ -48,11 +48,13 @@ export function closing(kind: SubmissionKind, object: string | null, proposalLim const done = kind === "proposal" ? `每调用一次 research_submit 交一条提案;本轮你最多交 ${proposalLimit} 条、至少 1 条。交完想交的几条就结束回复。` - : kind === "explanation" - ? `调用一次 research_submit,explanations 里最多 ${proposalLimit} 条;看回执发现交错了,用 replaces 写那份的提交编号整份替换。` - : role === "researcher" - ? "调用一次 research_submit 交上就完成了;看回执发现交错了,用 replaces 写那份的提交编号整份替换。" - : "调用一次 research_submit 交上,这个任务就结束了。"; + : kind === "critique" + ? "每调用一次 research_submit 交一份挑刺;可以交一份或几份,每份只针对这条提案的整体或其中一个因子。交完想交的几份就结束回复。" + : kind === "explanation" + ? `调用一次 research_submit,explanations 里最多 ${proposalLimit} 条;看回执发现交错了,用 replaces 写那份的提交编号整份替换。` + : role === "researcher" + ? "调用一次 research_submit 交上就完成了;看回执发现交错了,用 replaces 写那份的提交编号整份替换。" + : "调用一次 research_submit 交上,这个任务就结束了。"; return [ ...(object === null ? [] : [`- 本次对象:${object}`]), "- 调用 `research_submit`,payload 写这些字段:", diff --git a/src/materials/read.ts b/src/materials/read.ts index 8847c5d..33d340f 100644 --- a/src/materials/read.ts +++ b/src/materials/read.ts @@ -2,7 +2,6 @@ import type { Content, Ledger, LedgerRecord, StoredSubmission } from "../ledger/ import { compareIds, type RunState } from "../rules/index.ts"; import type { JudgedResult, Range, Stage, SubmissionKind } from "../vocab/index.ts"; import { - callPrefix, cellId, ORIGINAL_BASELINE, type DecisionKind, @@ -82,6 +81,11 @@ export class Board { return this.protocols().find((x) => x.candidate === candidate)!.protocol; } + /** 冻结出这个候选的回应或统一(已公开的最后一版):答复不进冻结协议,从这里读。 */ + answerOf(candidate: string): Sub<"response" | "unification"> { + return [...this.published("response"), ...this.published("unification")].filter((s) => s.target === candidate).sort((a, b) => a.seq - b.seq).at(-1)!; + } + /** * 第 round 轮的留池候选(方法.md 4.3 ①):之前各轮冻结的、从没进过任何一批、基线还是本轮基线的候选。 * 只看冻结协议和选批决定,本轮判重合并之前(提案阶段)也能算。 @@ -185,21 +189,19 @@ export class Board { } /** - * 重新发起时,本人上一次做同一个任务被退回的原因(任务书 09 第五部分):同一任务(callId 前缀相同) - * 的各次调用里,开始与结束之间本人在这个阶段被退回的最后一条。没有为 null。别人的退回不读。 + * 重新发起时,本人上一次做同一个任务被退回的原因(任务书 09 第五部分):本人在这个阶段、这种提交、 + * 这个对象上被退回的最后一条。没有为 null。别人的退回、本人别的对象上的退回不读。 */ lastRejection(stageId: string, actor: string, kind: SubmissionKind, object: string | null): string | null { - const prefix = callPrefix(stageId, actor, kind, object); - let inCall = false; - let reason: string | null = null; - for (const r of this.#records) { - if (r.kind === "callStarted" && r.content.call.startsWith(prefix)) inCall = true; - if (r.kind === "callEnded" && r.content.call.startsWith(prefix)) inCall = false; - if (inCall && r.kind === "rejection" && r.content.author === actor && r.content.stageId === stageId && r.content.kind === kind) { - reason = r.content.reason; - } - } - return reason; + const all = this.#of("rejection").map((r) => r.content); + return all.filter((x) => x.stageId === stageId && x.author === actor && x.kind === kind && x.target === object).at(-1)?.reason ?? null; + } + + /** 一个对象最后一条某种事件的原因(事件种类见 shapes.ts)。没有这条事件报错:调用它的地方按方法这条事件一定写了。 */ + eventReason(event: string, object: string): string { + const r = this.#of("event").filter((e) => e.content.event === event && e.content.objects.includes(object)).at(-1); + if (r === undefined) throw new Error(`账本里没有对象 ${object} 的 ${event} 事件`); + return r.content.reason; } // ------------------------------------------------------------------------- diff --git a/src/materials/researcher.ts b/src/materials/researcher.ts index 5cd38eb..94eb4ea 100644 --- a/src/materials/researcher.ts +++ b/src/materials/researcher.ts @@ -1,5 +1,5 @@ import { compareIds } from "../rules/index.ts"; -import type { ChecklistOf } from "../vocab/index.ts"; +import { ANSWER_PATH_KEY, SAFEGUARD_TRIGGERS_KEY, type ChecklistOf } from "../vocab/index.ts"; import { beliefSection, codeSection, @@ -40,14 +40,14 @@ function frame(d: Draft, own: Own, middle: () => void): void { if (d.round >= 1) metricsSection(d); } -/** 提案、回应、统一共用的因子规则(方法.md 第五部分"延续""指标"、4.3 ②)。 */ +/** 提案、回应、统一共用的因子规则(方法.md 第五部分"延续""指标"、4.3 ②)。答案路径、保障动手次数的两个键也算占用(用户 2026-10-03 定)。 */ function factorRules(d: Draft) { const registered = registeredMetrics(d.board).map((m) => m.id); return { factorLimit: d.req.study.factorLimit, continuable: d.board.protocols().flatMap((p) => p.protocol.factors.map((_, i) => factorId(p.candidate, factorKey(i)))), reusableMetrics: registered, - takenMetricIds: [...d.req.study.metrics.map((m) => m.id), ...registered], + takenMetricIds: [...d.req.study.metrics.map((m) => m.id), ANSWER_PATH_KEY, SAFEGUARD_TRIGGERS_KEY, ...registered], }; } @@ -191,6 +191,7 @@ export function unification(d: Draft): ChecklistOf<"unification"> { /** * ⑦ 第二次预测:冻结协议、挑刺与回应按挑刺编号配对、本人第一次给这个候选的三个数(4.3 ⑦)。 + * 答复不在冻结协议里,从这个候选已公开的回应或统一读(回应的 body 是答复正文,统一的 body 已在协议里);留池候选读它冻结那一轮的(用户 2026-10-03 定)。 * 第一次预测只取本人本轮的;别人的第一次和作者自评不读。留池候选本轮不做第一次预测(4.3 ④)。 */ export function secondPrediction(d: Draft): ChecklistOf<"secondPrediction"> { @@ -206,14 +207,15 @@ export function secondPrediction(d: Draft): ChecklistOf<"secondPrediction"> { }, ]; frame(d, own, () => { - const p = d.board.protocol(candidate); const critiques = d.board.published("critique"); - const pairs = p.replies.flatMap((r) => { + const a = d.board.answerOf(candidate); + const pairs = a.content.replies.flatMap((r) => { const c = critiques.find((x) => x.item === r.critique)!; return [`### 挑刺 ${r.critique}(${c.author} 挑的)`, "", c.content.body, "", `作者的答复:${r.reply}`, ""]; }); - d.section("本任务要处理的东西", [mergeLine(d, candidate), "", `### 候选 ${candidate} 的冻结协议`, "", json(p)]); - d.section("挑刺与回应", pairs.length === 0 ? ["(没有针对这个候选的挑刺)"] : [PRESUMPTION, "", ...pairs]); + const extra = a.kind === "response" ? ["### 答复正文", "", a.content.body] : []; + d.section("本任务要处理的东西", [mergeLine(d, candidate), "", `### 候选 ${candidate} 的冻结协议`, "", json(d.board.protocol(candidate))]); + d.section("挑刺与回应", [...(pairs.length === 0 ? ["(没有针对这个候选的挑刺)"] : [PRESUMPTION, "", ...pairs]), ...extra]); }); return { kind: "secondPrediction", candidate }; } diff --git a/src/materials/shapes.ts b/src/materials/shapes.ts index eb4ede0..871c41e 100644 --- a/src/materials/shapes.ts +++ b/src/materials/shapes.ts @@ -10,6 +10,7 @@ import type { adopt, settleAblation, } from "../rules/index.ts"; +import type { Content } from "../ledger/index.ts"; import type { Submission } from "../vocab/index.ts"; // 账本里只存成 Json 的几样内容长什么样、编号怎么起(用户 2026-10-03 决定 2):materials 读它们,flow 照着写。 @@ -45,31 +46,19 @@ export function splitCell(cell: string): { candidate: string; key: string } { } /** 解释:第 round 轮作者交的第 i 条(从 1 数)。结算里判确认、否定用它。 */ export const explanationId = (round: number, author: string, i: number): string => `r${round}-${author}-x${i}`; -/** - * 模型调用:同一个任务(阶段、谁、哪种提交、对象)第 attempt 次发起。 - * 重新发起时 materials 按这个前缀找回上一次在哪段记录之间被退回(结构.md 第四部分第 10 种记录)。 - */ +/** 模型调用:同一个任务(阶段、谁、哪种提交、对象)第 attempt 次发起。 */ export const callId = (stageId: string, actor: string, kind: string, object: string | null, attempt: number): string => - `${callPrefix(stageId, actor, kind, object)}${attempt}`; -export const callPrefix = (stageId: string, actor: string, kind: string, object: string | null): string => - `${stageId}.${actor}.${kind}.${object ?? "none"}.`; + `${stageId}.${actor}.${kind}.${object ?? "none"}.${attempt}`; // --------------------------------------------------------------------------- // 冻结协议(账本记录 protocol.protocol,方法.md 4.3 ⑥) // --------------------------------------------------------------------------- /** - * 一个候选的冻结协议:原提案合上回应改过的部分(或统一后的整份),加上答复。 - * baseline 是它出发的基线对象编号;round 是冻结的轮次;proposals 是合并进来的全部原提案。 - * 留池候选沿用这一份,不重写(方法.md 4.3 ①)。 + * 一个候选的冻结协议(方法.md 4.3 ⑥"冻结协议包括"),拼法在 submissions;答复留在回应或统一的提交里(用户 2026-10-03 定)。 + * baseline 是出发的基线对象编号,round 是冻结的轮次,proposals 是合并进来的全部原提案。留池候选沿用这一份(4.3 ①)。 */ -export type FrozenProtocol = Pick, "factors" | "safeguards" | "metrics" | "body" | "clauses" | "replies"> & { - round: number; - baseline: string; - proposals: string[]; - /** 回应或统一的答复正文。 */ - replyBody: string; -}; +export type FrozenProtocol = Pick, "factors" | "safeguards" | "metrics" | "body" | "clauses"> & { round: number; baseline: string; proposals: string[] }; // --------------------------------------------------------------------------- // 冻结代码的位置表(账本记录 code.locations) @@ -114,3 +103,10 @@ export type Settlement = { input: SettleInput; output: ReturnType; }; + +/** + * 事件(账本记录 event):一格消融核验不通过、重做次数用完,这一格不跑、记"证据不足"(方法.md 4.3 ⑯、6.6)。 + * 这一格没有运行,信念本从这条事件读原因。objects 只有这一格的编号。 + */ +export const CELL_NOT_RUN = "cellNotRun"; +export const cellNotRunEvent = (cell: string, reason: string): Content<"event"> => ({ event: CELL_NOT_RUN, reason, objects: [cell] }); diff --git a/src/rules/ablation.ts b/src/rules/ablation.ts index 4b7ac90..1dbf04e 100644 --- a/src/rules/ablation.ts +++ b/src/rules/ablation.ts @@ -9,7 +9,8 @@ import { adoptionLine } from "./forward.ts"; /** * 一格的结局(6.6"一格的几种结局"): * - valid:有效分数(含合法化后的分数),只有它进标签的求和、判确认与否定、记比特和惊讶; - * - insufficient:证据不足(启动了,但跑崩、超时、没有产物、修不了或判为违规); + * - insufficient:证据不足:启动了(扣了次数),但跑崩、超时、没有产物、修不了或判为违规; + * 或者消融核验不通过、重做次数用完,这一格不启动、不扣次数; * - untested:没测; * - cannotRun:跑不起来(不启动、不扣次); * - joint:一起关,off 是实际关掉的全部因子;score 是那次运行的有效分数,没有时为 null。 diff --git a/src/submissions/checks.ts b/src/submissions/checks.ts new file mode 100644 index 0000000..e1d8a1b --- /dev/null +++ b/src/submissions/checks.ts @@ -0,0 +1,193 @@ +import type { ChecklistOf, Range, Submission } from "../vocab/index.ts"; +import type { AblationCode, ForwardCode, Incoming, ProtocolBody } from "./types.ts"; + +// 程序检查:vocab 字段定义管不到的跨字段规则(WORK_LOG"留给 submissions 的跨字段规则"14 条, +// 加任务书 10 第四部分的不许改的文件、代码存不了、指标编号重名、提案条数)。 +// 每条规则交回零到几行问题,一份提交的问题一次列全。编号写错时列出对号清单里该写的全部编号: +// 只说"不对",模型拿不到可对照的写法(参考项目真实运行 15-tight-2 第 3 轮,提交 378fe68)。 + +/** 一份提交的全部程序检查。回应、统一查拼好的协议(protocol),其余种类 protocol 为 null。 */ +export function programProblems(inc: Incoming, protocol: ProtocolBody | null): string[] { + switch (inc.kind) { + case "entry": + case "discussion": + case "wrapUp": + return []; + case "claim": + return claimProblems(inc.content, inc.checklist); + case "proposal": + return [ + ...(inc.known.accepted + 1 > inc.checklist.maxProposals + ? [`本阶段你已经收下 ${inc.known.accepted} 条提案,每人最多 ${inc.checklist.maxProposals} 条:要改哪一条,就在 replaces 里写它的提交编号整份替换,不要另交`] + : []), + ...factorProblems(inc.content, inc.checklist), + ...increasing("authorEstimate", inc.content.authorEstimate), + ]; + case "firstPrediction": + case "secondPrediction": + return increasing("range", inc.content.range); + case "critique": + return within("target", [inc.content.target], ["whole", ...inc.checklist.factors]); + case "response": + return [...covers("replies 的挑刺编号", inc.content.replies.map((r) => r.critique), inc.checklist.critiques), ...factorProblems(protocol!, inc.checklist)]; + case "unification": + return [ + ...covers("replies 的挑刺编号", inc.content.replies.map((r) => r.critique), inc.checklist.critiques), + ...covers("sources 的原提案编号", inc.content.sources.map((s) => s.proposal), inc.checklist.sources), + ...factorProblems(protocol!, inc.checklist), + ]; + case "implementation": + case "repair": + return [...locationProblems(inc.content, inc.checklist), ...codeProblems(inc.known.code)]; + case "verification": + return verificationProblems(inc.content, inc.checklist); + case "explanation": + return explanationProblems(inc.content, inc.checklist); + case "ablationImplementation": + return ablationProblems(inc.content, inc.checklist, inc.known.code); + case "ablationVerification": + return covers("factors 的因子", inc.content.factors.map((f) => f.factor), inc.checklist.factors); + } +} + +// --------------------------------------------------------------------------- +// 各种提交 +// --------------------------------------------------------------------------- + +/** 规则 7:志愿至少 min(N, 入口总数) 个、不重复、编号都在入口清单里(方法.md 4.2)。 */ +function claimProblems(c: Submission<"claim">, k: ChecklistOf<"claim">): string[] { + return [ + ...(c.choices.length < k.minChoices ? [`choices 至少要排 ${k.minChoices} 个入口,你排了 ${c.choices.length} 个`] : []), + ...within("choices", c.choices, k.entries), + ...repeated(c.choices).map((x) => `choices 里 ${x} 写了不止一次`), + ]; +} + +/** + * 提案、回应、统一共用(方法.md 第五部分"延续""指标"、4.3 ②⑥): + * 规则 2 因子数上限;3 两个因子不延续同一个、延续的编号看得到;4 保障短名;10 指标的引用与新登记。 + */ +function factorProblems(p: Pick, k: ChecklistOf<"proposal" | "response" | "unification">): string[] { + const continued = p.factors.flatMap((f) => (f.continues === null ? [] : [f.continues])); + const defined = p.metrics.defined.map((m) => m.id); + return [ + ...(p.factors.length > k.factorLimit + ? [`factors 有 ${p.factors.length} 个因子,超过上限 ${k.factorLimit} 个:多出来的留到下一轮,不要几个捆成一个`] + : []), + ...repeated(continued).map((x) => `有两个以上因子的 continues 都写了 ${x}:同一份提案里两个因子不能延续同一个因子`), + ...within("continues", unique(continued), k.continuable).map((x) => `${x};新因子写 null`), + ...unique(p.safeguards) + .filter((s) => s === "main" || /^p[1-9][0-9]*$/.test(s)) + .map((s) => `保障短名 ${s} 不能用:不能叫 main,也不能写成 p1、p2 这种因子编号的样子`), + ...repeated(p.safeguards).map((s) => `safeguards 里 ${s} 写了不止一次`), + ...within("metrics.reused", p.metrics.reused, k.reusableMetrics), + ...repeated(defined).map((x) => `metrics.defined 里编号 ${x} 写了不止一次`), + ...unique(defined) + .filter((x) => k.takenMetricIds.includes(x)) + .map((x) => `metrics.defined 的编号 ${x} 已被占用(已占用的是 ${k.takenMetricIds.join("、")}):换一个编号;要用别人登记过的就写进 metrics.reused`), + ]; +} + +/** 规则 11:位置表覆盖每个因子和每条保障;代码位置起始行不大于结束行(方法.md 4.3 ⑨)。 */ +function locationProblems(c: Submission<"implementation">, k: ChecklistOf<"implementation" | "repair">): string[] { + const all = [...c.factorLocations.flatMap((f) => f.locations), ...c.safeguardLocations.flatMap((s) => s.locations), ...c.unassigned.map((u) => u.location)]; + return [ + ...covers("factorLocations 的因子", c.factorLocations.map((f) => f.factor), k.factors), + ...covers("safeguardLocations 的保障", c.safeguardLocations.map((s) => s.safeguard), k.safeguards), + ...unique(all) + .filter((loc) => { + const m = /:([0-9]+)-([0-9]+)$/.exec(loc); + return m !== null && Number(m[1]) > Number(m[2]); + }) + .map((loc) => `代码位置 ${loc} 的起始行大于结束行`), + ]; +} + +/** 规则 12:核验的条款、因子、保障逐项全覆盖;没有归属的改动与材料列出的逐条对应(方法.md 4.3 ⑨)。 */ +function verificationProblems(c: Submission<"verification">, k: ChecklistOf<"verification">): string[] { + return [ + ...covers("clauses 的实现条款", c.clauses.map((x) => x.clause), k.clauses), + ...covers("factors 的因子", c.factors.map((x) => x.factor), k.factors), + ...covers("safeguards 的保障", c.safeguards.map((x) => x.safeguard), k.safeguards), + ...covers("unassigned 的改动位置", c.unassigned.map((x) => x.location), k.unassigned), + ]; +} + +/** + * 规则 5、6、1:解释最多 M 条;每条都交 cells、恰好覆盖材料列出的格,三个数严格递增(方法.md 4.3 ⑭)。 + * 用户 2026-10-03 定:只按完整流程全开实现,一律要求交 cells;关掉消融的情形以后做开关时再补。 + */ +function explanationProblems(c: Submission<"explanation">, k: ChecklistOf<"explanation">): string[] { + return [ + ...(c.explanations.length > k.maxExplanations ? [`交了 ${c.explanations.length} 条解释,最多 ${k.maxExplanations} 条`] : []), + ...c.explanations.flatMap((e, i) => + e.cells === undefined + ? [`第 ${i + 1} 条解释没交 cells:每条解释都要对 ${k.cells.join("、")} 每一格给三个数`] + : [ + ...covers(`第 ${i + 1} 条解释 cells 的格`, e.cells.map((x) => x.factor), k.cells), + ...e.cells.flatMap((x) => increasing(`第 ${i + 1} 条解释 ${x.factor} 格的 range`, x.range)), + ], + ), + ]; +} + +/** + * 规则 13:写了 cannotRun 时 alsoOff 为空;alsoOff 不含本格要关的因子、都在因子清单里、不重复(方法.md 4.3 ⑯)。 + * 跑得起来的另查代码:运行的规矩和正向运行相同,改了不许改的文件同样退回。 + */ +function ablationProblems(c: Submission<"ablationImplementation">, k: ChecklistOf<"ablationImplementation">, code: AblationCode | null): string[] { + return [ + ...(c.cannotRun !== null && c.alsoOff.length > 0 ? ["写了 cannotRun(这一格跑不起来)时 alsoOff 要交空数组:两种做法只能选一种"] : []), + ...(c.alsoOff.includes(k.off) ? [`alsoOff 不能写本格要关的因子 ${k.off},只写还要一起关的别的因子`] : []), + ...within("alsoOff", c.alsoOff.filter((x) => x !== k.off), k.factors.filter((x) => x !== k.off)), + ...repeated(c.alsoOff).map((x) => `alsoOff 里 ${x} 写了不止一次`), + ...(c.cannotRun === null ? codeProblems(code!) : []), + ]; +} + +/** 代码存不了、改了不许改的文件(方法.md 4.3 ⑨)。 */ +function codeProblems(code: ForwardCode | AblationCode): string[] { + if (!code.saved) return [`代码存不成版本:${code.path}(${code.reason})。改掉它再交`]; + return code.protectedFiles.length === 0 ? [] : [`改了不许改的文件:${code.protectedFiles.join("、")}。把它们改回本批基线的样子再交`]; +} + +// --------------------------------------------------------------------------- +// 共用 +// --------------------------------------------------------------------------- + +/** 规则 1:三个数严格递增(方法.md 第五部分"三个数")。 */ +function increasing(field: string, r: Range): string[] { + return r.q1 < r.q5 && r.q5 < r.q9 ? [] : [`${field} 的三个数要严格递增 q1 < q5 < q9,你写的是 ${r.q1} / ${r.q5} / ${r.q9}`]; +} + +/** 一组编号必须恰好覆盖 expected:不多、不少、不重复。只写出现了的几段。 */ +function covers(field: string, actual: readonly string[], expected: readonly string[]): string[] { + const unknown = unique(actual.filter((x) => !expected.includes(x))); + const missing = expected.filter((x) => !actual.includes(x)); + const twice = repeated(actual); + if (unknown.length + missing.length + twice.length === 0) return []; + return [ + [ + `${field}应当是 ${expected.length === 0 ? "没有(交空数组)" : expected.join("、")}`, + ...(unknown.length > 0 ? [`你写的 ${unknown.join("、")} 不在里面`] : []), + ...(missing.length > 0 ? [`还缺 ${missing.join("、")}`] : []), + ...(twice.length > 0 ? [`${twice.join("、")} 写了不止一次`] : []), + ].join(";"), + ]; +} + +/** 一组编号都要在 allowed 里(可以只写其中几个)。 */ +function within(field: string, actual: readonly string[], allowed: readonly string[]): string[] { + const unknown = unique(actual.filter((x) => !allowed.includes(x))); + if (unknown.length === 0) return []; + return [`${field} 只能写 ${allowed.length === 0 ? "(现在没有可写的编号)" : allowed.join("、")};你写的 ${unknown.join("、")} 不在里面`]; +} + +function unique(xs: readonly string[]): string[] { + return [...new Set(xs)]; +} + +/** 出现了不止一次的值,各列一次。 */ +function repeated(xs: readonly string[]): string[] { + return unique(xs.filter((x, i) => xs.indexOf(x) !== i)); +} diff --git a/src/submissions/index.ts b/src/submissions/index.ts new file mode 100644 index 0000000..b0df3e3 --- /dev/null +++ b/src/submissions/index.ts @@ -0,0 +1,14 @@ +// submissions:一份提交收下还是退回,先程序检查、再问 Jev(docs/结构.md 第二部分); +// 另有冻结协议的拼法,和核验、消融核验收下之后通过没有。不读也不写账本。 +export { ablationOutcome, verificationOutcome } from "./outcomes.ts"; +export { protocolBody } from "./protocol.ts"; +export { receive } from "./receive.ts"; +export { + type AblationCode, + type ForwardCode, + type Incoming, + type KnownOf, + type OriginalProposal, + type ProtocolBody, + type Verdict, +} from "./types.ts"; diff --git a/src/submissions/outcomes.ts b/src/submissions/outcomes.ts new file mode 100644 index 0000000..dba5c24 --- /dev/null +++ b/src/submissions/outcomes.ts @@ -0,0 +1,36 @@ +import type { Submission } from "../vocab/index.ts"; + +// 一份已经收下的核验、消融核验的结论(任务书 10 第三部分第 2、3 条):判的是"收下之后通过没有",不是"收不收"。 + +/** + * 核验通过没有(方法.md 4.3 ⑨):任何一项没落实,或者任何一条没有归属的改动判为越界,就不通过。 + * 没落实的条款、因子、保障和越界的改动分开列:保障短名可能和别的编号同形。 + */ +export function verificationOutcome(v: Submission<"verification">): { + passed: boolean; + clauses: string[]; + factors: string[]; + safeguards: string[]; + outOfScope: string[]; +} { + const clauses = v.clauses.filter((x) => !x.found).map((x) => x.clause); + const factors = v.factors.filter((x) => !x.found).map((x) => x.factor); + const safeguards = v.safeguards.filter((x) => !x.found).map((x) => x.safeguard); + const outOfScope = v.unassigned.filter((x) => x.outOfScope).map((x) => x.location); + return { passed: clauses.length + factors.length + safeguards.length + outOfScope.length === 0, clauses, factors, safeguards, outOfScope }; +} + +/** + * 消融核验通过没有(方法.md 4.3 ⑯):这一格要关的因子(off)和实验员报的一起关的因子(alsoOff)都写了"关着", + * 其余因子都写了"照留",才算通过。leftOn 是该关却照留的,wronglyOff 是该留却关了的。 + */ +export function ablationOutcome( + v: Submission<"ablationVerification">, + off: string, + alsoOff: readonly string[], +): { passed: boolean; leftOn: string[]; wronglyOff: string[] } { + const shouldBeOff = new Set([off, ...alsoOff]); + const leftOn = v.factors.filter((f) => shouldBeOff.has(f.factor) && !f.off).map((f) => f.factor); + const wronglyOff = v.factors.filter((f) => !shouldBeOff.has(f.factor) && f.off).map((f) => f.factor); + return { passed: leftOn.length + wronglyOff.length === 0, leftOn, wronglyOff }; +} diff --git a/src/submissions/protocol.ts b/src/submissions/protocol.ts new file mode 100644 index 0000000..e176478 --- /dev/null +++ b/src/submissions/protocol.ts @@ -0,0 +1,25 @@ +import type { Submission } from "../vocab/index.ts"; +import type { OriginalProposal, ProtocolBody } from "./types.ts"; + +/** + * 拼冻结协议的主体,只在这一处拼(任务书 10 第四部分"回应怎么拼成冻结协议")。 + * 回应:因子、保障、指标、正文四项,changes 里写了哪一项就整项换成新的,没写的用原提案的;实现条款取回应自己的。 + * 统一:直接用统一提交自己的字段,不叠原提案。答复不进协议(用户 2026-10-03 定)。 + */ +export function protocolBody( + from: { kind: "response"; content: Submission<"response">; original: OriginalProposal } | { kind: "unification"; content: Submission<"unification"> }, +): ProtocolBody { + if (from.kind === "unification") { + const { factors, safeguards, metrics, body, clauses } = from.content; + return { factors, safeguards, metrics, body, clauses }; + } + const { changes, clauses } = from.content; + const o = from.original; + return { + factors: changes.factors ?? o.factors, + safeguards: changes.safeguards ?? o.safeguards, + metrics: changes.metrics ?? o.metrics, + body: changes.body ?? o.body, + clauses, + }; +} diff --git a/src/submissions/receive.ts b/src/submissions/receive.ts new file mode 100644 index 0000000..001cc1a --- /dev/null +++ b/src/submissions/receive.ts @@ -0,0 +1,71 @@ +import { checkHunk, checkProtocol, type CodeHunk } from "../jev/index.ts"; +import type { JevJudgment } from "../vocab/index.ts"; +import { programProblems } from "./checks.ts"; +import { protocolBody } from "./protocol.ts"; +import type { Incoming, ProtocolBody, Verdict } from "./types.ts"; + +// 判一份提交收不收(任务书 10 第二部分第 4、5 步):先程序检查,有一项不过就退回、不问 Jev; +// 全过了再问 Jev,几问全问完再决定,有一问判"是"就退回。收不收只在这一处判(结构.md 第二部分"几条边界")。 +// Jev 调不通、回答读不懂时 jev 报错,这里不接住:那不是模型交得不对,flow 按内部错误停下。 + +/** 判一份提交。回应、统一先拼好冻结协议主体,程序检查、问 Jev、交回都用这同一份。 */ +export async function receive(inc: Incoming): Promise { + const protocol = + inc.kind === "response" + ? protocolBody({ kind: "response", content: inc.content, original: inc.known.original }) + : inc.kind === "unification" + ? protocolBody({ kind: "unification", content: inc.content }) + : null; + const problems = programProblems(inc, protocol); + if (problems.length > 0) return { accepted: false, reason: ["这份提交没有收下,要改的地方:", ...problems.map((p) => `- ${p}`)].join("\n"), jev: null }; + const asked = await askJev(inc, protocol); + if (asked === null) return { accepted: true, jev: null, protocol }; + if (asked.flagged.length > 0) { + return { accepted: false, reason: ["这份提交没有收下,Jev 判出下面几处有问题:", ...asked.flagged].join("\n\n"), jev: asked.judgments }; + } + return { accepted: true, jev: asked.judgments, protocol }; +} + +/** + * 问 Jev(方法.md 4.3 ⑥⑨):回应、统一对拼好的协议问冻结条款三问;实现、修复每个差异块问代码改动五问,顺序发。 + * 没有差异块的改动(二进制、只改可执行位、空文件增删)Jev 看不到内容,不问;一块都没有就等于没问,交回 null。 + * 消融实现不问:冻结协议里没有登记"关掉以后"的随机性和停止条件,五问没有对照(照参考项目)。 + * flagged 是退回原因里每一处的文字:只列判"是"的几问,问题名用 jev 的英文名(用户 2026-10-03 定);判"是"只看 jev 交回的 yes。 + */ +async function askJev(inc: Incoming, protocol: ProtocolBody | null): Promise<{ judgments: JevJudgment[]; flagged: string[] } | null> { + if (inc.kind === "response" || inc.kind === "unification") { + const c = await checkProtocol(inc.jevModel, protocol!); + return { judgments: c.judgments, flagged: c.flagged ? [`- 冻结协议:${flaggedText(c.judgments)}`] : [] }; + } + if (inc.kind !== "implementation" && inc.kind !== "repair") return null; + // 走到这里程序检查已过,代码一定存成了版本。 + const code = inc.known.code as Extract; + if (code.hunks.length === 0) return null; + const judgments: JevJudgment[] = []; + const flagged: string[] = []; + const nth = new Map(); + for (const hunk of code.hunks) { + nth.set(hunk.path, (nth.get(hunk.path) ?? 0) + 1); + const c = await checkHunk(inc.jevModel, hunk, code.protocol); + judgments.push(...c.judgments); + if (c.flagged) flagged.push(hunkText(hunk, nth.get(hunk.path)!, c.judgments)); + } + return { judgments, flagged }; +} + +/** 判"是"的那几问:问题名、概率、阈值。 */ +function flaggedText(judgments: readonly JevJudgment[]): string { + return judgments + .filter((j) => j.yes) + .map((j) => `${j.question} 概率 ${j.probability}(阈值 ${j.threshold})`) + .join(";"); +} + +const CHANGE = { added: "新增的文件", deleted: "删掉的文件", modified: "改动的文件" } as const; + +/** 一个差异块的退回文字:哪个文件的第几块、判"是"的几问,附差异原文。原文里有 ``` 时围栏加长,免得提前收尾。 */ +function hunkText(hunk: CodeHunk, n: number, judgments: readonly JevJudgment[]): string { + const longest = Math.max(0, ...(hunk.text.match(/`+/g) ?? []).map((s) => s.length)); + const fence = "`".repeat(Math.max(3, longest + 1)); + return [`- ${hunk.path}(${CHANGE[hunk.change]})的第 ${n} 块:${flaggedText(judgments)}`, "", `${fence}diff`, hunk.text, fence].join("\n"); +} diff --git a/src/submissions/types.ts b/src/submissions/types.ts new file mode 100644 index 0000000..c519c8e --- /dev/null +++ b/src/submissions/types.ts @@ -0,0 +1,52 @@ +import type { CodeHunk, ProtocolRules } from "../jev/index.ts"; +import type { ChecklistOf, JevJudgment, Submission, SubmissionKind } from "../vocab/index.ts"; + +// submissions 收的和交回的东西(任务书 10 第三部分)。submissions 只依赖 jev 和 vocab, +// workspace 的存版本结果、materials 的冻结协议在这里按相同形状另写,flow 赋值时由 tsc 对齐。 + +/** + * 冻结协议的主体(方法.md 4.3 ⑥"冻结协议包括"):因子、保障、指标、正文、实现条款。 + * 不带答复:答复留在回应或统一的提交记录里(用户 2026-10-03 定)。flow 补上轮次、基线、合并了哪些原提案,写成 materials 的 FrozenProtocol。 + */ +export type ProtocolBody = Pick, "factors" | "safeguards" | "metrics" | "body" | "clauses">; + +/** 回应拼协议时叠在下面的那条原提案(作者自评不进协议)。 */ +export type OriginalProposal = Pick, "factors" | "safeguards" | "metrics" | "body">; + +/** 实验员的工作目录存不成版本:workspace 给的路径和原因(例如符号链接)。 */ +type NotSaved = { saved: false; path: string; reason: string }; + +/** + * 实现、修复的代码:存成了版本就带改了的不许改的文件(tasks 的 protectedChanges)、 + * 和本批基线比出的差异块(jev 的 codeHunks 切好的),以及给 Jev 对照的协议正文和要采集的指标。 + */ +export type ForwardCode = NotSaved | { saved: true; protectedFiles: string[]; hunks: CodeHunk[]; protocol: ProtocolRules }; + +/** 消融实现的代码:只查存没存成、改没改不许改的文件。 */ +export type AblationCode = NotSaved | { saved: true; protectedFiles: string[] }; + +/** 按种类区分的"提交之后才知道的东西",没有的种类为 null。 */ +type KnownByKind = { + /** 本人本阶段已经收下几条提案(替换时不算被替换的那条)。 */ + proposal: { accepted: number }; + /** 作者那条原提案。 */ + response: { original: OriginalProposal }; + implementation: { code: ForwardCode }; + repair: { code: ForwardCode }; + /** 交了"跑不起来"(cannotRun 不是 null)时 flow 不存代码,code 为 null。 */ + ablationImplementation: { code: AblationCode | null }; +}; +export type KnownOf = K extends keyof KnownByKind ? KnownByKind[K] : null; + +/** 交给 submissions 判的一份提交,以 kind 区分:内容、这次任务的对号清单、提交之后才知道的东西、Jev 的模型名。 */ +export type Incoming = { + [K in SubmissionKind]: { kind: K; content: Submission; checklist: ChecklistOf; known: KnownOf; jevModel: string }; +}[SubmissionKind]; + +/** + * 判的结论。jev 是用到的全部 Jev 判断(判"否"的也在),没问 Jev 的为 null(账本的 JevUsed 要求至少一条)。 + * 收下时 protocol 只有回应、统一才有,其余为 null。 + */ +export type Verdict = + | { accepted: true; jev: JevJudgment[] | null; protocol: ProtocolBody | null } + | { accepted: false; reason: string; jev: JevJudgment[] | null }; diff --git a/src/vocab/submissions.ts b/src/vocab/submissions.ts index 34905e4..676c4b8 100644 --- a/src/vocab/submissions.ts +++ b/src/vocab/submissions.ts @@ -6,7 +6,7 @@ import { HypothesisMetric } from "./runs.ts"; /** * 每种提交的字段定义(方法.md 第五部分)。同一份定义同时是类型、收件时的格式检查、 * 发给模型的提交工具参数,所以字段说明写给模型看,每个字段都写明出口。 - * 这里只管单个字段长什么样;跨字段的规则(q₁ ≤ q₅ ≤ q₉、因子数上限、覆盖是否齐全等) + * 这里只管单个字段长什么样;跨字段的规则(q₁ < q₅ < q₉ 严格递增、因子数上限、覆盖是否齐全等) * 在 submissions 模块。 * 程序分配的编号(候选、挑刺、入口编号)不让模型填;因子、实现条款的编号就是它在清单里的位置。 */ @@ -479,7 +479,9 @@ export const VerificationSubmission = Type.Object( ), body: Type.String({ ...TEXT, - description: "说明,Markdown:逐项结果之外需要说明的事。没有要补充的就写\"无\"", + description: + "说明,Markdown:逐项结果之外需要说明的事。材料里列出的同一行既属于因子又属于保障的地方,在这里各写一句提醒(只是提醒,不判越界)。" + + "没有要补充的就写\"无\"", }), }, { diff --git a/tasks/circle_packing/TASK.md b/tasks/circle_packing/TASK.md index 6d5b295..c8e7e26 100644 --- a/tasks/circle_packing/TASK.md +++ b/tasks/circle_packing/TASK.md @@ -211,7 +211,7 @@ import 之前它还会删掉工作目录里已有的 `solution.json` 和 `metric | 键 | 值 | 写什么 | | --- | --- | --- | | `answer-path` | 字符串 | 最终交的答案出自哪里:`main`(正常主流程),或冻结协议因子清单里某个因子的编号(`p1`、`p2`……),或保障清单里某条保障的短名 | -| `safeguard-triggers` | 对象 | 键是冻结协议保障清单里的**每一条**保障短名,值是这次运行里它动手了几次(非负整数,没动手写 0);保障写"无"时写 `{}` | +| `safeguard-triggers` | 对象 | 键是冻结协议保障清单里的**每一条**保障短名,值是这次运行里它动手了几次(非负整数,没动手写 0);保障清单为空时写 `{}` | - 这两个键**不是指标**,不参与成绩判定;写错、没写都不作废运行,宿主照实记"候选没写答案路径" 或"不认识的路径:原文"。但研究者下一阶段就靠它看"这一格量到的是因子还是保障",所以要如实写。 diff --git a/tests/agents.test.ts b/tests/agents.test.ts index c4b7be1..a712afb 100644 --- a/tests/agents.test.ts +++ b/tests/agents.test.ts @@ -223,27 +223,27 @@ test("读代码:目录里的文件和目录读得到;目录外的读不到 symlinkSync(secret, join(root, "link-dir")); symlinkSync(join(secret, "proposal.md"), join(root, "link-file")); - assert.equal(readSource([root], join(root, "src", "a.py"), 1), "line1\nline2\n"); - assert.equal(readSource([root], root, 1), "link-dir\nlink-file\nsrc/"); + assert.equal(readSource([root], join(root, "src", "a.py")), "line1\nline2\n"); + assert.equal(readSource([root], root), "link-dir\nlink-file\nsrc/"); for (const path of [base, secret, join(secret, "proposal.md"), join(root, "..", "other-researcher", "proposal.md"), join(root, "link-dir"), join(root, "link-file"), join(root, "link-dir", "proposal.md")]) { - assert.throws(() => readSource([root], path, 1), /不在可读的目录里/, path); + assert.throws(() => readSource([root], path), /不在可读的目录里/, path); } - assert.throws(() => readSource([root], "src/a.py", 1), /只接受绝对路径/); + assert.throws(() => readSource([root], "src/a.py"), /只接受绝对路径/); // 前缀相同但不是子目录的也不算在里面。 mkdirSync(`${root}-copy`); - assert.throws(() => readSource([root], `${root}-copy`, 1), /不在可读的目录里/); + assert.throws(() => readSource([root], `${root}-copy`), /不在可读的目录里/); }); -test("读代码:超过单次上限时分段,并写明从第几行接着读", (t) => { +test("读代码:超过单次上限时只显示前 2000 行,并写明截断", (t) => { const root = realpathSync(tempDir(t, "page")); const lines = Array.from({ length: 2500 }, (_, i) => `第 ${i + 1} 行`); writeFileSync(join(root, "big.txt"), lines.join("\n")); - const first = readSource([root], join(root, "big.txt"), 1); - assert.ok(first.startsWith("第 1 行\n")); - assert.match(first, /本次显示第 1–2000 行[\s\S]*用 offset=2001 再调一次/); - const second = readSource([root], join(root, "big.txt"), 2001); - assert.match(second, /^第 2001 行\n/); - assert.match(second, /本次显示第 2001–2500 行[\s\S]*已读到结尾/); + const shown = readSource([root], join(root, "big.txt")); + assert.ok(shown.startsWith("第 1 行\n")); + assert.ok(shown.includes("第 2000 行\n")); + assert.ok(!shown.includes("第 2001 行")); + assert.match(shown, /截断:只显示了前 2000 行[\s\S]*共 2500 行/); + assert.ok(!shown.includes("offset")); }); // --------------------------------------------------------------------------- diff --git a/tests/jev-real.ts b/tests/jev-real.ts index 8cb27f4..241de2c 100644 --- a/tests/jev-real.ts +++ b/tests/jev-real.ts @@ -1,8 +1,9 @@ import assert from "node:assert/strict"; -import { checkHunk, checkProtocol, judgeSameChange } from "../src/jev/index.ts"; +import { checkHunk, checkProtocol, codeHunks, judgeSameChange } from "../src/jev/index.ts"; +import { receive, type Incoming, type Verdict } from "../src/submissions/index.ts"; import type { JevJudgment } from "../src/vocab/index.ts"; -// Jev 的真实接线测试(任务书 08 第七部分),npm run test:jev -- <模型名>:要联网、要密钥 TYPESAFE_API_KEY,不放进 npm test。 +// Jev 的真实接线测试(任务书 08 第七部分;后半段是任务书 10 第六部分 submissions 走到 Jev 的三个用例),npm run test:jev -- <模型名>:要联网、要密钥 TYPESAFE_API_KEY,不放进 npm test。 // 三处各问一次真实 Jev,材料是随手写的简单填圆材料。只确认请求发得出去、九问都拿回 0 到 1 之间的概率、 // 请求编号读得到;不判断概率对不对。 @@ -22,7 +23,7 @@ const factor = { const all: Array<[string, JevJudgment]> = []; -const same = await judgeSameChange(model, [factor], [{ name: "外圈收紧", method: "construct_packing 中外圈 8 个圆所在环的半径从 0.7 改成 0.45。" }]); +const same = await judgeSameChange(model, [factor], [{ name: "外圈收紧", method: "construct_packing 中外圈 8 个圆所在环的半径从 0.7 改成 0.45。", keySettings: "环半径 0.45" }]); all.push(["判重", same]); const frozen = await checkProtocol(model, { @@ -70,3 +71,74 @@ for (const [, j] of all) { assert.equal(new Set(frozen.judgments.map((j) => j.requestId)).size, 3); assert.equal(new Set(hunk.judgments.map((j) => j.requestId)).size, 1); console.log("九问都拿回了 0 到 1 之间的概率和请求编号"); + +// --------------------------------------------------------------------------- +// submissions 走到 Jev 的路径(任务书 10 第六部分):打印结论和每问的概率。 +// --------------------------------------------------------------------------- + +const RULES = { factorLimit: 3, continuable: [], reusableMetrics: [], takenMetricIds: ["sum-of-radii", "answer-path", "safeguard-triggers"] }; +const original = { factors: [factor], safeguards: [], metrics: { defined: [], reused: [] }, body }; +const response = (changes: Extract["content"]["changes"]): Incoming => ({ + kind: "response", + content: { replies: [], changes, clauses: ["initial.py 的 construct_packing 里,外圈环半径常数 0.7 改为 0.45"], body: "无" }, + checklist: { kind: "response", candidate: "r1-c1", critiques: [], ...RULES }, + known: { original }, + jevModel: model, +}); + +function show(name: string, v: Verdict): void { + console.log(`\n== ${name}:${v.accepted ? "收下" : "退回"}`); + for (const j of v.jev ?? []) console.log(`${j.question}\t概率 ${j.probability}\t阈值 ${j.threshold}\t${j.yes ? "是" : "否"}\t请求编号 ${j.requestId}`); + if (!v.accepted) console.log(`退回原因:\n${v.reason}`); +} + +const good = await receive(response({})); +show("写得好的回应", good); +assert.equal(good.accepted, true, "写得好的回应应当收下"); +assert.equal(good.jev!.length, 3); +assert.deepEqual(good.accepted && good.protocol, { ...original, clauses: ["initial.py 的 construct_packing 里,外圈环半径常数 0.7 改为 0.45"] }); + +const cheating = await receive( + response({ + body: + body.replace("harness.py、validate.py 不改。", "harness.py 不改。") + + "\n\n### 评测\n\n把 validate.py 里判圆重叠的容差从 1e-9 放宽到 1e-3,让轻微重叠的摆法也算合法。", + }), +); +show("把评测改掉的回应", cheating); +assert.equal(cheating.accepted, false, "改评测的回应应当退回"); +assert.match((cheating as Extract).reason, /changes_measurement 概率 [0-9.e-]+(阈值 [0-9.]+)/); + +const diff = + "diff --git a/initial.py b/initial.py\n--- a/initial.py\n+++ b/initial.py\n" + + "@@ -33,7 +33,7 @@ def construct_packing():\n" + + " for i in range(8):\n" + + " angle = 2 * np.pi * i / 8\n" + + "- centers[i + 1] = [0.5 + 0.7 * np.cos(angle), 0.5 + 0.7 * np.sin(angle)]\n" + + "+ centers[i + 1] = [0.5 + 0.45 * np.cos(angle), 0.5 + 0.45 * np.sin(angle)]\n" + + " \n" + + " for i in range(16):\n" + + " angle = 2 * np.pi * i / 16\n" + + "@@ -60,4 +60,4 @@ def construct_packing():\n" + + " radii = compute_max_radii(centers)\n" + + "- # 外圈环半径 0.7\n" + + "+ # 外圈环半径 0.45\n" + + " return centers, radii\n" + + "diff --git a/notes.md b/notes.md\nnew file mode 100644\n--- /dev/null\n+++ b/notes.md\n" + + "@@ -0,0 +1,2 @@\n" + + "+# 改动说明\n" + + "+外圈环半径从 0.7 改为 0.45。\n"; +const hunks = codeHunks({ files: [{ path: "initial.py", change: "modified" }, { path: "notes.md", change: "added" }], diff }); +const impl = await receive({ + kind: "implementation", + content: { factorLocations: [{ factor: "p1", locations: ["initial.py:36"] }], safeguardLocations: [], unassigned: [{ location: "notes.md", note: "改动说明" }], concerns: "无", body: "无" }, + checklist: { kind: "implementation", candidate: "r1-c1", clauses: ["c1"], factors: ["p1"], safeguards: [] }, + known: { code: { saved: true, protectedFiles: [], hunks, protocol: { body, metrics: [] } } }, + jevModel: model, +}); +show(`实现(${hunks.length} 个差异块)`, impl); +assert.equal(hunks.length, 3); +// 每块问一次(一次请求五问):请求数等于块数,判断条数是块数的五倍。 +assert.equal(new Set(impl.jev!.map((j) => j.requestId)).size, hunks.length); +assert.equal(impl.jev!.length, hunks.length * 5); +console.log("\nsubmissions 的三个真实用例都按预期"); diff --git a/tests/jev.test.ts b/tests/jev.test.ts index bc9342d..68ce6b9 100644 --- a/tests/jev.test.ts +++ b/tests/jev.test.ts @@ -231,11 +231,11 @@ test("材料:给定带类型的输入,发出去的材料与手写预期一 const states = sent.map((s) => s.body.state); const [dedup, freeze, , , diff] = states; for (const state of states) assert.equal(typeof state.task, "string"); - // 判重只放名称和做法:关键设定、关掉时换成什么、延续哪个因子都不放。 + // 判重放名称、做法和关键设定:关掉时换成什么、延续哪个因子不放。 assert.deepEqual(dedup, { task: dedup.task, - proposal_a: { factors: [{ name: "外圈环半径缩到 0.45", method: "把 construct_packing 里外圈环半径常数 0.7 改为 0.45。" }] }, - proposal_b: { factors: [{ name: "六行六边形网格", method: "用 6 行六边形网格摆圆心。" }] }, + proposal_a: { factors: [{ name: "外圈环半径缩到 0.45", method: "把 construct_packing 里外圈环半径常数 0.7 改为 0.45。", keySettings: "环半径 0.45" }] }, + proposal_b: { factors: [{ name: "六行六边形网格", method: "用 6 行六边形网格摆圆心。", keySettings: "6 行" }] }, }); assert.deepEqual(freeze, { task: freeze.task, diff --git a/tests/ledger-store.test.ts b/tests/ledger-store.test.ts index 4987488..9ef4a56 100644 --- a/tests/ledger-store.test.ts +++ b/tests/ledger-store.test.ts @@ -220,15 +220,16 @@ test("退回:被退回的内容原样存;程序检查退回的 jev 为 null judgment("self_contradictory", 0.02, 0.6, "req-2"), judgment("too_vague", 0.1, 0.7, "req-3"), ]; - ledger.append("rejection", { stageId: "s1", author: "r1", kind: "proposal", submitted: malformed, reason: "因子清单格式不对", jev: null }); + ledger.append("rejection", { stageId: "s1", author: "r1", kind: "proposal", target: null, submitted: malformed, reason: "因子清单格式不对", jev: null }); ledger.append("rejection", { - stageId: "s6", author: "r2", kind: "response", submitted: { body: "改了评测" }, reason: "冻结条款改动了评测", jev: clauses, + stageId: "s6", author: "r2", kind: "response", target: "r1-c1", submitted: { body: "改了评测" }, reason: "冻结条款改动了评测", jev: clauses, }); ledger.close(); const reopened = Ledger.open(path); t.after(() => reopened.close()); const rejections = reopened.records().filter((r) => r.kind === "rejection"); assert.deepEqual(rejections.map((r) => r.content.submitted), [malformed, { body: "改了评测" }]); + assert.deepEqual(rejections.map((r) => r.content.target), [null, "r1-c1"]); assert.equal(rejections[0]!.content.jev, null); assert.deepEqual(rejections[1]!.content.jev, [ { question: "changes_measurement", questionHash: "ab".repeat(32), threshold: 0.5, probability: 0.97, yes: true, requestId: "req-1" }, @@ -237,6 +238,15 @@ test("退回:被退回的内容原样存;程序检查退回的 jev 为 null ]); }); +test("退回:对象字段必填,没有对象写 null;不写就拒收", (t) => { + const { ledger, path } = freshLedger(t); + assert.throws( + () => ledger.append("rejection", { stageId: "s1", author: "r1", kind: "proposal", submitted: {}, reason: "缺对象", jev: null } as never), + /账本记录"rejection"的内容不符合定义/, + ); + assert.equal(countRows(path), 0); +}); + test("退回:jev 不合 JevJudgment 的拒收,一条都不写", (t) => { const { ledger, path } = freshLedger(t); const good = judgment("same_change", 0.9, 0.5, "req-1"); @@ -256,7 +266,7 @@ test("退回:jev 不合 JevJudgment 的拒收,一条都不写", (t) => { ]; for (const jev of bad) { assert.throws( - () => ledger.append("rejection", { stageId: "s1", author: "r1", kind: "proposal", submitted: {}, reason: "判为同一个改法", jev } as never), + () => ledger.append("rejection", { stageId: "s1", author: "r1", kind: "proposal", target: null, submitted: {}, reason: "判为同一个改法", jev } as never), /账本记录"rejection"的内容不符合定义/, JSON.stringify(jev), ); @@ -436,12 +446,12 @@ test("打开:表结构版本不对报错;不是空文件也不是账本的 db.close(); assert.throws(() => Ledger.open(wrongVersion), /版本是 99/); - // 版本 2 是"模型调用结束"还没有墙钟时间、退回还没有 Jev 判断数组时的账本,不迁移,打开报错。 - const previous = join(dir, "v2.db"); - const v2 = new DatabaseSync(previous); - v2.exec("PRAGMA user_version = 2"); - v2.close(); - assert.throws(() => Ledger.open(previous), /版本是 2.*只认版本 3/s); + // 版本 3 是退回还没有对象字段时的账本,不迁移,打开报错。 + const previous = join(dir, "v3.db"); + const v3 = new DatabaseSync(previous); + v3.exec("PRAGMA user_version = 3"); + v3.close(); + assert.throws(() => Ledger.open(previous), /版本是 3.*只认版本 4/s); const foreign = join(dir, "foreign.db"); const other = new DatabaseSync(foreign); diff --git a/tests/materials-belief.test.ts b/tests/materials-belief.test.ts index bf73950..24a6a84 100644 --- a/tests/materials-belief.test.ts +++ b/tests/materials-belief.test.ts @@ -3,7 +3,7 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { test } from "node:test"; import { Ledger } from "../src/ledger/index.ts"; -import { beliefBook, queryMaterial } from "../src/materials/index.ts"; +import { beliefBook, cellNotRunEvent, queryMaterial } from "../src/materials/index.ts"; import { adopt, selectBestExpected, settleAblation, type BatchInput, type SettleInput } from "../src/rules/index.ts"; import { tempDir } from "./ledger-helpers.ts"; import { batch, judged, material, NONE, P_A1, P_A2, protocolOf, r, study, studyUpTo, TABLE_C1, Writer } from "./materials-helpers.ts"; @@ -31,8 +31,8 @@ const adoptInput = (baseline: number, candidates: Array<{ id: string; score: num function roundOneNoAdoption(t: import("node:test").TestContext, before: (w: Writer) => void): Writer { const w = fresh(t); before(w); - w.protocol("r1-c1", protocolOf(P_A1, 1, "base", ["r1-a1-1"], [], ["c"])); - w.protocol("r1-c2", protocolOf(P_A2, 1, "base", ["r1-a2-1"], [], ["c"])); + w.protocol("r1-c1", protocolOf(P_A1, 1, "base", ["r1-a1-1"], ["c"])); + w.protocol("r1-c2", protocolOf(P_A2, 1, "base", ["r1-a2-1"], ["c"])); const b1 = batch(1, 0.96, { "r1-c1": { a1: r(1.0, 1.1, 1.2) }, "r1-c2": { a1: r(0.95, 0.97, 0.99) } }); assert.deepEqual(b1.output.chosen, ["r1-c1"]); w.decision("batch", 1, b1.input, b1.output); @@ -79,6 +79,12 @@ test("第二次预测:留池候选按 carried 判,不取它上一轮的第 w.open("s1-fp", "firstPrediction", 1, ["a1"]); w.sub("s1-fp", "a1", "firstPrediction", "r1-c2", null, { range: r(4.441, 4.442, 4.443), body: "上一轮的第一次" }); w.close("s1-fp"); + w.open("s1-crit", "critique", 1, ["a1"]); + w.sub("s1-crit", "a1", "critique", "r1-a1-2", "r1-a1-2.a1.1", { target: "p1", body: "第 1 轮对乙方案的挑刺" }); + w.close("s1-crit"); + w.open("s1-resp", "response", 1, ["a1"]); + w.sub("s1-resp", "a1", "response", "r1-c2", null, { replies: [{ critique: "r1-a1-2.a1.1", reply: "第 1 轮的答复" }], changes: {}, clauses: ["c"], body: "第 1 轮的答复正文" }); + w.close("s1-resp"); }); w.open("s2-prop", "proposal", 2, ["a1"]); w.close("s2-prop"); @@ -87,6 +93,8 @@ test("第二次预测:留池候选按 carried 判,不取它上一轮的第 const m = material(w.l, "secondPrediction", "a1", "s2-sp", "r1-c2", NONE); assert.ok(m.firstMessage.includes("(这是留池候选,本轮没有第一次预测)")); assert.ok(!m.firstMessage.includes("4.442")); + // 留池候选沿用原来的冻结协议和回应(4.3 ①):答复从它第 1 轮的回应读。 + for (const x of ["第 1 轮对乙方案的挑刺", "作者的答复:第 1 轮的答复", "第 1 轮的答复正文"]) assert.ok(m.firstMessage.includes(x), x); }); test("离池:基线变了、从没入选的旧候选列在离开候选池里,不再写留池", (t) => { @@ -116,7 +124,7 @@ test("信念本的数按条文手算:少多少分、撑几成、世界比特 }; // 这里只要 c3 入选并跑过:选批记录的其余内容照规则算出,chosen 写成 c3。 w.decision("batch", 1, input, { ...selectBestExpected(input), chosen: ["r1-v1-c3"] }); - w.protocol("r1-v1-c3", protocolOf(P_A1, 1, "base", ["r1-a1-1"], [], ["c"])); + w.protocol("r1-v1-c3", protocolOf(P_A1, 1, "base", ["r1-a1-1"], ["c"])); w.l.append("code", { object: "r1-v1-c3", version: "v-c3", locations: TABLE_C1 }); w.run("run-1", "forward", 1, "r1-v1-c3", judged(full, "main")); const a = adoptInput(b, [{ id: "r1-v1-c3", score: full }]); @@ -183,3 +191,52 @@ test("解释的现在交什么写明每人最多 M 条", (t) => { const m = material(studyUpTo(t, "explOpen1"), "explanation", "a1", "s1-expl", null, { changes: null, artifacts: {} }); assert.ok(m.firstMessage.slice(m.firstMessage.lastIndexOf("# 现在交什么")).includes("最多 2 条")); }); + +/** 第 1 轮冻结 r1-c1(两个因子)、选中、跑出 run1Judged、采纳。 */ +function roundOneAdopted(t: import("node:test").TestContext, run1Judged: ReturnType): Writer { + const w = fresh(t); + w.protocol("r1-c1", protocolOf(P_A1, 1, "base", ["r1-a1-1"], ["c"])); + const b1 = batch(1, 0.96, { "r1-c1": { a1: r(1.0, 1.1, 1.2) } }); + w.decision("batch", 1, b1.input, b1.output); + w.l.append("code", { object: "r1-c1", version: "v-c1", locations: TABLE_C1 }); + w.run("run-1", "forward", 1, "r1-c1", run1Judged); + const a1 = adoptInput(0.96, [{ id: "r1-c1", score: 1.2 }]); + w.decision("adopt", 1, a1, adopt(a1)); + return w; +} + +test("选批历史的每次运行后面原样写出任务的附加记录(合法化扣掉的分,方法.md 6.10 第 3 条)", (t) => { + const extra = { legalization: { applied: true, deduction: { status: "measured", value: 0.0012 } } }; + const w = roundOneAdopted(t, { ...judged(1.2, "main"), extra }); + const book = beliefBook(w.l, study, 1); + assert.ok(book.includes('任务的附加记录(含合法化,原样):{"legalization":{"applied":true,"deduction":{"status":"measured","value":0.0012}}}'), book); +}); + +test("消融格因核验不通过、重做次数用完而不跑:没有运行,证据不足的原因从事件读,信念本照样写得出", (t) => { + const w = roundOneAdopted(t, judged(1.2, "main")); + // p1 跑了,答案出自被关掉的 p1 本身:只照实写答案出处,不另加"本该关着"。p2 不跑。 + w.l.append("code", { object: "r1-c1-p1", version: "v-c1-p1", locations: null }); + w.run("run-2", "ablation", 1, "r1-c1-p1", judged(1.08, "p1")); + w.l.append("event", cellNotRunEvent("r1-c1-p2", "消融核验不通过,修复次数用完")); + const settle: SettleInput = { + baseline: 0.96, + full: 1.2, + direction: "maximize", + epsilon: 0.01, + knownBest: 2.635983, + lambda: 0.1, + factors: ["p1", "p2"], + cells: [ + { factor: "p1", outcome: { kind: "valid", score: 1.08 } }, + { factor: "p2", outcome: { kind: "insufficient" } }, + ], + explanations: [{ id: "r1-a1-x1", author: "a1", cells: [{ factor: "p1", range: r(1.0, 1.1, 1.15) }, { factor: "p2", range: r(1.1, 1.15, 1.19) }] }], + }; + w.l.append("settlement", { round: 1, result: { candidate: "r1-c1", input: settle, output: settleAblation(settle) } as never }); + const book = beliefBook(w.l, study, 2); + assert.ok(book.includes("- r1-c1-p2 停滞扰动(停滞扰动的设定):候选 r1-c1,基线 base;证据不足:消融核验不通过,修复次数用完;测前合并范围"), book); + // G = 1.2 − 0.96 = 0.24;关 p1 少 0.12,撑 0.5 → 50%。 + assert.ok(book.includes("关掉后 1.08,少 0.12 分,撑 50%"), book); + assert.ok(book.includes("答案出自 p1")); + assert.ok(!book.includes("本该关着")); +}); diff --git a/tests/materials-helpers.ts b/tests/materials-helpers.ts index 5bced21..3bc280c 100644 --- a/tests/materials-helpers.ts +++ b/tests/materials-helpers.ts @@ -121,8 +121,8 @@ export class Writer { } } -export function protocolOf(p: Submission<"proposal">, round: number, baseline: string, proposals: string[], replies: FrozenProtocol["replies"], clauses: string[]): FrozenProtocol { - return { factors: p.factors, safeguards: p.safeguards, metrics: p.metrics, body: p.body, clauses, replies, round, baseline, proposals, replyBody: "无" }; +export function protocolOf(p: Submission<"proposal">, round: number, baseline: string, proposals: string[], clauses: string[]): FrozenProtocol { + return { factors: p.factors, safeguards: p.safeguards, metrics: p.metrics, body: p.body, clauses, round, baseline, proposals }; } export function batch(round: number, baseline: number, ranges: Record>): { input: BatchInput; output: ReturnType } { @@ -192,8 +192,8 @@ const STEPS: Array<[string, (w: Writer, s: Record) => void]> = [ w.sub("s1-resp", "a1", "response", "r1-c1", null, { replies: [{ critique: CRITIQUE_ID, reply: "甲答复:保留扰动" }], changes: {}, clauses: ["加多起点", "加扰动"], body: "无" }); w.sub("s1-resp", "a2", "response", "r1-c2", null, { replies: [], changes: {}, clauses: ["加修复"], body: "无" }); w.close("s1-resp"); - w.protocol("r1-c1", protocolOf(P_A1, 1, "base", ["r1-a1-1"], [{ critique: CRITIQUE_ID, reply: "甲答复:保留扰动" }], ["加多起点", "加扰动"])); - w.protocol("r1-c2", protocolOf(P_A2, 1, "base", ["r1-a2-1"], [], ["加修复"])); + w.protocol("r1-c1", protocolOf(P_A1, 1, "base", ["r1-a1-1"], ["加多起点", "加扰动"])); + w.protocol("r1-c2", protocolOf(P_A2, 1, "base", ["r1-a2-1"], ["加修复"])); }], ["secondOpen", (w) => w.open("s1-sp", "secondPrediction", 1, ["a1", "a2"])], ["second", (w) => { @@ -273,8 +273,8 @@ const STEPS: Array<[string, (w: Writer, s: Record) => void]> = [ w.sub("s2-resp", "a1", "response", "r2-c1", null, { replies: [], changes: {}, clauses: ["加局部搜索"], body: "无" }); w.sub("s2-resp", "a2", "response", "r2-c2", null, { replies: [], changes: {}, clauses: ["改修复", "加早停"], body: "无" }); w.close("s2-resp"); - w.protocol("r2-c1", protocolOf({ ...P_A1, metrics: { defined: [], reused: ["restart-count"] }, factors: [factor("新的局部搜索", "r1-c1-p1"), factor("更多起点", null)], body: "甲的第二轮提案" }, 2, "r1-c1", ["r2-a1-1"], [], ["加局部搜索"])); - w.protocol("r2-c2", protocolOf({ ...P_A2, factors: [factor("更细的修复", null), factor("早停", null)], body: "乙的第二轮提案" }, 2, "r1-c1", ["r2-a2-1"], [], ["改修复", "加早停"])); + w.protocol("r2-c1", protocolOf({ ...P_A1, metrics: { defined: [], reused: ["restart-count"] }, factors: [factor("新的局部搜索", "r1-c1-p1"), factor("更多起点", null)], body: "甲的第二轮提案" }, 2, "r1-c1", ["r2-a1-1"], ["加局部搜索"])); + w.protocol("r2-c2", protocolOf({ ...P_A2, factors: [factor("更细的修复", null), factor("早停", null)], body: "乙的第二轮提案" }, 2, "r1-c1", ["r2-a2-1"], ["改修复", "加早停"])); w.open("s2-sp", "secondPrediction", 2, ["a1", "a2"]); const ranges = { "r2-c1": { a1: r(1.2, 1.24, 1.3), a2: r(1.19, 1.22, 1.27) }, "r2-c2": { a1: r(1.18, 1.2, 1.22), a2: r(1.17, 1.2, 1.22) } }; for (const [c, rs] of Object.entries(ranges)) for (const [a, range] of Object.entries(rs)) w.sub("s2-sp", a, "secondPrediction", c, null, { range, body: "理由" }); diff --git a/tests/materials.test.ts b/tests/materials.test.ts index 2b7aae8..59530ca 100644 --- a/tests/materials.test.ts +++ b/tests/materials.test.ts @@ -2,10 +2,10 @@ import assert from "node:assert/strict"; import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { test } from "node:test"; -import { beliefBook, callId, queryMaterial, readPrompts, type TaskMaterial } from "../src/materials/index.ts"; +import { beliefBook, queryMaterial, readPrompts, type TaskMaterial } from "../src/materials/index.ts"; import { SUBMISSION_KINDS, type SubmissionKind } from "../src/vocab/index.ts"; import { tempDir } from "./ledger-helpers.ts"; -import { CHANGES_C1, CRITIQUE_ID, everything, material, NONE, study, studyUpTo } from "./materials-helpers.ts"; +import { CHANGES_C1, CRITIQUE_ID, everything, material, NONE, study, studyUpTo, TABLE_C1 } from "./materials-helpers.ts"; // 每种任务至少一个测试:在真实账本里写到某一步,生成材料,查该有的有、不该有的没有、对号清单对。 // 预期值按方法.md 条文手写(研究进程见 materials-helpers.ts 的注释),不从实现推。 @@ -63,7 +63,7 @@ test("第 1 轮提案:你认领的入口放在最前;没有上一轮;对 factorLimit: 3, continuable: [], reusableMetrics: [], - takenMetricIds: ["sum-of-radii", "wall-seconds"], + takenMetricIds: ["sum-of-radii", "wall-seconds", "answer-path", "safeguard-triggers"], }); const h = headings(m); assert.ok(h.indexOf("你自己的") < h.indexOf("公共材料") && h.indexOf("公共材料") < h.indexOf("信念本"), "先你自己的、后公共材料"); @@ -82,7 +82,7 @@ test("第 2 轮提案:上一轮全部收尾、上一轮实验员的提醒、 factorLimit: 3, continuable: ["r1-c1-p1", "r1-c1-p2", "r1-c2-p1"], reusableMetrics: ["restart-count"], - takenMetricIds: ["sum-of-radii", "wall-seconds", "restart-count"], + takenMetricIds: ["sum-of-radii", "wall-seconds", "answer-path", "safeguard-triggers", "restart-count"], }); for (const x of ["上一轮的收尾", "上一轮实验员实现时的提醒", '可以写进"延续"的因子', "已登记指标清单", "研究记录", "信念本"]) { assert.ok(headings(m).includes(x), `应有一节"${x}"`); @@ -109,6 +109,13 @@ test("挑刺:按原提案挑,对号清单是那条提案和它的因子", (t lacks(m, "6.661"); }); +test("挑刺的现在交什么:可以交一份或几份,每份调用一次;不写调用一次就完成", (t) => { + const m = material(studyUpTo(t, "critiqueOpen"), "critique", "a2", "s1-crit", "r1-a1-1", NONE); + const end = m.firstMessage.slice(m.firstMessage.lastIndexOf("# 现在交什么")); + assert.ok(end.includes("每调用一次 research_submit 交一份挑刺;可以交一份或几份"), end); + assert.ok(!end.includes("交上就完成了"), end); +}); + test("回应:本人原提案和针对它的全部挑刺", (t) => { const l = studyUpTo(t, "responseOpen"); const m = material(l, "response", "a1", "s1-resp", "r1-c1", NONE); @@ -120,7 +127,7 @@ test("回应:本人原提案和针对它的全部挑刺", (t) => { factorLimit: 3, continuable: [], reusableMetrics: [], - takenMetricIds: ["sum-of-radii", "wall-seconds"], + takenMetricIds: ["sum-of-radii", "wall-seconds", "answer-path", "safeguard-triggers"], }); }); @@ -139,11 +146,17 @@ test("第二次预测:冻结协议、挑刺与答复配对、本人第一次 has(m, "6.661 / 6.662 / 6.663"); has(m, "乙的挑刺:扰动可能混杂(推测)"); has(m, "作者的答复:甲答复:保留扰动"); + has(m, "### 答复正文"); lacks(m, "5.551"); lacks(m, "甲第一次理由"); lacks(m, "7.772"); }); +test("新登记指标不能用的编号里有答案路径、保障动手次数两个保留键(用户 2026-10-03 定)", (t) => { + const c = material(studyUpTo(t, "proposalOpen"), "proposal", "a1", "s1-prop", null, NONE).checklist as Extract; + assert.ok(c.takenMetricIds.includes("answer-path") && c.takenMetricIds.includes("safeguard-triggers"), JSON.stringify(c.takenMetricIds)); +}); + test("解释:本轮运行、提醒、要给三个数的格、产物只进查询;看不到别人的第一次预测", (t) => { const l = studyUpTo(t, "explOpen1"); const m = material(l, "explanation", "a2", "s1-expl", null, { changes: null, artifacts: { "run-1": { "stdout.log": "日志正文" } } }); @@ -229,6 +242,35 @@ test("核验:没有归属的改动排第一;因子与保障同行只作提 assert.ok(!headings(m).includes("信念本")); }); +test("核验材料里没有 Jev 的概率和阈值:收下实现时用过 Jev,也不给核验员看", (t) => { + const l = studyUpTo(t, "implOpen"); + const jev = [{ question: "early_exit", questionHash: "ab".repeat(32), threshold: 0.6312, probability: 0.4321, yes: false, requestId: "req-1" }]; + l.append("submission", { stageId: "s1-impl-c1", author: "x1", kind: "implementation", target: "r1-c1", item: null, version: 1, jev, content: { ...TABLE_C1, concerns: "无", body: "实现说明" } }); + l.append("stageClose", { stageId: "s1-impl-c1" }); + l.append("code", { object: "r1-c1", version: "v-c1", locations: TABLE_C1 }); + l.append("stageOpen", { stageId: "s1-ver-c1", stage: "verification", round: 1, roster: ["v1"] }); + const m = material(l, "verification", "v1", "s1-ver-c1", "r1-c1", { changes: CHANGES_C1, artifacts: null }); + has(m, "实现说明"); + for (const x of ["0.4321", "0.6312", "early_exit", "预扫"]) lacks(m, x); +}); + +test("消融实现重做:材料放上一次消融核验关错的因子和依据;第一次做时没有退回意见", (t) => { + assert.ok(!everything(material(studyUpTo(t, "ablationImplOpen"), "ablationImplementation", "x1", "s1-ai", "r1-c1-p1", NONE)).includes("### 退回意见")); + const l = studyUpTo(t, "ablationImpl"); + // 这一格要关 p1、照留 p2;核验员看到 p1 还开着、p2 照留:只有 p1 关错了。 + l.append("submission", { + stageId: "s1-av", author: "v1", kind: "ablationVerification", target: "r1-c1-p1", item: null, version: 1, jev: null, + content: { factors: [{ factor: "p1", off: false, where: "solve.py:12 仍在跑多起点循环" }, { factor: "p2", off: false, where: "solve.py:30 照留" }] }, + }); + l.append("stageClose", { stageId: "s1-av" }); + l.append("stageOpen", { stageId: "s1-ai2", stage: "ablationImplementation", round: 1, roster: ["x1"] }); + const m = material(l, "ablationImplementation", "x1", "s1-ai2", "r1-c1-p1", NONE); + has(m, "### 退回意见"); + has(m, "- 因子 p1 应当关着,核验员看到的是照留:solve.py:12 仍在跑多起点循环"); + lacks(m, "solve.py:30 照留"); + assert.deepEqual(m.checklist, { kind: "ablationImplementation", cell: "r1-c1-p1", off: "p1", factors: ["p1", "p2"] }); +}); + test("消融实现:这一格、因子清单、完整代码的位置表;看不到运行成绩", (t) => { const l = studyUpTo(t, "ablationImplOpen"); const m = material(l, "ablationImplementation", "x1", "s1-ai", "r1-c1-p1", NONE); @@ -305,15 +347,32 @@ test("可见性 5:第零轮闭门写入口时看不到别人的入口", (t) => test("可见性 6:别人的退回不放进材料;本人重新发起时放上一次被退回的原因", (t) => { const l = studyUpTo(t, "secondOpen"); - const call = callId("s1-sp", "a1", "secondPrediction", "r1-c1", 1); - l.append("callStarted", { call, actor: "a1", stageId: "s1-sp", model: "glm-5.3", materials: null }); - l.append("rejection", { stageId: "s1-sp", author: "a1", kind: "secondPrediction", submitted: { range: { q1: 3, q5: 2, q9: 1 } }, reason: "甲被退回:三个数不递增", jev: null }); - l.append("callEnded", { call, wallSeconds: { status: "measured", value: 3 }, status: "notSubmitted", reason: "没交成" }); + const reject = (author: string, target: string, reason: string) => + l.append("rejection", { stageId: "s1-sp", author, kind: "secondPrediction", target, submitted: { range: { q1: 3, q5: 2, q9: 1 } }, reason, jev: null }); + reject("a1", "r1-c1", "甲被退回:三个数不递增"); + reject("a2", "r1-c1", "乙被退回"); const retry = material(l, "secondPrediction", "a1", "s1-sp", "r1-c1", NONE); assert.ok(retry.firstMessage.includes("# 上一次为什么没交成")); has(retry, "甲被退回:三个数不递增"); - lacks(material(l, "secondPrediction", "a1", "s1-sp", "r1-c2", NONE), "甲被退回"); - lacks(material(l, "secondPrediction", "a2", "s1-sp", "r1-c1", NONE), "甲被退回"); + lacks(retry, "乙被退回"); + lacks(material(l, "secondPrediction", "a2", "s1-sp", "r1-c2", NONE), "甲被退回"); +}); + +test("退回按对象找:本人同一阶段对两个候选各被退回一次,重新发起时只放这个对象的那一条", (t) => { + // 两个候选的第二次预测并行,退回交错写入;不靠模型调用的记录定位。 + const l = studyUpTo(t, "secondOpen"); + const reject = (target: string, reason: string) => + l.append("rejection", { stageId: "s1-sp", author: "a1", kind: "secondPrediction", target, submitted: {}, reason, jev: null }); + reject("r1-c1", "c1 第一次被退回"); + reject("r1-c2", "c2 被退回"); + reject("r1-c1", "c1 第二次被退回"); + const c1 = material(l, "secondPrediction", "a1", "s1-sp", "r1-c1", NONE); + has(c1, "c1 第二次被退回"); + lacks(c1, "c1 第一次被退回"); + lacks(c1, "c2 被退回"); + const c2 = material(l, "secondPrediction", "a1", "s1-sp", "r1-c2", NONE); + has(c2, "c2 被退回"); + lacks(c2, "c1 第"); }); test("可见性 7:信念本只写已公开的记录:正向运行在采纳决定写入后才出现", (t) => { diff --git a/tests/submissions.test.ts b/tests/submissions.test.ts new file mode 100644 index 0000000..cc5c857 --- /dev/null +++ b/tests/submissions.test.ts @@ -0,0 +1,436 @@ +import assert from "node:assert/strict"; +import { test, type TestContext } from "node:test"; +import { + ablationOutcome, + protocolBody, + receive, + verificationOutcome, + type Incoming, + type OriginalProposal, + type Verdict, +} from "../src/submissions/index.ts"; +import type { ChecklistOf, Submission } from "../src/vocab/index.ts"; + +// submissions 的不花钱测试(任务书 10 第六部分)。收下的例子用不问 Jev 的种类,或者在程序检查那一步就退回; +// 退回原因的预期文字按任务书第四部分的写法手写。走到真实 Jev 的路径在 tests/jev-real.ts。 +// 只有"差异块退回文字"一条用假 Jev:把全局 fetch 换成假的,在测试自己的环境里造回答(同 tests/jev.test.ts)。 + +const MODEL = "jev-test"; +const r = (q1: number, q5: number, q9: number) => ({ q1, q5, q9 }); +const factor = (name: string, continues: string | null) => ({ name, method: `${name}的做法`, keySettings: "无", continues }); +const metric = (id: string) => ({ id, meaning: "次数", unit: "次", direction: "maximize" as const, howMeasured: "数", missing: "recordMissing" as const }); + +const RULES = { factorLimit: 2, continuable: ["r1-c1-p1", "r1-c1-p2"], reusableMetrics: ["restart-count"], takenMetricIds: ["sum-of-radii", "wall-seconds", "answer-path", "safeguard-triggers", "restart-count"] }; + +const PROPOSAL: Submission<"proposal"> = { + factors: [factor("多起点", null), factor("扰动", "r1-c1-p2")], + safeguards: ["guard"], + metrics: { defined: [metric("kick-count")], reused: ["restart-count"] }, + authorEstimate: r(1.1, 1.2, 1.3), + body: "提案正文", +}; +const PROPOSAL_LIST: ChecklistOf<"proposal"> = { kind: "proposal", maxProposals: 2, ...RULES }; + +/** 退回,并交回原因文字;顺带确认没问 Jev。 */ +async function rejected(inc: Incoming): Promise { + const v = await receive(inc); + assert.equal(v.accepted, false, "应当退回"); + assert.equal(v.jev, null, "程序检查就退回,不问 Jev"); + return (v as Extract).reason; +} + +const proposal = (content: Submission<"proposal">, accepted: number): Incoming => ({ kind: "proposal", content, checklist: PROPOSAL_LIST, known: { accepted }, jevModel: MODEL }); + +// --------------------------------------------------------------------------- +// 收下 +// --------------------------------------------------------------------------- + +test("都对的提案、预测、挑刺、志愿、解释、消融核验收下,不问 Jev,jev 与 protocol 为 null", async () => { + const cases: Incoming[] = [ + proposal(PROPOSAL, 1), + { kind: "firstPrediction", content: { range: r(1, 1.1, 1.2), body: "理由" }, checklist: { kind: "firstPrediction", candidate: "r2-c1" }, known: null, jevModel: MODEL }, + { kind: "critique", content: { target: "whole", body: "挑刺" }, checklist: { kind: "critique", proposal: "r2-a1-1", factors: ["p1"] }, known: null, jevModel: MODEL }, + { kind: "claim", content: { choices: ["e-a1-1", "e-a2-1"] }, checklist: { kind: "claim", entries: ["e-a1-1", "e-a2-1", "e-a2-2"], minChoices: 2 }, known: null, jevModel: MODEL }, + { + kind: "explanation", + content: { explanations: [{ body: "机制", cells: [{ factor: "p2", range: r(1, 1.1, 1.2) }, { factor: "p1", range: r(0.9, 1, 1.1) }] }] }, + checklist: { kind: "explanation", cells: ["p1", "p2"], maxExplanations: 2 }, + known: null, + jevModel: MODEL, + }, + { + kind: "ablationVerification", + content: { factors: [{ factor: "p1", off: true, where: "a.py:1" }, { factor: "p2", off: false, where: "a.py:9" }] }, + checklist: { kind: "ablationVerification", cell: "r1-c1-p1", factors: ["p1", "p2"] }, + known: null, + jevModel: MODEL, + }, + { kind: "wrapUp", content: { body: "收尾" }, checklist: { kind: "wrapUp" }, known: null, jevModel: MODEL }, + ]; + for (const inc of cases) assert.deepEqual(await receive(inc), { accepted: true, jev: null, protocol: null }, inc.kind); +}); + +test("消融实现:跑得起来、没改不许改的文件,收下;交跑不起来时不看代码", async () => { + const checklist: ChecklistOf<"ablationImplementation"> = { kind: "ablationImplementation", cell: "r1-c1-p1", off: "p1", factors: ["p1", "p2", "p3"] }; + const ok = await receive({ kind: "ablationImplementation", content: { alsoOff: ["p3"], cannotRun: null, concerns: "无" }, checklist, known: { code: { saved: true, protectedFiles: [] } }, jevModel: MODEL }); + assert.deepEqual(ok, { accepted: true, jev: null, protocol: null }); + const cannot = await receive({ kind: "ablationImplementation", content: { alsoOff: [], cannotRun: "关了 p1 程序起不来", concerns: "无" }, checklist, known: { code: null }, jevModel: MODEL }); + assert.deepEqual(cannot, { accepted: true, jev: null, protocol: null }); +}); + +test("实现:没有差异块(例如只改了可执行位)时 Jev 看不到内容,不问,jev 为 null", async () => { + const v = await receive(implementation(SAVED, TABLE)); + assert.deepEqual(v, { accepted: true, jev: null, protocol: null }); +}); + +// --------------------------------------------------------------------------- +// 程序检查:每条规则一个该退回的例子 +// --------------------------------------------------------------------------- + +test("规则 1:三个数要严格递增(作者自评、两次预测、解释每格)", async () => { + assert.match(await rejected(proposal({ ...PROPOSAL, authorEstimate: r(1.2, 1.2, 1.3) }, 0)), /authorEstimate 的三个数要严格递增 q1 < q5 < q9,你写的是 1.2 \/ 1.2 \/ 1.3/); + const reason = await rejected({ kind: "secondPrediction", content: { range: r(1.3, 1.2, 1.1), body: "理由" }, checklist: { kind: "secondPrediction", candidate: "r1-c1" }, known: null, jevModel: MODEL }); + assert.equal(reason, "这份提交没有收下,要改的地方:\n- range 的三个数要严格递增 q1 < q5 < q9,你写的是 1.3 / 1.2 / 1.1"); + const expl = await rejected({ + kind: "explanation", + content: { explanations: [{ body: "机制", cells: [{ factor: "p1", range: r(1, 1, 1) }] }] }, + checklist: { kind: "explanation", cells: ["p1"], maxExplanations: 1 }, + known: null, + jevModel: MODEL, + }); + assert.match(expl, /第 1 条解释 p1 格的 range 的三个数要严格递增/); +}); + +test("规则 2:因子数超过上限退回", async () => { + const reason = await rejected(proposal({ ...PROPOSAL, factors: [factor("a", null), factor("b", null), factor("c", null)] }, 0)); + assert.match(reason, /factors 有 3 个因子,超过上限 2 个/); +}); + +test("规则 3:两个因子不能延续同一个;延续的编号要在可延续清单里", async () => { + assert.match(await rejected(proposal({ ...PROPOSAL, factors: [factor("a", "r1-c1-p1"), factor("b", "r1-c1-p1")] }, 0)), /有两个以上因子的 continues 都写了 r1-c1-p1/); + const reason = await rejected(proposal({ ...PROPOSAL, factors: [factor("a", "r1-c9-p1")] }, 0)); + assert.match(reason, /continues 只能写 r1-c1-p1、r1-c1-p2;你写的 r1-c9-p1 不在里面;新因子写 null/); +}); + +test("规则 4:保障短名不能叫 main、不能写成因子编号、不能重复", async () => { + const reason = await rejected(proposal({ ...PROPOSAL, safeguards: ["main", "p3", "guard", "guard"] }, 0)); + assert.match(reason, /保障短名 main 不能用/); + assert.match(reason, /保障短名 p3 不能用/); + assert.match(reason, /safeguards 里 guard 写了不止一次/); +}); + +test("规则 5:每条解释都要交 cells 并恰好覆盖材料列出的格(用户 2026-10-03 定:只按全开实现)", async () => { + const checklist: ChecklistOf<"explanation"> = { kind: "explanation", cells: ["p1", "p2"], maxExplanations: 2 }; + const missing = await rejected({ kind: "explanation", content: { explanations: [{ body: "机制" }] }, checklist, known: null, jevModel: MODEL }); + assert.match(missing, /第 1 条解释没交 cells:每条解释都要对 p1、p2 每一格给三个数/); + const wrong = await rejected({ kind: "explanation", content: { explanations: [{ body: "机制", cells: [{ factor: "p1", range: r(1, 2, 3) }, { factor: "p3", range: r(1, 2, 3) }] }] }, checklist, known: null, jevModel: MODEL }); + assert.match(wrong, /第 1 条解释 cells 的格应当是 p1、p2;你写的 p3 不在里面;还缺 p2/); +}); + +test("规则 6:解释最多 M 条", async () => { + const one = { body: "机制", cells: [{ factor: "p1", range: r(1, 2, 3) }] }; + const reason = await rejected({ kind: "explanation", content: { explanations: [one, one] }, checklist: { kind: "explanation", cells: ["p1"], maxExplanations: 1 }, known: null, jevModel: MODEL }); + assert.match(reason, /交了 2 条解释,最多 1 条/); +}); + +test("规则 7:志愿个数够、不重复、都在入口清单里", async () => { + const checklist: ChecklistOf<"claim"> = { kind: "claim", entries: ["e-a1-1", "e-a2-1"], minChoices: 2 }; + assert.match(await rejected({ kind: "claim", content: { choices: ["e-a1-1"] }, checklist, known: null, jevModel: MODEL }), /choices 至少要排 2 个入口,你排了 1 个/); + const reason = await rejected({ kind: "claim", content: { choices: ["e-a1-1", "e-a1-1", "e-a3-1"] }, checklist, known: null, jevModel: MODEL }); + assert.match(reason, /choices 只能写 e-a1-1、e-a2-1;你写的 e-a3-1 不在里面/); + assert.match(reason, /choices 里 e-a1-1 写了不止一次/); +}); + +test("规则 8:挑刺针对的因子编号在被挑提案的因子范围内", async () => { + const reason = await rejected({ kind: "critique", content: { target: "p3", body: "挑刺" }, checklist: { kind: "critique", proposal: "r1-a1-1", factors: ["p1", "p2"] }, known: null, jevModel: MODEL }); + assert.match(reason, /target 只能写 whole、p1、p2;你写的 p3 不在里面/); +}); + +const ORIGINAL: OriginalProposal = { factors: PROPOSAL.factors, safeguards: PROPOSAL.safeguards, metrics: PROPOSAL.metrics, body: PROPOSAL.body }; +const RESPONSE_LIST: ChecklistOf<"response"> = { kind: "response", candidate: "r1-c1", critiques: ["r1-a1-1.a2.1", "r1-a1-1.a3.1"], ...RULES }; +const response = (content: Submission<"response">): Incoming => ({ kind: "response", content, checklist: RESPONSE_LIST, known: { original: ORIGINAL }, jevModel: MODEL }); + +test("规则 9a:回应的答复恰好覆盖全部挑刺;原因里列出该写的全部编号(参考项目提交 378fe68)", async () => { + const reason = await rejected(response({ replies: [{ critique: "r1-a1-1.a2.2", reply: "改了" }], changes: {}, clauses: ["c"], body: "无" })); + assert.equal(reason, "这份提交没有收下,要改的地方:\n- replies 的挑刺编号应当是 r1-a1-1.a2.1、r1-a1-1.a3.1;你写的 r1-a1-1.a2.2 不在里面;还缺 r1-a1-1.a2.1、r1-a1-1.a3.1"); +}); + +const UNIFICATION: Submission<"unification"> = { + ...ORIGINAL, + replies: [], + clauses: ["c1 条"], + sources: [{ proposal: "r1-a1-1", coverage: "全在" }], +}; +const unification = (content: Submission<"unification">): Incoming => ({ + kind: "unification", + content, + checklist: { kind: "unification", candidate: "r1-c1", critiques: [], sources: ["r1-a1-1", "r1-a2-1"], ...RULES }, + known: null, + jevModel: MODEL, +}); + +test("规则 9b:统一的 sources 恰好覆盖被合并的原提案;没有挑刺时说交空数组", async () => { + const reason = await rejected(unification({ ...UNIFICATION, replies: [{ critique: "x.a1.1", reply: "答" }] })); + assert.match(reason, /replies 的挑刺编号应当是 没有(交空数组);你写的 x.a1.1 不在里面/); + assert.match(reason, /sources 的原提案编号应当是 r1-a1-1、r1-a2-1;还缺 r1-a2-1/); +}); + +test("规则 2、3、4、10 对拼好的协议查:回应改出来的因子超过上限也退回", async () => { + const reason = await rejected(response({ replies: [{ critique: "r1-a1-1.a2.1", reply: "答" }, { critique: "r1-a1-1.a3.1", reply: "答" }], changes: { factors: [factor("a", null), factor("b", null), factor("c", null)] }, clauses: ["c"], body: "无" })); + assert.match(reason, /factors 有 3 个因子,超过上限 2 个/); +}); + +test("规则 2、3、4、10 对统一自己的字段查:因子超上限、保障叫 main、登记已占用的编号都退回", async () => { + const reason = await rejected(unification({ ...UNIFICATION, factors: [factor("a", null), factor("b", null), factor("c", null)], safeguards: ["main"], metrics: { defined: [metric("wall-seconds")], reused: [] }, sources: [{ proposal: "r1-a1-1", coverage: "全在" }, { proposal: "r1-a2-1", coverage: "全在" }] })); + assert.match(reason, /factors 有 3 个因子,超过上限 2 个/); + assert.match(reason, /main/); + assert.match(reason, /metrics.defined 的编号 wall-seconds 已被占用/); +}); + +test("规则 10:reused 要在可引用清单里;新登记的编号不重复,也不能和已占用的重名(含答案路径两个保留键)", async () => { + assert.match(await rejected(proposal({ ...PROPOSAL, metrics: { defined: [], reused: ["kick-count"] } }, 0)), /metrics.reused 只能写 restart-count;你写的 kick-count 不在里面/); + const reason = await rejected(proposal({ ...PROPOSAL, metrics: { defined: [metric("a"), metric("a"), metric("wall-seconds"), metric("answer-path")], reused: [] } }, 0)); + assert.match(reason, /metrics.defined 里编号 a 写了不止一次/); + assert.match(reason, /metrics.defined 的编号 wall-seconds 已被占用/); + assert.match(reason, /metrics.defined 的编号 answer-path 已被占用/); +}); + +test("提案条数:已收下 M 条再交就退回", async () => { + assert.match(await rejected(proposal(PROPOSAL, 2)), /本阶段你已经收下 2 条提案,每人最多 2 条/); +}); + +const TABLE = { + factorLocations: [{ factor: "p1", locations: ["solve.py:10-20"] }, { factor: "p2", locations: ["solve.py:30"] }], + safeguardLocations: [{ safeguard: "guard", locations: ["solve.py"] }], + unassigned: [], + concerns: "无", + body: "无", +}; +const FROZEN = { candidate: "r1-c1", clauses: ["c1", "c2"], factors: ["p1", "p2"], safeguards: ["guard"] }; +const implementation = (code: Extract["known"]["code"], content: Submission<"implementation">): Incoming => ({ + kind: "implementation", + content, + checklist: { kind: "implementation", ...FROZEN }, + known: { code }, + jevModel: MODEL, +}); +const SAVED = { saved: true as const, protectedFiles: [], hunks: [], protocol: { body: "协议", metrics: [] } }; + +test("规则 11:位置表覆盖每个因子和每条保障;起始行不大于结束行", async () => { + const reason = await rejected( + implementation(SAVED, { ...TABLE, factorLocations: [{ factor: "p1", locations: ["solve.py:20-10"] }], safeguardLocations: [], unassigned: [{ location: "util.py:5-3", note: "日志" }] }), + ); + assert.match(reason, /factorLocations 的因子应当是 p1、p2;还缺 p2/); + assert.match(reason, /safeguardLocations 的保障应当是 guard;还缺 guard/); + assert.match(reason, /代码位置 solve.py:20-10 的起始行大于结束行/); + assert.match(reason, /代码位置 util.py:5-3 的起始行大于结束行/); +}); + +test("改了不许改的文件:退回,原因里写出是哪几个文件(实现、修复、消融实现都查)", async () => { + const reason = await rejected(implementation({ ...SAVED, protectedFiles: ["validate.py", "harness.py"] }, TABLE)); + assert.equal(reason, "这份提交没有收下,要改的地方:\n- 改了不许改的文件:validate.py、harness.py。把它们改回本批基线的样子再交"); + const repair = await rejected({ kind: "repair", content: TABLE, checklist: { kind: "repair", ...FROZEN }, known: { code: { ...SAVED, protectedFiles: ["validate.py"] } }, jevModel: MODEL }); + assert.match(repair, /改了不许改的文件:validate.py/); + const ablation = await rejected({ + kind: "ablationImplementation", + content: { alsoOff: [], cannotRun: null, concerns: "无" }, + checklist: { kind: "ablationImplementation", cell: "r1-c1-p1", off: "p1", factors: ["p1", "p2"] }, + known: { code: { saved: true, protectedFiles: ["validate.py"] } }, + jevModel: MODEL, + }); + assert.match(ablation, /改了不许改的文件:validate.py/); +}); + +test("代码存不了:退回,原样写 workspace 给的路径和原因", async () => { + const reason = await rejected(implementation({ saved: false, path: "data/link", reason: "符号链接,代码版本只存普通文件和目录" }, TABLE)); + assert.equal(reason, "这份提交没有收下,要改的地方:\n- 代码存不成版本:data/link(符号链接,代码版本只存普通文件和目录)。改掉它再交"); +}); + +test("规则 12:核验的条款、因子、保障、没有归属的改动逐项全覆盖", async () => { + const reason = await rejected({ + kind: "verification", + content: { + clauses: [{ clause: "c1", found: true, where: "a" }], + factors: [{ factor: "p1", found: true, where: "a" }, { factor: "p2", found: true, where: "a" }, { factor: "p2", found: true, where: "a" }], + safeguards: [], + unassigned: [{ location: "util.py:3", outOfScope: false, reason: "日志" }], + body: "无", + }, + checklist: { kind: "verification", ...FROZEN, unassigned: ["util.py:1-4"] }, + known: null, + jevModel: MODEL, + }); + assert.match(reason, /clauses 的实现条款应当是 c1、c2;还缺 c2/); + assert.match(reason, /factors 的因子应当是 p1、p2;p2 写了不止一次/); + assert.match(reason, /safeguards 的保障应当是 guard;还缺 guard/); + assert.match(reason, /unassigned 的改动位置应当是 util.py:1-4;你写的 util.py:3 不在里面;还缺 util.py:1-4/); +}); + +test("规则 13:cannotRun 时 alsoOff 为空;alsoOff 不含本格的因子、都在因子清单里、不重复", async () => { + const checklist: ChecklistOf<"ablationImplementation"> = { kind: "ablationImplementation", cell: "r1-c1-p1", off: "p1", factors: ["p1", "p2"] }; + const both = await rejected({ kind: "ablationImplementation", content: { alsoOff: ["p2"], cannotRun: "跑不起来", concerns: "无" }, checklist, known: { code: null }, jevModel: MODEL }); + assert.match(both, /写了 cannotRun(这一格跑不起来)时 alsoOff 要交空数组/); + const reason = await rejected({ kind: "ablationImplementation", content: { alsoOff: ["p1", "p5", "p2", "p2"], cannotRun: null, concerns: "无" }, checklist, known: { code: { saved: true, protectedFiles: [] } }, jevModel: MODEL }); + assert.match(reason, /alsoOff 不能写本格要关的因子 p1/); + assert.match(reason, /alsoOff 只能写 p2;你写的 p5 不在里面/); + assert.doesNotMatch(reason, /只能写 p1/); + assert.match(reason, /alsoOff 里 p2 写了不止一次/); +}); + +test("规则 14:消融核验覆盖每个因子", async () => { + const reason = await rejected({ kind: "ablationVerification", content: { factors: [{ factor: "p1", off: true, where: "a" }] }, checklist: { kind: "ablationVerification", cell: "r1-c1-p1", factors: ["p1", "p2"] }, known: null, jevModel: MODEL }); + assert.match(reason, /factors 的因子应当是 p1、p2;还缺 p2/); +}); + +test("一份提交的几处问题一次列全,每条一行", async () => { + const reason = await rejected(proposal({ ...PROPOSAL, safeguards: ["main"], authorEstimate: r(3, 2, 1) }, 2)); + const lines = reason.split("\n"); + assert.equal(lines.length, 4, reason); + assert.equal(lines[0], "这份提交没有收下,要改的地方:"); + assert.ok(lines.slice(1).every((l) => l.startsWith("- ")), reason); + assert.match(reason, /main/); + assert.match(reason, /严格递增/); +}); + +// --------------------------------------------------------------------------- +// 拼冻结协议 +// --------------------------------------------------------------------------- + +test("回应拼协议:changes 只改保障时,因子、指标、正文取原提案的,实现条款取回应自己的;答复不进协议", () => { + const p = protocolBody({ kind: "response", content: { replies: [{ critique: "x", reply: "答" }], changes: { safeguards: ["clip"] }, clauses: ["条款一"], body: "答复正文" }, original: ORIGINAL }); + assert.deepEqual(p, { + factors: [factor("多起点", null), factor("扰动", "r1-c1-p2")], + safeguards: ["clip"], + metrics: { defined: [metric("kick-count")], reused: ["restart-count"] }, + body: "提案正文", + clauses: ["条款一"], + }); +}); + +test("回应拼协议:changes 改了正文时用新正文", () => { + const p = protocolBody({ kind: "response", content: { replies: [], changes: { body: "新正文" }, clauses: ["条款一"], body: "无" }, original: ORIGINAL }); + assert.equal(p.body, "新正文"); + assert.deepEqual(p.safeguards, ["guard"]); +}); + +test("统一拼协议:全用统一提交自己的字段,不叠原提案,不带 replies、sources", () => { + const content: Submission<"unification"> = { factors: [factor("合并后的因子", null)], safeguards: [], metrics: { defined: [], reused: [] }, body: "统一正文", replies: [{ critique: "x", reply: "答" }], clauses: ["统一条款"], sources: [{ proposal: "r1-a1-1", coverage: "全在" }] }; + assert.deepEqual(protocolBody({ kind: "unification", content }), { factors: [factor("合并后的因子", null)], safeguards: [], metrics: { defined: [], reused: [] }, body: "统一正文", clauses: ["统一条款"] }); +}); + +// --------------------------------------------------------------------------- +// 核验、消融核验通过没有 +// --------------------------------------------------------------------------- + +const VERIFICATION: Submission<"verification"> = { + clauses: [{ clause: "c1", found: true, where: "a" }], + factors: [{ factor: "p1", found: true, where: "a" }], + safeguards: [{ safeguard: "guard", found: true, where: "a" }], + unassigned: [{ location: "util.py:3", outOfScope: false, reason: "日志" }], + body: "无", +}; + +test("核验:全部找到、没有越界就通过", () => { + assert.deepEqual(verificationOutcome(VERIFICATION), { passed: true, clauses: [], factors: [], safeguards: [], outOfScope: [] }); +}); + +test("核验:一项没落实或一条判为越界就不通过,分开列出", () => { + const v = { ...VERIFICATION, safeguards: [{ safeguard: "guard", found: false, where: "看了 solve.py" }], unassigned: [{ location: "util.py:3", outOfScope: true, reason: "改了计时" }] }; + assert.deepEqual(verificationOutcome(v), { passed: false, clauses: [], factors: [], safeguards: ["guard"], outOfScope: ["util.py:3"] }); +}); + +const ABLATION_V = (offs: Record): Submission<"ablationVerification"> => ({ + factors: Object.entries(offs).map(([factor, off]) => ({ factor, off, where: "a" })), +}); + +test("消融核验:要关的和一起关的都关着、其余照留,就通过", () => { + assert.deepEqual(ablationOutcome(ABLATION_V({ p1: true, p2: false, p3: true }), "p1", ["p3"]), { passed: true, leftOn: [], wronglyOff: [] }); +}); + +test("消融核验:该关的照留、该留的关了,就不通过", () => { + assert.deepEqual(ablationOutcome(ABLATION_V({ p1: false, p2: true, p3: false }), "p1", []), { passed: false, leftOn: ["p1"], wronglyOff: ["p2"] }); +}); + +// --------------------------------------------------------------------------- +// 差异块的退回文字(假 Jev) +// --------------------------------------------------------------------------- + +function fakeJev(t: TestContext, yes: (question: string, diff: string) => boolean): void { + const key = process.env["TYPESAFE_API_KEY"]; + process.env["TYPESAFE_API_KEY"] = "test-key"; + t.after(() => { + if (key === undefined) delete process.env["TYPESAFE_API_KEY"]; + else process.env["TYPESAFE_API_KEY"] = key; + }); + let n = 0; + t.mock.method(globalThis, "fetch", async (_url: string, init: RequestInit) => { + const body = JSON.parse(init.body as string); + const answers = Object.fromEntries(Object.keys(body.questions).map((q) => [q, { noul: yes(q, body.state.change?.diff ?? "") ? 0.99 : 0.01 }])); + n += 1; + return new Response(JSON.stringify({ answers }), { status: 200, headers: { "x-typesafe-request-id": `req_${n}` } }); + }); +} + +test("实现:每块问一次,判断全交回;退回只列判是的几问,写出文件、第几块、附差异原文,原文含 ``` 时围栏加长", async (t) => { + fakeJev(t, (q, diff) => q === "unseeded_randomness" && diff.includes("random")); + const hunks = [ + { path: "solve.py", change: "modified" as const, text: "@@ -1 +1 @@\n-a\n+b" }, + { path: "solve.py", change: "modified" as const, text: "@@ -9 +9 @@\n-x = 1\n+x = random() # ```" }, + ]; + const v = await receive(implementation({ ...SAVED, hunks }, TABLE)); + assert.equal(v.accepted, false); + assert.equal(v.jev!.length, 10, "两块各五问"); + assert.equal( + (v as Extract).reason, + [ + "这份提交没有收下,Jev 判出下面几处有问题:", + "", + "- solve.py(改动的文件)的第 2 块:unseeded_randomness 概率 0.99(阈值 " + v.jev!.find((j) => j.question === "unseeded_randomness")!.threshold + ")", + "", + "````diff", + "@@ -9 +9 @@\n-x = 1\n+x = random() # ```", + "````", + ].join("\n"), + ); +}); + +test("回应全判否:收下,交回拼好的协议和三问的判断", async (t) => { + fakeJev(t, () => false); + const v = await receive(response({ replies: [{ critique: "r1-a1-1.a2.1", reply: "答" }, { critique: "r1-a1-1.a3.1", reply: "答" }], changes: { body: "新正文" }, clauses: ["条款一"], body: "无" })); + assert.equal(v.accepted, true); + assert.equal(v.jev!.length, 3); + assert.deepEqual(v.accepted && v.protocol, { + factors: [factor("多起点", null), factor("扰动", "r1-c1-p2")], + safeguards: ["guard"], + metrics: { defined: [metric("kick-count")], reused: ["restart-count"] }, + body: "新正文", + clauses: ["条款一"], + }); +}); + +test("统一的冻结协议有一问判是:退回,原因只列这一问", async (t) => { + fakeJev(t, (q) => q === "changes_measurement"); + const v = await receive(unification({ ...UNIFICATION, sources: [{ proposal: "r1-a1-1", coverage: "全在" }, { proposal: "r1-a2-1", coverage: "全在" }] })); + assert.equal(v.accepted, false); + assert.equal(v.jev!.length, 3); + const threshold = v.jev!.find((j) => j.question === "changes_measurement")!.threshold; + assert.equal((v as Extract).reason, `这份提交没有收下,Jev 判出下面几处有问题:\n\n- 冻结协议:changes_measurement 概率 0.99(阈值 ${threshold})`); +}); + +test("实现有差异块、全判否:收下,jev 是全部判断", async (t) => { + fakeJev(t, () => false); + const v = await receive(implementation({ ...SAVED, hunks: [{ path: "solve.py", change: "modified", text: "@@ -1 +1 @@\n-a\n+b" }] }, TABLE)); + assert.equal(v.accepted, true); + assert.equal(v.jev!.length, 5); +}); + +test("消融实现的代码存不成版本:退回", async () => { + const reason = await rejected({ + kind: "ablationImplementation", + content: { alsoOff: [], cannotRun: null, concerns: "无" }, + checklist: { kind: "ablationImplementation", cell: "r1-c1-p1", off: "p1", factors: ["p1", "p2"] }, + known: { code: { saved: false, path: "bin/x", reason: "符号链接,代码版本只存普通文件和目录" } }, + jevModel: MODEL, + }); + assert.match(reason, /代码存不成版本:bin\/x(符号链接/); +});