跳到主要内容

ADR 0060 — 代码的读取面,受约束且有预算

  • 状态: 已接受
  • 日期: 2026-08-08
  • 先前背景: ADR 0001(git 的规范化契约以及进入其中的内容)、ADR 0058(中央的路径约束,RN-092)、ADR 0055(这一约束复用的范围原语)

背景​

FASE 26a 为 GitProviderContract 增加了 listTree 和 getPullRequestDiff,由唯一的测试套件在三个 provider 中证明。缺的是使用者——而 26a 自己安装的锁(SEM_CONSUMIDOR_AINDA)在它存在之前会让 CI 失败。

使用者是 Code 标签页,而编写它迫使我们决定三件之前任何代码都没有决定过的事,因为产品到目前为止没有任何路由从客户端接收文件路径。

第一是搜索放在哪里。 标签页需要四种读取:树、内容、搜索和 PR 的 diff。其中三种是契约的操作。搜索不属于三个 provider 中的任何一个:GitHub 和 GitLab 有平台的代码搜索,各有自己的语义和限制;LocalGitProvider 是一个 bare repo,什么都没有。在契约中声明 search 意味着要么在 local 上有一个能力为 false 的第 13 个操作——一个在两个 provider 上工作、在第三个上消失的标签页——要么把某个平台的代码搜索词汇引入规范化契约,而它的存在恰恰是为了不让 Octokit 的形态泄漏。

第二是成本。 读取每次调用便宜,重复起来昂贵。树和 diff 在契约中已经有上限(26a),但它们限制的是一个响应;组合搜索要做 N 次调用,而 N 随仓库的大小而不是请求的大小增长。付费的是 workspace owner 的凭据(RN-058/RN-082),rate limit 属于 provider。产品已经近距离见过这类缺陷:仪表盘每分钟发出 3.824 个请求,因为每个项目都在请求自己的(RN-090),由此产生的 429 变成了白屏。

第三是路径。 查询字符串中的 ../../etc/passwd 到达 handler 时已经被 Express 解码。在 github/gitlab 中,这个路径变成 provider API 的 URL 片段,所以 .. 不会读错文件:它会更换端点,手里握着 owner 的 token。在 local 中,它变成 git show <ref>:<path> 的右侧。这与 ADR 0058 为 projectId 关闭的是同一类问题,现在应用到文件路径上——而 FASE 14d 已经教会了值得避免的故障模式:测试零件不等于测试通往它的路径。

决策​

搜索留在 git 契约之外**,在应用层组合,带预算。** ReadProjectCodeUseCase 以广度优先遍历树,调用 listTree,并用 getFileContent 打开文件。三个预算会让它停下——遍历的目录、打开的文件、返回的匹配——而 truncated 表示它停了。用广度而不是深度,因为在中途被截断时,它交付的是最浅的文件,那正是搜索者通常看的地方。一个短 TTL(30s)、条目数有限的缓存,避免浏览和搜索重复同样的调用;短是因为标签页读取的是一个活的分支,长 TTL 会显示已经改变的代码。

路径约束是一个函数,位于 RN-092 的文件中。 caminhoDeRepositorioContido() 通过 projectScopeRoot() 把客户端的路径锚定在项目文件夹中,并复用 ADR 0055 的 normalizarCaminho/dentroDoEscopo。它返回规范化的路径,调用者使用返回的东西——返回原始路径会允许检查一个字符串却把另一个发给 provider。绝对路径会被拒绝,即使该名称在仓库内存在:重新解释开头的斜杠会是一种静默转换。ref 在同一个地方检查,其中的 .. 会被拒绝,因为对 git 来说 dev..main 是提交区间,而不是修订。没有任何路由自行验证路径。

四个路由都是 GET,role:viewer,控制器没有写入动词。 查看项目的代码与查看项目是同样的权限。读取不变成 proposed_action:它不是外部效果,把它变成审批动作会让队列充满噪声,直到没人再读真正的那些。使用的凭据是 owner 的,由与写入相同的解析器给出。

后果​

CodeQL 会继续指出,这是接受的代价。 位于另一个函数中的屏障它看不到——正是这一点让三个 js/path-injection 在 PÓS-FASE 15 的修正之后存活下来,而当时的决定是保持中央检查并在面板上付出代价。这里没有改变:在每个路由中复制约束会让告警安静,却产生四份终有一天会分歧的副本。来自这条路径的新告警要带书面理由驳回,绝不静默。

这个搜索不是 GitHub 的搜索,也不应假装是。 它不建索引,不理解语法,不按相关性排序,在大仓库中它会截断。与使用者的契约是 truncated + filesScanned:界面说明发生了截断,并建议细化 path。把它换成平台的代码搜索是未来的决定,而且必须先解决 local provider 的不对称。

缓存在能访问同一项目的人之间共享。 键中没有用户,因为到达的人已经通过了路由的 role:viewer,而仓库对他们所有人都一样。如果有一天读取开始取决于谁在读,键就需要增加这个维度——这写在代码中,因为这是那种会悄悄消失的前提。

30 秒的窗口是可见的延迟。 在产品之外做的推送可能需要长达半分钟才会出现在标签页中。真正的失效需要知道仓库何时改变,而产品对外部推送没有这个信号;在它存在之前,短 TTL 是诚实的选择。

明确留在外面的: 语法高亮(新依赖,本阶段第 35 项,26c 的决定)、交互式终端(依赖 FASE 25 的按项目容器)以及通过标签页的任何写入。当写入到来时,它以 proposed_action 诞生——而这个控制器连一个写入动词都没有,正是这一点让这条边界可以验证,而不只是一个意图。