Files

148 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 任务书 06:workspace(代码版本)
写 workspace 模块:用 git 存每一份代码版本,取出到目录,比较两个版本改了什么。顺带给 tasks 的 `task.json` 加一项"不存的东西"。
本任务书已由用户和监督者在 2026-10-02 一起确定。下面说的"监督者",是在参考项目目录里和用户讨论设计的另一个会话。用不用 git、怎么用,是先派一个全新上下文的子 Agent 独立分析、实测之后才定的,下面"实测过"的结论都来自那次分析,监督者复现过其中第 3 条。
## 开工前先读
- `CLAUDE.md`:全部,尤其是"罕见情况怎么办"(什么算边界、哪些不算)、"不设默认值"、"测试"里"不为了方便测试而给产品代码加参数"。
- `docs/结构.md`:
- 第二部分:模块表里 workspace 一行;
- 第三部分:账本记录第 6 种"冻结代码";
- 第六部分:只有 runner 和 workspace 能起子进程;
- 第八部分:tasks 一节。
- `docs/方法.md`:
- 4.3 ⑨:实现与核验、因子位置表、没有归属的改动、不许改的文件;
- 4.3 里消融格的实现:从完整候选出发关掉因子;
- 第九部分:冻结的东西不可覆盖,恢复时只核对。
- vocab 里的 `CodeLocation`(`src/vocab/submissions.ts`):代码位置写成"文件"、"文件:行号"或"文件:起-止"。
- `WORK_LOG.md`。
- 参考项目(只读,不要改):
- `/home/iomgaa/Projects/RivalBits/src/board/code-store.ts`;
- `/home/iomgaa/Projects/RivalBits/src/rounds/code-diff.ts`。
只为了解它们原来要解决什么问题,第六部分说明为什么都不搬。
## 一、这个模块做什么
用一个例子走一遍:
1. 研究开始:把任务目录里的 `baseline/` 存成第一个版本,得到一个版本号。
2. 实验员实现候选 c1:把"本批基线"那个版本取出到一个空目录,实验员在里面改。
3. 实验员交了:把这个目录现在的内容存成一个版本,得到 c1 的版本号。flow 把它记进账本。
4. 核验员、研究者要看改了什么:比较本批基线和 c1 两个版本。
5. 正式运行:把 c1 取出到一个目录,交给 tasks 准备运行目录。
6. 采纳:c1 成为新基线。git 里什么都不做,账本记一句"现在的基线是哪个版本号"。
7. 消融格:从 c1 取出,实验员关掉一个因子,再存成一个版本。
"上一版是谁"由账本记,git 里不记。
## 二、怎么用 git
**一个裸仓库,版本号就是目录内容的编号。**
- 一份研究一个裸仓库(`git init --bare`),位置由调用方给。
- 版本号用 git 的目录树编号(tree id),不造提交。同样的内容永远得到同一个编号,所以:
- 中断恢复时重新存一次,得到的号和账本里已经记的一样;
- 存过的版本没法被覆盖,因为内容变了编号就变。
- 每存一个版本,挂一个以版本号命名的引用,例如 `refs/versions/<编号>`,防止 git 清理没有引用指着的对象。实测过:没挂引用的对象,`git gc --prune=now` 之后就没了。
**存版本不用 `git add`,由我们自己遍历目录、列出文件清单**:
- 每个文件用 `git hash-object -w --no-filters` 存,再用临时的索引文件(`GIT_INDEX_FILE`)加 `git update-index --index-info` 和 `git write-tree` 得到目录树编号。
- 为什么不用 `git add`:实测过,目录里有 `.gitignore` 时,被它列中的文件 `git add` 不存,退出码是 0。子目录里有另一个 git 仓库时,里面的文件也会丢。实验员写个 `.gitignore` 是模型干活时顺手就会做的事,而漏存以后,正式运行跑的代码和实验员交的就不是同一份,谁也发现不了。
- 记下可执行位:普通文件 100644,可执行文件 100755。
**git 不读用户的配置**:
- 我们启动的每个 git 子进程都设 `GIT_CONFIG_NOSYSTEM=1` 和 `GIT_CONFIG_GLOBAL=/dev/null`。用户本机的全局配置里有换行符转换、比较算法、lfs 过滤器这些设置,会悄悄改掉存进去的内容或比较输出的格式。这件事没人操作也会发生,所以要防。
- 仓库位置(`GIT_DIR`)只写进我们自己启动的子进程的环境变量,不写进 `process.env`。插件进程的环境会被实验员的 shell 继承,模型顺手敲一句 `git checkout`,就会动到版本仓库。
**取出用 `git archive`**(或者效果一样的命令):写进一个空目录,可执行位要还原。取出的目录里没有 `.git`,实验员看不到别的版本。
## 三、对外提供什么
经 `src/workspace/index.ts` 导出,函数名由你定。仓库位置作为参数传入,不设默认值。
1. **建仓库**:在给定位置建一个空的裸仓库。
2. **存版本**:给一个目录和一份"不存的名字"清单,交回版本号,或者交回"存不了 + 原因"。
- 遍历目录:名字在清单里的文件或目录,在任何一层都跳过。
- 遇到符号链接、设备文件这类既不是普通文件也不是目录的东西,交回"存不了"并写明是哪个路径。这个目录是实验员交上来的,属于系统边界(CLAUDE.md"罕见情况怎么办"),flow 会把它当作提交不合格退回给实验员。不要替它猜怎么存。
- 基线也用这个函数存。
3. **取出**:把一个版本的文件写进一个空目录。
4. **比较两个版本**:给"从哪个版本"和"到哪个版本",交回:
- 每个改动过的文件:路径,新增、删除还是修改,是不是只改了可执行位,是不是二进制;
- 每个文件里变了哪几块,**新旧两边的行号范围都给**。正向核验对照的是新版本的行号(位置表写的是候选代码的行号);消融时位置表写的是完整候选的行号,也就是比较里"从"那一边。只删不加的块在新版本里没有行,用 `git diff -U0` 那种"第几行之后"的写法表示,在类型里写清楚;
- 给模型看的差异文本:带上下文的 `git diff` 原样输出,不加颜色。
列文件用 `git diff-tree -r` 这类给程序读的命令,它的输出格式 git 保证不变。二进制文件不拆块,当作整个文件改了。
**不归 workspace 管的**:
- 哪些改动没归到任何因子或保障:由拿得到位置表的模块,用这里给的行号范围现算;
- 哪些改动属于不许改的文件:用这里的文件清单,交给 tasks 现成的函数判断;
- 给研究者看的只读目录:以后要用时,调用方用"取出"。
## 四、顺带改 tasks
- `task.json` 加一项"不存的名字",字段名你定,必填。填圆写:
- `__pycache__`:实验员跑 Python 时会自动生成;
- `.git`:模型可能在目录里 `git init`;
- `solution.json`、`metrics.json`:运行输出,不是代码。
- tasks 读任务时一并交回这份清单,flow 存版本时传给 workspace。workspace 不引用 tasks。
- `protectedFiles` 里的 `solution.json` 删掉:它现在在"不存"清单里,harness 开跑前也会删掉旧的。
- 相应的测试和 `docs/结构.md` 第八部分 tasks 一节跟着改。
## 五、测试
workspace 的测试用真实 git,测试仓库建在测试自己的临时目录里。
- **同内容同号**:同一个目录存两次;把目录复制一份、改了文件时间再存,都得到同一个版本号。
- **取出再存回来,号不变**:内容、可执行位都还原。
- **`.gitignore` 不起作用**:目录里有 `.gitignore` 写了 `*.json`,`cfg.json` 照样存进去。
- **不读用户配置**:测试里给 git 一份会改行为的全局配置(例如 `core.autocrlf=true`,或者改比较前缀的 `diff.noprefix=true`),结果不受影响。这份配置用测试自己的环境变量(例如 `HOME` 指向临时目录)造出来,不给产品代码加参数。
- **"不存"清单**:清单里的名字在顶层和子目录里都跳过。
- **存不了**:目录里有符号链接时,交回"存不了"和那个路径。
- **引用防清理**:存完以后 `git gc --prune=now`,版本照样能取出。
- **比较**:
- 新增、删除、修改、只改可执行位的文件各一个,以及一个二进制文件;
- 行号:手写两个小文件,算好预期的新旧行号范围再比,至少包括改一行、加几行、只删不加三种块;
- 预期值手算,不从实现里推。
- **tasks**:`task.json` 少了新字段时报错;读任务交回这份清单。
workspace 关系到"冻结的东西不可覆盖"和中断恢复(CLAUDE.md"工作方式"),写完派一个全新上下文的子 Agent 审阅一轮。告诉审阅者:
- 每条意见先回答"不写会怎样";
- 只防三类:没人操作也会发生的、模型正常工作时就会做的、出错后发现不了的;
- 模块之间传错参数、有人用 shell 故意破坏,都只列出不改;
- 下面这些分析时已经判断过不用防,除非有新证据:
- 并发存版本:每次用自己的临时索引,控制进程只有一个;
- 换行符和大文件:`--no-filters` 原样存;
- 嵌套仓库:清单是自己列的,不会出现。
审阅意见逐条判断,采纳或不采纳都在报告里写理由。
## 六、参考项目的保留与删除
| 处理 | 内容 |
|---|---|
| 删除 | `code-store.ts` 整个文件:复制、逐个重算哈希、改只读、强制写盘、目录已存在时逐字节比对。git 的内容编号本身就保证了这些 |
| 删除 | `code-diff.ts` 整个文件:自写的逐行比较算法,改用 git。它的理由是"换台机器结果也一样",但一份研究只在一台机器上算 |
| 删除 | `candidateContentHash` 里的可执行位处理:git 的目录树本来就记可执行位 |
| 借鉴 | 无 |
## 七、规模与文档
- 预期 200–300 行,不含测试。超过 300 行就停下来检查。
- 完成后更新:
- `docs/结构.md` 第八部分:补 workspace 对外的函数和类型,每项一句话;tasks 一节按第四部分改;
- `WORK_LOG.md`:在"已完成"补一条。
- 不新建 md 文件。
## 八、验收与报告
做完后本地提交,然后在回复里交一份报告,监督者会对着实际文件逐项核对:
1. 提交号,新增和修改的文件列表,每个文件的行数,workspace 的总行数。
2. `npm run check` 和 `npm test` 的完整输出。
3. 比较两个版本的实际输出样例:一份结构化结果,一份差异文本。
4. 审阅意见逐条:内容、采纳或不采纳、理由。
5. 你和这份任务书、结构.md、方法.md 不一致的地方,以及为什么。
6. **规则不清楚的地方**:读 CLAUDE.md、结构.md、方法.md 时,哪里让你犹豫、哪里两份文件说法不一致、哪里你是靠猜的。没有就写"没有"。