【Codex with ChatGPT 源码学习⑩】Workspace Search 与 Git Diff:从快速检索到可信代码审查——两项真实复现的证据完整性问题

本文深入分析Codex with ChatGPT中Workspace Search与Git Diff两大模块的证据完整性缺陷:一是UTF-8分页导致字符边界截断引发的字节失真(6,467B→6,489B),二是Git Diff分页未绑定不可变快照导致跨版本混合。文章通过隔离实验复现问题,指出搜索结果≠依赖覆盖、Diff分页≠版本一致,并提出Evidence Contract设计原则,强调可信代码审查需保障内容完整性与版本一致性。

【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 看到的代码,不一定是完整的代码;看到的多个片段,也不一定属于同一个工作区版本。

第十章要解决三个问题:

  1. 定位效率:面对数百、数千乃至更多文件,怎样找到相关代码?
  2. 上下文完整性:找到多少文件才足够,搜索结果为什么不能直接等于依赖覆盖?
  3. 证据可信度: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.tssearchWorkspace()、findRipgrep()、Node 回退、过滤与截断
src/workspace/git.tsgitStatus()、gitDiff()、重命名判断、敏感路径过滤、分页
src/workspace/manager.tsWorkspace 路径边界、文件读取、类型/大小检查
src/workspace/ignore.tsSensitive、Noise 和 .c2cignore 等路径规则

图 1:工程上下文获取逻辑(示意,而非固定执行顺序)

工程审查任务

Agent 选择检索策略

MCP 工具与 Scope

Workspace 边界

search_workspace

git_status 与 git_diff

Node 回退

ripgrep

命中位置与截断信息

变更位置与 Diff 分页

read_file 精确读取

Reviewer 组织审查证据

一个容易混淆的边界是: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 返回结构相同,不意味着行为完全一致

比较点ripgrepNode 回退
执行形态外部子进程当前 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 的变化只看暂存区会遗漏
untrackedGit 尚未追踪普通 git diff 不会自动包含正文
conflicted合并冲突需要独立处理冲突状态
hidden被隐藏的变更数量可知有被遮蔽内容,不公开敏感路径正文

隔离实验实际构造了 staged 新增文件、unstaged 修改文件、untracked 文本和已跟踪的 .env 修改。C2C 返回 hidden.changes=1,可见列表没有包含 .env。

这说明在该测试条件下路径隐藏确实生效;但不能推论普通文件中的所有敏感字符串也会被自动识别。


07. Git Diff:三种模式,三种审查基准

C2C gitDiff() 提供:

模式审查的是用途
unstaged工作区 vs Git Index看尚未暂存的修改
stagedGit 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 内容

是

否

git_diff 请求

name-status NUL 文件清单

解析普通变更与重命名

新旧路径是否敏感

排除整项

保留安全路径

分批执行 git diff

聚合 Diff 后分页

返回 offset 与 hasMore

为什么不能先生成完整的原始 Diff 再简单替换 .env 字样?因为那会让敏感内容已经进入内部读取与暂存过程。尽可能在采集前做边界判定,比生成后用字符串替换可靠。

7.2 双向重命名需要同时检查旧路径和新路径

本章实际使用 Git 的三条 R100 重命名测试:

场景Git 识别C2C 结果
a-before.ts → a-after.tsR100两侧路径可见
b-before.ts → .envR100两侧路径隐藏
.env.local → c-after.tsR100两侧路径隐藏

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返回字节数
10999
29991,000
31,9991,000
42,9991,000
53,9991,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:同一字符被切成两页的风险

原始 UTF-8:汉 与 Emoji

按字节偏移切片

第一页独立 UTF-8 解码

第二页独立 UTF-8 解码

不完整字符可能转为替换符

页文本拼接不等于原文

这次多出的 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:跨版本分页的形成

工作区 V1

gitDiff 读取第 1 页

工作区修改为 V2

gitDiff 重新生成剩余页

混合分页拼接

字节总数可以正确

