【Codex with ChatGPT 源码学习⑩】Workspace Search 与 Git Diff:从快速检索到可信代码审查——两项真实复现的证据完整性问题
系列定位:深入研究开源工程
XiaoDuoYa/codex-with-chatgpt,从浏览器控制、MCP、OAuth、状态机、安全与部署,继续来到 Workspace Search、Git Diff 与上下文工程。本文面向对 AI Agent、MCP、Node.js、Git、代码审查可靠性有兴趣的开发者。
开源项目:XiaoDuoYa/codex-with-chatgpt
本文固定源码基线:535a07dbb7aee887924c53767d985c09d405319d,不是浮动的最新分支。
实测范围:Windows x64、Node.js 24.21.0、ripgrep 15.2.0、Git 隔离临时工作区;直接调用已构建的dist/workspace/*.js;不是 ChatGPT 经 OAuth/Tunnel 远程调用 MCP 的端到端实验。
阅读时间:约 25~35 分钟。文末附可复现 ZIP、Windows 与 Linux/macOS 使用说明。
01. 先提出一个问题:AI 已经读到了代码,为什么仍然可能审查错?
假设我们让 Agent 修复一个财务报表导出函数。它很快找到 exportInvoice(),读了文件,并总结“修改没有影响其他模块”。
但如果后台任务也调用了这个函数?如果它内部依赖了 formatAmount(),而搜索只用了 exportInvoice 关键词?如果 ChatGPT 分页读取 Git Diff 时,Codex 正好修改了源文件?
Agent 看到的代码,不一定是完整的代码;看到的多个片段,也不一定属于同一个工作区版本。
第十章要解决三个问题:
- 定位效率:面对数百、数千乃至更多文件,怎样找到相关代码?
- 上下文完整性:找到多少文件才足够,搜索结果为什么不能直接等于依赖覆盖?
- 证据可信度:Git Diff 经过分页、过滤和持续变化后,是否还能作为可靠审查依据?
这次我们不止阅读源码,还将使用真实模块做隔离实验。最重要的两个结果先列出来:
| 实验主题 | 实际观察 | 结论 |
|---|---|---|
| UTF-8 Diff 分页 | 完整 6,467 B,分页拼接 6,489 B,出现替换字符 | 当前固定版本在多字节长行下不保证无损 |
| 跨版本分页 | V1/V2/混合均 6,238 B,混合结果不等于任一个完整版本 | 当前分页接口不保证同一快照 |
这两项是特定源码基线和测试条件下的可靠性问题。本文不会将其夸大为已证实的越权访问、代码执行或真实凭据泄露。
02. 先建立源码地图:搜索、Git、读取各做什么?
本章重点文件及固定链接:
| 文件 | 在系统中的角色 |
|---|---|
src/mcp/server.ts | 注册 search_workspace、git_status、git_diff 等 MCP 工具;执行 Scope 检查 |
src/workspace/search.ts | searchWorkspace()、findRipgrep()、Node 回退、过滤与截断 |
src/workspace/git.ts | gitStatus()、gitDiff()、重命名判断、敏感路径过滤、分页 |
src/workspace/manager.ts | Workspace 路径边界、文件读取、类型/大小检查 |
src/workspace/ignore.ts | Sensitive、Noise 和 .c2cignore 等路径规则 |
图 1:工程上下文获取逻辑(示意,而非固定执行顺序)
一个容易混淆的边界是:MCP 工具存在权限控制,workspace.search 是独立 Scope;但我们接下来直接导入本地模块验证,不能因此声称已验证 OAuth 授权和云端 MCP 请求链路。
03. Search 工具契约:不要只看 matches
search_workspace 的关键参数来自固定版本 server.ts:
| 参数 | 作用 | 边界 |
|---|---|---|
query | 搜索文本 | 至少两个字符 |
path | 限定工作区内的路径 | 不能借此越过 Workspace |
glob | 过滤候选文件名 | 例如 *.ts |
limit | 限制响应匹配数 | 默认 50,最高 200 |
regex | 是否启用正则模式 | 默认 false |
一个教学示例:
{
"query": "searchWorkspace",
"path": "src",
"glob": "*.ts",
"limit": 20,
"regex": false
}
响应包含 matches(路径、行号、文本)、matchCount、truncated 和 engine。本章真正关注的是:
truncated=true表示结果集尚未完整返回,不应据此断言“相关代码全部找齐”。
比如一次搜索 const,设置 limit=3,实测仅返回 3 个位置、truncated=true;将 Limit 扩大到 200,仍是 truncated=true。这不是程序报错,而是对响应范围的诚实描述。
同时也应注意:truncated=false 仅说明该查询、该候选范围和该引擎规则下没有因结果限制而被截断。它并不能证明语义层面的所有依赖都被检索到了。
04. 两种搜索引擎:优先 ripgrep,为什么还需要 Node 回退?
阅读 search.ts 可以看出,C2C 会尝试使用 rg,并在不可用或执行异常时回退到 Node.js 搜索。
4.1 ripgrep 的执行路线
它通过 child_process.spawn() 启动外部程序,使用 --json 获得机器可读匹配事件。实现中的关键选项包括:
--json
--max-filesize 2M
--max-count <limit+1>
-F # regex=false 时
--smart-case
-g <glob> # 提供 glob 时
-- <query> <path>
-F 将查询按普通文本处理,-- 可以避免查询文本被解释为选项。--max-count 是 ripgrep 的每文件约束,C2C 还会依据全局 limit 截取响应。
4.2 Node 回退的执行路线
Node 方案遍历目录、过滤候选路径、读取大小符合条件的文本文件并逐行匹配。它不需要额外的 rg.exe,对环境依赖更少。
C2C_DISABLE_RG=1 可以强制使用 Node 回退;C2C_RG_PATH 可以指定单独的 ripgrep 程序。对同一个进程而言,还要留意 findRipgrep() 的缓存,因此比较不同引擎时,最好分别启动 Node 进程。
4.3 返回结构相同,不意味着行为完全一致
| 比较点 | ripgrep | Node 回退 |
|---|---|---|
| 执行形态 | 外部子进程 | 当前 Node 进程 |
| 大小写 | --smart-case | 不区分大小写的文本搜索 |
| 正则处理 | rg 正则语法 | JavaScript RegExp |
| 忽略规则 | rg 自身规则加 C2C 过滤 | C2C 内部忽略规则 |
| 大小限制 | --max-filesize 2M | 约 2 MiB 文件限制 |
| 结果输出 | JSON 事件解析 | 逐行构造结果 |
需要重点记住:两个搜索引擎既可能产生不同耗时,也可能对某些输入给出不同的命中集合。统一接口只是统一了数据形状,不代表语义等价。
05. Workspace 文件过滤:减少噪声,也降低敏感数据暴露风险
ignore.ts 将不同路径规则区分处理:
- Sensitive:典型如
.env、凭据和密钥路径;不应返回给模型。 - Noise:如
.git、node_modules、构建输出等;主要为减少无关结果与处理成本。 - Custom:工作区中的
.c2cignore,补充特定项目的隐藏策略。
这些过滤机制不是完整的秘密扫描器。一个看起来普通的 src/config.ts 中若硬编码了 Token,单靠敏感文件名过滤并不能保证发现它。
同样,查询返回的匹配文本也不能直接替代安全审核。Agent 需要遵守 Workspace 边界和权限策略,所有证据采集都应带有出处。
06. Git Status:Staged、Unstaged、Untracked 不是同一回事
git.ts 的状态解析以 git status --porcelain=v2 --branch -- . 为基础,可返回分支、上游、ahead/behind,以及文件状态列表。
| 状态 | 意义 | 审查风险 |
|---|---|---|
staged | 已加入 Git Index | 只看工作区 Diff 可能漏掉它 |
unstaged | 当前工作树相对 Index 的变化 | 只看暂存区会遗漏 |
untracked | Git 尚未追踪 | 普通 git diff 不会自动包含正文 |
conflicted | 合并冲突 | 需要独立处理冲突状态 |
hidden | 被隐藏的变更数量 | 可知有被遮蔽内容,不公开敏感路径正文 |
隔离实验实际构造了 staged 新增文件、unstaged 修改文件、untracked 文本和已跟踪的 .env 修改。C2C 返回 hidden.changes=1,可见列表没有包含 .env。
这说明在该测试条件下路径隐藏确实生效;但不能推论普通文件中的所有敏感字符串也会被自动识别。
07. Git Diff:三种模式,三种审查基准
C2C gitDiff() 提供:
| 模式 | 审查的是 | 用途 |
|---|---|---|
unstaged | 工作区 vs Git Index | 看尚未暂存的修改 |
staged | Git Index vs HEAD | 看准备提交的补丁 |
head | 工作区 vs HEAD 的已跟踪变更 | 看待提交变更的整体情况 |
在我们的隔离实验中,安全过滤后的 Diff 总字节数分别是:
unstaged:5,001 B。staged:164 B。head:5,165 B。
此案例恰好 5001 + 164 = 5165,但这不是所有 Git Diff 的通用公式。同一文件同时存在 Staged 和 Unstaged 修改时,补丁上下文和合并结果可能不同。
7.1 Git Diff 为什么要先获取文件清单?
该实现先用 git diff --name-status -z --find-renames=1% 建立变更清单,再过滤敏感路径,最终依据允许的 Literal Pathspec 分批读取补丁。
图 2:先过滤路径,再获取 Diff 内容
为什么不能先生成完整的原始 Diff 再简单替换 .env 字样?因为那会让敏感内容已经进入内部读取与暂存过程。尽可能在采集前做边界判定,比生成后用字符串替换可靠。
7.2 双向重命名需要同时检查旧路径和新路径
本章实际使用 Git 的三条 R100 重命名测试:
| 场景 | Git 识别 | C2C 结果 |
|---|---|---|
a-before.ts → a-after.ts | R100 | 两侧路径可见 |
b-before.ts → .env | R100 | 两侧路径隐藏 |
.env.local → c-after.ts | R100 | 两侧路径隐藏 |
C2C 返回的隐藏变更数为 2。这证明了给定三个受控重命名样例中的两侧过滤行为,而非所有 Git 边界情形都已审计完成。
08. Diff 字节分页:响应很短,底层并不一定很便宜
gitDiff() 默认分页大小为 64 KiB,请求的 maxBytes 有 1 KiB~256 KiB 边界,安全 Diff 聚合有 64 MiB 上限。返回结构包括:
{
"totalBytes": 5001,
"offset": 0,
"returnedBytes": 999,
"hasMore": true,
"nextOffset": 999,
"diff": "..."
}
这些数值是下面教程中的受控 ASCII 样例,不是通用输出。
一个容易误解的地方是:maxBytes=1024 只约束这一页的响应数据量,不代表 C2C 在本次调用中只读取或生成了 1024 B Diff。
固定实现会先运行 Git、过滤、合并允许的 Diff,再转为 UTF-8 Buffer 并分页。请求后续页时,会重新进行这个流程。
8.1 ASCII 测试为什么是 PASS?
我们构造了 5,001 B 的 ASCII Diff,连续请求 5 页:
| 页 | Offset | 返回字节数 |
|---|---|---|
| 1 | 0 | 999 |
| 2 | 999 | 1,000 |
| 3 | 1,999 | 1,000 |
| 4 | 2,999 | 1,000 |
| 5 | 3,999 | 1,002 |
重组后仍是 5,001 B,使用 Buffer.equals() 比对完整 Diff,结果为 true。这为静态 ASCII 数据提供了正例。
但这个 PASS 还不能证明 UTF-8、变更中的工作区或任意偏移也具有相同性能与正确性。
09. 真实发现一:UTF-8 分页为什么会把 6,467 B 变成 6,489 B?
复现条件:在隔离 Git 仓库创建一条包含 900 次 汉🚀 的长行,调用真实 gitDiff(ws, {mode:'unstaged',maxBytes:1024}),按 nextOffset 连续读取,将每页文本重新编码并拼接,与一次性读取的完整 Diff 比对。
结果如下:
{
"fullBytes": 6467,
"pageCount": 8,
"reconstructedBytes": 6489,
"exactMatch": false,
"byteLengthConsistent": false,
"replacementPages": [2, 3, 4, 5, 6, 7, 8],
"midCharacterReplacement": true,
"result": "BOUNDARY_ISSUE_OBSERVED"
}
9.1 真正的根因是字节边界不等于字符边界
源码在 git.ts 分页处理部分 将聚合 Diff 转为 Buffer,再按 offset 和 maxBytes 做字节切片,并对切片单独执行 toString('utf8')。
用原理性伪代码表示:
const bytes = Buffer.from(allSafeDiff, 'utf8');
const pageBytes = bytes.subarray(offset, offset + maxBytes);
const pageText = pageBytes.toString('utf8');
UTF-8 中,英文 ASCII 往往占 1 B,而汉字、Emoji 可以分别占 3 B、4 B。一个字符若被切到两个页面,再分别解码,可能产生 Unicode 替换字符 �。
图 3:同一字符被切成两页的风险
这次多出的 22 B,是因为独立 UTF-8 解码产生替换字符,再编码后总字节数改变;不是 Git 给源文件额外添加了 22 B 内容。
9.2 为什么“优先按换行断页”没有解决问题?
代码会尝试按照页面内最后一个换行符缩小返回范围。但对一条超过 1,024 B 的长行,很多窗口内部没有换行,依然可能从多字节字符中间截断。
本次第 1 页只返回 142 B,后续多个页面按 1,024 B 切片,其中第 2~8 页观察到了替换字符。
9.3 缺陷性质与建议修复
这是数据完整性 / 代码审查可靠性问题。它可能影响中文注释、非 ASCII 字符串、自动化 Diff 比对;本实验没有证明权限绕过、真实密钥泄露或远程执行。
建议:验证传入 offset 是否是合法 UTF-8 边界;分页结束位置选择合法字符边界;优先完整换行;长行时保证前进;使用真正返回的原始字节数填写 returnedBytes。若需要绝对逐字节传输,可考虑 Base64 页。上述都是改进建议,而非仓库已实现功能。
10. 真实发现二:Git Diff 分页为何会混合两个代码版本?
在另一个纯 ASCII 临时 Git 仓库中,我们人为制造两个长度相同的工作区版本:
- V1:
ONE_0001和大量A。 - V2:
TWO_0002和大量B。
先读取 V1 的第一页,再修改工作区,后续页面从 V2 读取。
实测结果:
{
"firstPageChanged": true,
"v1Bytes": 6238,
"v2Bytes": 6238,
"mixedBytes": 6238,
"pageCount": 7,
"mixedMatchesV1": false,
"mixedMatchesV2": false,
"result": "SNAPSHOT_DRIFT_OBSERVED"
}
即使三者总字节数完全一致,混合结果仍然既不是 V1,也不是 V2。
图 4:跨版本分页的形成
10.1 根因与 UTF-8 问题不同
每次 gitDiff() 都重新获取变更清单并生成完整 Diff,而多页请求之间没有共享不可变快照标识。分页元数据记录的是该次调用的范围,不是整个审查会话的源码版本。
这意味着修复了 UTF-8 切片也还不够:分页可以在编码完全正常的情况下发生版本漂移。
10.2 为什么只绑定 Git HEAD 不够?
本实验改变的是未暂存工作区文件,Git HEAD 没有变化。
| 校验字段 | 是否能发现本次版本漂移 |
|---|---|
offset、nextOffset | 不能 |
totalBytes | 不能:V1/V2 长度相同 |
gitHead | 不能:只改了工作区 |
| 完整安全 Diff 的 Hash | 可以区分本次两个不同内容 |
| 不可变快照 ID | 在服务端正确保存和绑定快照时,可保证跨页一致 |
短期设计可以在每页附完整安全 Diff 的 SHA-256 进行漂移检测;长期则需要受权限和 TTL 控制的不可变快照存储。这两者目前都是未在原仓库实现的设计建议。
10.3 两项发现不能混为一谈
| 缺陷 | 触发条件 | 主要修复方向 |
|---|---|---|
| UTF-8 页损坏 | 多字节文本被切断 | UTF-8 安全分页或字节页编码 |
| 快照漂移 | 多次读取间工作区发生变化 | 内容哈希检测或不可变 Snapshot |
两者都关系到 Reviewer 的证据可信度,但缺陷机制完全不同。实验是受控交错读取,不是线上双 Agent 并发损失统计,也不是安全漏洞严重性评级。
11. Windows 实测:为什么小仓库里 Node 搜索反而更快?
使用独立的 ripgrep 15.2.0,分别运行两种引擎。每种查询预热后采样 7 次,比较 C2C 工具层整体调用时间:
| 查询 | Node 中位(ms) | ripgrep 中位(ms) | 两者命中位置 |
|---|---|---|---|
searchworkspace | 9.275 | 68.410 | 5 个,完全一致 |
requirescope | 8.741 | 64.887 | 11 个,完全一致 |
另外,对 rg --version 的同步进程启动诊断采样 9 次,中位耗时 59.746 ms(范围 59.028~64.382 ms)。
因此,“当前 Windows 小仓库中外部进程启动开销较突出”有了证据支持。但 spawnSync() 诊断与 C2C 的异步 spawn() 并不完全等价,不能严格推导 59.746 ms 就是每次搜索的纯启动成本。
这里的教训是:
比较工具性能,应衡量完整调用链;判断哪个工具更适合生产,还应考虑仓库规模、平台和结果完整性。
不能根据这两组小仓库测试就断言 Node 在任何情况下都比 ripgrep 快。
12. Node 搜索为何出现“匹配越多反而越快”?
同一 Node 回退搜索的真实数据:
| Query | Limit | 返回数 | Truncated | 中位耗时 |
|---|---|---|---|---|
const | 3 | 3 | true | 0.805 ms |
const | 50 | 50 | true | 1.278 ms |
const | 200 | 200 | true | 2.055 ms |
searchWorkspace | 50 | 5 | false | 8.694 ms |
| 不存在的关键词 | 50 | 0 | false | 8.445 ms |
对于高频词,达到返回上限后能提前结束;无命中查询通常需要扫描更大范围才能确认没有结果。
所以,“const 搜索仅 0.805 ms”并不代表系统已经完整扫描了工作区。恰好相反,它返回的是被截断的局部结果。
在 Agent 的工具使用策略中,应当把 truncated 作为证据质量信号,而不是只把搜索速度与命中数量作为成功指标。
13. 上下文工程:6 个文件与 506 个文件,策略为什么逆转?
我们构造了一个虚构的报表导出项目,其中有四个已知相关文件:
exporter.ts:修改中的exportInvoice。routes.ts:HTTP 调用方。jobs.ts:后台任务调用方。format.ts:下游formatAmount依赖。
其余文件是无关的 logger.ts、date.ts 以及可选的 500 个 noise_*.ts 文件。
比较三种方式:
- A 全量读取:已知全部文件名,直接读取。
- B Search → Read:搜索
exportInvoice,只读命中路径。 - C Git Diff → Search → Read:先看 Diff,再以
exportInvoice和formatAmount两词扩大搜索,读取候选路径。
注意:策略 C 的第二个关键词是教学实验预先指定的,不是 C2C 自主生成的依赖推理结果。
13.1 真实对照结果
| 规模 | 策略 | 读取文件 | 估算文本量 | 召回率 | 精确率 |
|---|---|---|---|---|---|
| 6 文件 | A 全读 | 6 | 583 B | 100% | 66.7% |
| 6 文件 | B 搜索再读 | 3 | 837 B | 75% | 100% |
| 6 文件 | C Diff+搜索再读 | 4 | 1,570 B | 100% | 100% |
| 506 文件 | A 全读 | 506 | 24,583 B | 100% | 0.79% |
| 506 文件 | B 搜索再读 | 3 | 837 B | 75% | 100% |
| 506 文件 | C Diff+搜索再读 | 4 | 1,570 B | 100% | 100% |
在 6 文件样本中,A 直接读取反而文本量最少,因为额外的搜索 JSON 和 Diff 本身也是上下文开销。
在 506 文件样本中,C 将估算文本量从 24,583 B 降至 1,570 B(减少约 93.6%),同时保留了四个相关文件的完整召回。B 更省文本,但漏掉了关键的 format.ts。
13.2 实际读取耗时不能误用
506 文件样本中读取阶段耗时:
| 策略 | readStageMs |
|---|---|
| A | 260.517 ms |
| B | 1.573 ms |
| C | 3.147 ms |
这里的 readStageMs 不包括 B/C 已发生的搜索时间和 C 的 Git Diff 生成时间,也没包含 A 文件清单的发现成本。因此不能据此宣称 C 的端到端耗时“快 80 多倍”。
estimatedTextBytes 也不等于精确 LLM Tokens;它只按实验约定加总读取正文、搜索 JSON 与 Diff 文本。
13.3 真正可迁移的结论
- 文件很少且范围已知:直接读取可能更简单、更经济。
- 工作区较大:搜索和 Diff 有助于减少无关正文。
- 任务需要影响分析:只搜函数名可能漏掉下游依赖,需要扩展检索或使用静态分析。
- 无论哪种策略:必须说明哪些文件已覆盖、哪些可能遗漏、结果是否截断。
14. 从“工具结果”进化为“可审计 Evidence Contract”
前面两个真实发现说明,单靠 path、line、offset、totalBytes 还不足以支撑严肃的代码审查。
我们可以为后续自研 Agent / Reviewer 设计一个建议中的 Evidence Contract(不是 C2C 当前的 MCP 响应协议):
{
"schemaVersion": "0.1-draft",
"workspaceId": "opaque-workspace-id",
"kind": "git-diff",
"gitHead": "optional-head-sha",
"sourceRevision": "sha256-of-complete-safe-diff",
"snapshotId": "optional-immutable-snapshot-id",
"encoding": "utf-8",
"pagination": {
"offset": 0,
"returnedBytes": 1024,
"totalBytes": 6238,
"hasMore": true,
"nextOffset": 1024
},
"integrity": {
"contentSha256": "placeholder-not-real-hash",
"pageSha256": "placeholder-not-real-hash"
},
"retrieval": {
"truncated": false,
"engine": "git",
"scope": "."
}
}
这个 JSON 仅展示设计字段,哈希都是占位文字。完整草案 Schema 放在公开实验 ZIP 中,不得将占位值当成真实证据。
建议至少建立以下不变量:
offset + returnedBytes必须与nextOffset协调,并且严格前进。- 对 UTF-8 文本页,重新编码的字节长度应当等于该页报告的实际字节数。
- 同一审查对象的所有页面应当绑定同一份内容哈希或不可变快照。
- 文件路径、行号、工作区身份、工具版本、采集时刻和截断状态必须可追溯。
- Reviewer 应能明确声明证据覆盖范围和未检索区域,而非暗示“已经读完项目”。
图 5:面向 Planner/Reviewer 的证据链建议
图中“快照”和自动覆盖判断是建议架构,并不是说原 C2C 已实现 Planner/Reviewer 的完整闭环。
15. 两项问题的工程修复次序:先保证内容,再保证版本
| 项目 | 本章证据 | 建议优先级 | 可验证的验收条件 |
|---|---|---|---|
| UTF-8 字节页边界 | 已复现数据有损 | P1(建议) | 中文、Emoji、长行的分页无损重组;非法 offset 明确处理 |
| 跨版本混合 | 已复现 V1/V2 混合 | P1(建议) | 所有页来自同一快照,或不同完整内容哈希被拒绝混用 |
| Node 与 ripgrep 语义差异 | 源码存在差异,两例一致 | P2(建议) | 大小写、Glob、Regex、忽略规则对照矩阵 |
| 完整 Diff 重复生成 | 源码可见 | P2(建议) | 大 Diff 场景下有明确耗时/内存预算 |
| 未知依赖的覆盖 | 教学策略已模拟 | P2(建议) | 有可解释的候选扩展与停止条件 |
这里的 P1/P2 是文章作者提出的工程优先级建议,不是上游维护者的官方缺陷等级或已分配的 CVE。我们也没有在这篇文章中提交或验证修复 PR。
16. 读者如何从零复现?提供完整隔离实验 ZIP
这一节给出学习路线,不要求具备 ChatGPT 登录凭据,也不要求安装 C2C Skill。所有会修改文件的 Git 操作都发生在自动生成的临时仓库里。
16.1 下载实验资源
资源名称:第十章-Workspace-Search与Git-Diff-公开配套实验-v1.zip
资源下载地址:下载
ZIP 内包含:
c2c-ch10-public-lab/
├── README.md
├── 安全边界与清理.md
├── 故障排查.md
├── 实验结果模板.md
├── evidence-contract-v0.1-draft.schema.json
├── SHA256SUMS.txt
├── scripts/
│ ├── _helpers.mjs
│ ├── 00-preflight.mjs
│ ├── 01-search-benchmark.mjs
│ ├── 02-git-status-diff.mjs
│ ├── 03-utf8-diff.mjs
│ ├── 04-snapshot-drift.mjs
│ └── 05-context-scale.mjs
└── tests/
└── helpers.test.mjs
16.2 准备原仓库独立学习克隆
避免在正在开发的仓库中强行切换版本。新建空目录,执行:
# 在自己选择的学习目录执行
git clone https://github.com/XiaoDuoYa/codex-with-chatgpt.git c2c-ch10-source
Set-Location .\c2c-ch10-source
git checkout --detach 535a07dbb7aee887924c53767d985c09d405319d
corepack pnpm install --frozen-lockfile
corepack pnpm build
如果已有相同 Commit 的独立源码克隆并已经构建,直接使用它即可;不要重复修改主项目。
16.3 环境检查与自测试
将 ZIP 解压到独立实验目录。以下命令在实验目录执行:
$repo = 'C:\learning\c2c-ch10-source' # 这里替换成自己构建的源码克隆路径
node .\scripts\00-preflight.mjs --repo "$repo"
node --test .\tests\helpers.test.mjs
预检应确认 Git、Node 与 dist/workspace/manager.js、search.js、git.js 可用。单元测试只验证公开包的临时工作区机制和参数/清理保护,不是原仓库完整测试。
16.4 Step A:Node 搜索与结果截断
node .\scripts\01-search-benchmark.mjs --repo "$repo"
重点观察 engine、matches、truncated、matchKeys 与中位耗时。查询高频词时会看到有上限的结果,不能把 truncated=true 解释为已经全部检索。
16.5 Step B:比较 ripgrep(可选)
独立下载可信的 ripgrep 便携版,解压后执行:
$rgExe = 'C:\tools\ripgrep\rg.exe' # 替换成实际绝对路径
& $rgExe --version
node .\scripts\01-search-benchmark.mjs --repo "$repo" --rg "$rgExe"
两个引擎分别运行在独立 Node 进程里。先比较 matchKeys,再比较耗时。不要预设 ripgrep 一定更快。
16.6 Step C:Git 状态、Diff、敏感路径与重命名
node .\scripts\02-git-status-diff.mjs --repo "$repo"
它会创建独立临时仓库,构造 Staged、Unstaged、Untracked 和虚构 .env,以及三种重命名。重点看 hiddenChanges、newSensitivePathHidden、oldSensitivePathHidden 与 PASS。
16.7 Step D:UTF-8 分页完整性测试
node .\scripts\03-utf8-diff.mjs --repo "$repo"
旧版固定 Commit 在上述测试输入下可以观察到 BOUNDARY_ISSUE_OBSERVED;这代表实验成功复现边界问题,不是自动化测试崩溃。未来若源码修复,结果应依据返回的实际字段重新判断。
16.8 Step E:Git Diff 快照漂移测试
node .\scripts\04-snapshot-drift.mjs --repo "$repo"
它先获取 V1 第一页,再修改临时文件并继续取 V2 页;如出现 SNAPSHOT_DRIFT_OBSERVED,说明本条件下存在版本混合。不要在真实生产仓库执行此类受控改写实验。
16.9 Step F:6 / 506 文件上下文策略对比
node .\scripts\05-context-scale.mjs --repo "$repo"
脚本使用真实 Workspace.readFile()、searchWorkspace()、gitDiff(),但文件和正确答案集合由教学 Fixture 构造;C 策略的检索词是预先设定的。记录 filesRead、estimatedTextBytes、recall 和 precision。
16.10 Windows/Linux/macOS、清理及错误处理
Linux/macOS 用户可在构建原仓库后,使用等价形式:
REPO="$HOME/c2c-ch10-source"
node scripts/00-preflight.mjs --repo "$REPO"
node --test tests/helpers.test.mjs
node scripts/01-search-benchmark.mjs --repo "$REPO"
node scripts/02-git-status-diff.mjs --repo "$REPO"
node scripts/03-utf8-diff.mjs --repo "$REPO"
node scripts/04-snapshot-drift.mjs --repo "$REPO"
node scripts/05-context-scale.mjs --repo "$REPO"
平台说明:本系列直接实测环境是 Windows;此处提供 Linux/macOS 的 Node.js 用法,不声称已经在这两个平台复测上述数据。
所有临时工作区默认在 finally 中清理;只有显式 --keep 才保留供检查。报错时先运行 00-preflight,再查 ZIP 内 故障排查.md。不要上传 .env、真实 Git 凭据、Token、工作区绝对路径或其他个人日志。
17. 证据分级:哪些是测试结论,哪些只是改进提案?
| 结论 | 证据类别 | 本文可声称的范围 |
|---|---|---|
| Node / rg 两查询的命中位置一致 | 实测 PASS | 当前 Windows、当前仓库、两个查询 |
| rg 外部进程启动成本突出 | 实测 + 合理分析 | 当前环境,不可精确分解纯启动时间 |
| Git 状态分类及三个重命名过滤 | 实测 PASS | 所列 Fixture 场景 |
| ASCII Diff 无损重组 | 实测 PASS | 静态、ASCII、5 页 |
| UTF-8 分页有损 | 实测 ISSUE | 中文/Emoji 长行与非法字符中间 offset |
| Diff 跨版本混合 | 实测 ISSUE | 受控两版本交错读取 |
| 506 文件时 C 策略召回 100% | 教学策略模拟 | 已知 Gold Set、预设检索词 |
| Evidence Contract / Snapshot | 设计建议 | 尚未在上游实现或验证 |
| MCP OAuth / Tunnel 端到端可靠性 | 未测试 | 不作肯定结论 |
本文没有重新验证所有 Linux/macOS 条件,也没有测试大规模二进制 Diff、Git LFS、网络文件系统与恶意路径组合,更没有证明生产系统已经发生同类事故。
18. 总结:可信 Agent 不只是“找到代码”
这一章从最普通的关键词搜索,走到了一个更严肃的问题:交给审查模型的上下文能否被证明是完整、无损、同一版本且来源明确?
核心结论可以压缩为五句话:
- Search 是候选定位机制,不是自动依赖分析。
truncated、范围与版本应进入审查证据,而不是只返回命中数量。- Git 的安全路径过滤与 Diff 文本完整性,是两个不同维度的保证。
- 静态 ASCII 分页通过,不代表中文、Emoji 或持续变化的工作区也安全可靠。
- 上下文成本与覆盖率应共同优化,真正可靠的 Reviewer 还需要可核对的证据契约。
下一章预告:【Codex with ChatGPT 源码学习⑪】多模态与文件边界——哪些数据能被模型看见,哪些不能,证据怎样从不同媒介穿过 MCP 边界?
参考资料
- 开源项目 GitHub 仓库
- 固定 Commit 源码树
- MCP Tool 注册:src/mcp/server.ts
- 搜索引擎:src/workspace/search.ts
- Git 状态与 Diff:src/workspace/git.ts
- Workspace 管理:src/workspace/manager.ts
- 忽略规则:src/workspace/ignore.ts
- ripgrep 官方项目
系列导航:参见同一系列的「导读」及其他章节。
ChatGPT 负责思考,Codex 负责干活?从源码读懂 Codex with ChatGPT(系列导读)
【Codex with ChatGPT 源码学习①】整体架构:为什么要分离控制平面与数据平面?
【Codex with ChatGPT 源码学习②】694 行 SKILL.md:Markdown 怎么指挥 Codex 干活?
【Codex with ChatGPT 源码学习③】iab、DOM、Playwright:Codex 如何操作 ChatGPT 网页?
【Codex with ChatGPT 源码学习④】深入 MCP Bridge:从 HTTP 请求到安全读取源码的完整链路
【Codex with ChatGPT 源码学习⑤】深入 OAuth 2.1、PKCE 与 Cloudflare Tunnel:ChatGPT 如何安全连接本地 MCP?
【Codex with ChatGPT 源码学习⑥】多 Agent 任务状态机深度解析:Task ID、Checkpoint、执行证据与崩溃恢复
【Codex with ChatGPT 源码学习⑦】安全与可靠性审计:OAuth、MCP、Prompt Injection 与崩溃恢复
【Codex with ChatGPT 源码学习⑧】深入 CLI、Daemon 与 Doctor:从进程自愈到并发启动的真实故障
【Codex with ChatGPT 源码学习⑨】配置、自动更新与跨平台部署:如何实现可验证、可回滚的 Agent 系统?
本文深入分析Codex with ChatGPT中Workspace Search与Git Diff两大模块的证据完整性缺陷:一是UTF-8分页导致字符边界截断引发的字节失真(6,467B→6,489B),二是Git Diff分页未绑定不可变快照导致跨版本混合。文章通过隔离实验复现问题,指出搜索结果≠依赖覆盖、Diff分页≠版本一致,并提出Evidence Contract设计原则,强调可信代码审查需保障内容完整性与版本一致性。

513

被折叠的 条评论
为什么被折叠?