内容却不属于同一个版本

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)两者命中位置
searchworkspace9.27568.4105 个,完全一致
requirescope8.74164.88711 个,完全一致

另外,对 rg --version 的同步进程启动诊断采样 9 次,中位耗时 59.746 ms(范围 59.028~64.382 ms)。

因此,“当前 Windows 小仓库中外部进程启动开销较突出”有了证据支持。但 spawnSync() 诊断与 C2C 的异步 spawn() 并不完全等价,不能严格推导 59.746 ms 就是每次搜索的纯启动成本。

这里的教训是:

比较工具性能,应衡量完整调用链;判断哪个工具更适合生产,还应考虑仓库规模、平台和结果完整性。

不能根据这两组小仓库测试就断言 Node 在任何情况下都比 ripgrep 快。


12. Node 搜索为何出现“匹配越多反而越快”?

同一 Node 回退搜索的真实数据:

QueryLimit返回数Truncated中位耗时
const33true0.805 ms
const5050true1.278 ms
const200200true2.055 ms
searchWorkspace505false8.694 ms
不存在的关键词500false8.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 全读6583 B100%66.7%
6 文件B 搜索再读3837 B75%100%
6 文件C Diff+搜索再读41,570 B100%100%
506 文件A 全读50624,583 B100%0.79%
506 文件B 搜索再读3837 B75%100%
506 文件C Diff+搜索再读41,570 B100%100%

在 6 文件样本中,A 直接读取反而文本量最少,因为额外的搜索 JSON 和 Diff 本身也是上下文开销。

在 506 文件样本中,C 将估算文本量从 24,583 B 降至 1,570 B(减少约 93.6%),同时保留了四个相关文件的完整召回。B 更省文本,但漏掉了关键的 format.ts。

13.2 实际读取耗时不能误用

506 文件样本中读取阶段耗时:

策略readStageMs
A260.517 ms
B1.573 ms
C3.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 中,不得将占位值当成真实证据。

建议至少建立以下不变量:

  1. offset + returnedBytes 必须与 nextOffset 协调,并且严格前进。
  2. 对 UTF-8 文本页,重新编码的字节长度应当等于该页报告的实际字节数。
  3. 同一审查对象的所有页面应当绑定同一份内容哈希或不可变快照。
  4. 文件路径、行号、工作区身份、工具版本、采集时刻和截断状态必须可追溯。
  5. Reviewer 应能明确声明证据覆盖范围和未检索区域,而非暗示“已经读完项目”。

图 5:面向 Planner/Reviewer 的证据链建议

否

是

Planner 明确问题与范围

Workspace Search 与 Git Diff

安全过滤与文件版本标识

Evidence 快照与内容哈希

read_file 补充上下文

截断与依赖覆盖是否充分

缩小范围或补充检索

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 不只是“找到代码”

这一章从最普通的关键词搜索,走到了一个更严肃的问题:交给审查模型的上下文能否被证明是完整、无损、同一版本且来源明确?

核心结论可以压缩为五句话:

  1. Search 是候选定位机制,不是自动依赖分析。
  2. truncated、范围与版本应进入审查证据,而不是只返回命中数量。
  3. Git 的安全路径过滤与 Diff 文本完整性,是两个不同维度的保证。
  4. 静态 ASCII 分页通过,不代表中文、Emoji 或持续变化的工作区也安全可靠。
  5. 上下文成本与覆盖率应共同优化,真正可靠的 Reviewer 还需要可核对的证据契约。

下一章预告:【Codex with ChatGPT 源码学习⑪】多模态与文件边界——哪些数据能被模型看见,哪些不能,证据怎样从不同媒介穿过 MCP 边界?


参考资料

  1. 开源项目 GitHub 仓库
  2. 固定 Commit 源码树
  3. MCP Tool 注册:src/mcp/server.ts
  4. 搜索引擎:src/workspace/search.ts
  5. Git 状态与 Diff:src/workspace/git.ts
  6. Workspace 管理:src/workspace/manager.ts
  7. 忽略规则:src/workspace/ignore.ts
  8. 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 系统?

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值