首页
/ ty 生态差异最小化:基于 Ruff 仓库 SKILL 文档的完整工作流解析

ty 生态差异最小化:基于 Ruff 仓库 SKILL 文档的完整工作流解析

2026-09-05 09:50:21作者:齐冠琰

本文以 Ruff 仓库中的 Agent 技能文档 minimizing-ty-ecosystem-changes/SKILL.md 为主体,完整拆解"当一个 ty(Ruff 的 Python 类型检查器)PR 在生态项目扫描中产生了行为差异时,如何精确复现并把它最小化为可复现样本"的标准流程。读完本文,你将掌握一套可执行的方法:从 Actions 运行元数据采集、双二进制(base/PR)构建,到 mypy-primer 项目还原与系统化缩减循环,理解 Ruff 团队如何保证每一次最小化都是"溯源链完整、因果不变"的严谨工程实践。

一、背景:这条工作流解决什么问题

Ruff 仓库内置类型检查器 ty(相关源码位于 crates/ty),其 PR 会通过 GitHub Actions 工作流 ty-ecosystem-analyzer.yaml 对一批真实开源项目(由 mypy-primer 项目清单定义)做"生态分析",用 base 分支与 PR 分支两个 ty 二进制的诊断输出差异来评估行为变化。当报告里出现某项目的诊断差异或 panic 时,需要一名调查者:

  1. 先精确复现报告中的差异(用与 CI 完全一致的修订号、依赖截止时间和分析模式);
  2. 再把差异最小化为一个尽量小的、自包含的复现文件。

这份 SKILL 文档就是指导整个过程的"操作规程",其核心立场可以概括为:先复现、后解释;一切候选样本必须从上一个已验证样本派生;保留的是触发原因,而不是某条诊断规则或显示类型

二、五条不变量(Invariants)

文档开头给出五条必须全程遵守的不变量,这是整篇规程的骨架:

  1. 使用精确的运行输入:Actions 运行中实际使用的 Ruff 修订号、用户级 PR 配置、依赖截止(EXCLUDE_NEWER)、mypy-primer 修订号、项目 Python 版本与分析严格度,全部照搬,不许替换为"大概的版本";
  2. 先复现,后解释:在解释差异或手写更小的示例之前,必须先复现报告中报告的原始项目差异;
  3. 复制的二进制与配置视为只读:每一次缩减都要同时在 base 与 PR 两个二进制上验证;
  4. 候选必须链式派生:每个候选样本都从上一个已验证的候选派生而来,绝不用独立构造的示例顶替;
  5. 保留底层触发器(trigger),而不只是保留诊断规则、消息文本或显示类型。

文档还强调:每次调查都从全新工件开始,不信任保留的记忆、历史最小化结果、上游项目的当前状态或辅助脚本的默认 lockfile。另外由于每次工具调用可能开启新 shell,所有直接调用 gh 的命令都要加 GH_TELEMETRY=false 前缀。

三、采集精确的运行元数据

3.1 已有共享清单时的处理

如果主 Agent 提供了不可变的 TY_ECOSYSTEM_RUN_METADATA 清单(manifest),先核对其 run ID 与 attempt 是否与冻结报告一致、且包含每个待调查项目;所有子 Agent 复用同一份只读清单,不得修改它或另生成分享清单。

3.2 使用辅助脚本采集

否则运行仓库自带的辅助脚本一次,传入 Actions 运行 ID(或 URL)、匹配的 attempt 以及所有受影响的 mypy-primer 项目名:

GH_TELEMETRY=false uv run --script scripts/collect_ty_ecosystem_run_metadata.py \
  <actions-run> <project-name>... \
  --attempt <actions-attempt> \
  --output target/ty-ecosystem-run.json

collect_ty_ecosystem_run_metadata.py 的源码可以看到,这个清单是如何被精确提取出来的:

  • Ruff 修订号parse_build_log 从构建任务日志中用正则 Merge base: <40位SHA>PR commit: <40位SHA> 提取 merge base 与 PR 修订号(见 第95-98行)。文档特别指出,当前工作流把编译拆成了 Build ty (base)Build ty (pr) 两个 job,脚本读取的是 base job——它同时记录了 merge base 与 PR merge 修订号——并且仍兼容历史上单个 Build ty job 的老运行;源码里 build_job 也确实同时匹配这两种 job 名。
  • EXCLUDE_NEWER 与 analyzer 修订号parse_shard_log 从第一个 analyze-shards (N) job 的日志中提取 EXCLUDE_NEWER: <ISO8601时间戳>第101-113行)。这与 ty-ecosystem-analyzer.yaml 中把 EXCLUDE_NEWER 设为 github.event.pull_request.updated_at 的做法对应。若日志中没有显式的 ECOSYSTEM_ANALYZER_COMMIT,脚本会退回读取该 PR 修订号下 uv.lock 里 ecosystem-analyzer 的 git 修订号(第116-132行)。
  • 项目 Python 版本parse_project_versions 用 AST 解析固定修订号下 mypy-primer 的 projects.py,提取每个 Project(...)locationname_overridemin_python_version,并与 ecosystem-analyzer 配置中的 MINIMUM_PYTHON_VERSION 取较大值(第135-217行)。
  • 一致性校验:脚本会校验 build 与 shard 日志中的 merge base 是否一致,否则直接报 MetadataError 中止(第309-310行)。

SKILL 文档的要求与之呼应:如果辅助脚本无法确定某个唯一值就停下来,绝不拿注释时间戳或本地默认值来顶替

四、准备 ty:构建精确修订号的双二进制

4.1 复用主 Agent 提供的二进制

若主 Agent 已经复制好了 base 与 PR 两个 profiling 二进制以及 PR 生态配置,则保留其绝对路径为 TY_ECOSYSTEM_BASE_BINARYTY_ECOSYSTEM_PR_BINARY,验证其存在后直接复用;不要重建二进制、切换共享 Ruff 的 ref 或覆盖共享工件。若需要精确修订号的 debug 二进制定位某个含糊的内部类型,应向主 Agent 申请;profiling 二进制始终作为行为判定基准(behavioral oracle)。

4.2 自行构建完整脚本

否则要求工作树干净、记住原始 ref,然后在分派任何子任务前把两个精确修订号都构建出来。复用 checkout 已有的 Cargo target 目录,把 profiling 二进制与 PR 生态配置复制到 target/ty-ecosystem-bins,完成后恢复原始 ref。PR 修订号需要显式 fetch,因为 pull-request 运行通常使用 GitHub 合成的 merge commit,普通 clone 拿不到:

set -euo pipefail

test -z "$(git status --short)" || { git status --short; exit 1; }
original_ref="$(git symbolic-ref --quiet --short HEAD || git rev-parse HEAD)"
GH_TELEMETRY=false git fetch https://github.com/astral-sh/ruff.git <pr-revision>
mkdir -p target/ty-ecosystem-bins
trap 'git checkout "$original_ref"' EXIT

artifact_dir="$PWD/target/ty-ecosystem-bins"
build_target_dir="${CARGO_TARGET_DIR:-target}"
export CARGO_PROFILE_PROFILING_DEBUG=line-tables-only

git checkout --detach <merge-base>
cargo build --package ty --profile profiling
cp "$build_target_dir/profiling/ty" "$artifact_dir/ty-base"

git checkout --detach <pr-revision>
cp .github/ty-ecosystem.toml "$artifact_dir/ty-ecosystem.toml"
cargo build --package ty --profile profiling
cp "$build_target_dir/profiling/ty" "$artifact_dir/ty-pr"

几个值得注意的工程细节:

  • set -euo pipefail + trap ... EXIT 保证中途失败也会恢复原始 ref,不污染共享 checkout;
  • CARGO_PROFILE_PROFILING_DEBUG=line-tables-only 让 profiling profile 只保留行表调试信息,在保留 backtrace 可用性的同时减小二进制;
  • 复制 .github/ty-ecosystem.toml 作为"用户级配置"的素材,其内容(第1-16行)是一组默认关闭、生态分析时统一开启为 warn 的规则,如 blanket-ignore-commentdivision-by-zeromissing-type-argumentpossibly-unresolved-referenceunsound-return-statementunsound-yieldunsupported-dynamic-base 等。

构建完成后,查看 vendored 定义或 Rust 实现时,用 git -C <ruff-checkout> show <exact-revision>:<repository-relative-path> 按不可变清单中选定的 merge-base 或 PR 修订号取内容——绝不能假设工作树文件与任一被分析的二进制一致,更不能切换共享 checkout 的 ref。

五、复现:还原 mypy-primer 项目与 CI 环境

复现阶段的要求(对应文档 "Reproduce" 一节)可以拆成四步:

5.1 还原项目到报告中的精确修订号

为每个项目创建唯一临时目录并使用其绝对路径;Python 版本与 pin 住的 mypy-primer 修订号从共享清单读取;项目修订号从原始诊断 source permalink 的 /blob/<commit>/ 部分获得,并核对同一项目的多个链接是否一致。若没有诊断 permalink,则检查对应的 diagnostics shard 或 Actions 日志;若确实无法恢复精确修订号,要明确报告这一限制

5.2 绕过相邻脚本 lockfile 拉取项目

GH_TELEMETRY=false uv run \
  --python <project-python> \
  --with "mypy-primer @ git+https://github.com/hauntsaninja/mypy_primer@<mypy-primer-revision>" \
  --no-project \
  python scripts/setup_primer_project.py \
  <project-name> <absolute-temporary-directory> \
  --revision <report-project-revision> \
  --exclude-newer <EXCLUDE_NEWER>

这与 setup_primer_project.py 模块 docstring 的说明完全一致:生态报告复现必须选择项目对应的 ecosystem-analyzer Python 版本,并绕过脚本旁锁文件(--no-project + 显式 --with pin),--exclude-newer 仍用于约束 mypy-primer 的注册表依赖。脚本还支持 --print-ty-command 只打印项目特定的 ty 命令而不实际拉取项目(第108-112行);项目命令由 get_ty_command 根据项目的 ty_cmd 模板(缺省为 {ty} check {paths}{ty} check)拼装,并追加 --python <venv> --output-format concise第83-89行)——SKILL 文档中 run_ecosystem_ty 函数里的 <project-specific command printed by setup_primer_project.py> 就是指这条命令。

5.3 安装用户级配置、锁定分析模式

  • 把生态配置作为用户级配置使用,与 CI 保持一致,且不取代项目级配置发现;每个新 shell 重新导出 XDG_CONFIG_HOMERUST_BACKTRACE=1。若主 Agent 提供了 TY_ECOSYSTEM_CONFIG_HOME,复用其已安装配置且不修改;否则把复制来的配置安装到本地。
  • 从冻结的详细报告读取项目的 strict / non-strict 标签(或从对应 diagnostics shard 的 strict_settings 值),两个二进制都必须保持该模式运行:
if [[ -n "${TY_ECOSYSTEM_CONFIG_HOME:-}" ]]; then
  export XDG_CONFIG_HOME="$TY_ECOSYSTEM_CONFIG_HOME"
  test -f "$XDG_CONFIG_HOME/ty/ty.toml" || exit 1
else
  export XDG_CONFIG_HOME="$PWD/target/ty-ecosystem-config"
  mkdir -p "$XDG_CONFIG_HOME/ty"
  cp "$PWD/target/ty-ecosystem-bins/ty-ecosystem.toml" "$XDG_CONFIG_HOME/ty/ty.toml"
fi
unset TY_CONFIG_FILE
export RUST_BACKTRACE=1

project_dir="<absolute-temporary-directory>"
ty_base="${TY_ECOSYSTEM_BASE_BINARY:-$PWD/target/ty-ecosystem-bins/ty-base}"
ty_pr="${TY_ECOSYSTEM_PR_BINARY:-$PWD/target/ty-ecosystem-bins/ty-pr}"
test -x "$ty_base" && test -x "$ty_pr" || exit 1
ecosystem_analysis_mode="<strict-or-non-strict-from-detailed-report>"

if [[ "$ecosystem_analysis_mode" != strict && "$ecosystem_analysis_mode" != non-strict ]]; then
  echo "Unknown ecosystem analysis mode: $ecosystem_analysis_mode" >&2
  exit 1
fi

run_ecosystem_ty() {
  if [[ "$ecosystem_analysis_mode" == strict ]]; then
    <project-specific command printed by setup_primer_project.py> \
      --config analysis.strict-equality-semantics=true \
      --config analysis.strict-generic-narrowing=true
  else
    <project-specific command printed by setup_primer_project.py>
  fi
}

cd "$project_dir"
ty_binary="$ty_base"
base_exit_status=0
run_ecosystem_ty || base_exit_status=$?
ty_binary="$ty_pr"
pr_exit_status=0
run_ecosystem_ty || pr_exit_status=$?

要点:strict 模式下追加 --config analysis.strict-equality-semantics=true--config analysis.strict-generic-narrowing=true 两个配置项;unset TY_CONFIG_FILE 防止环境变量意外指向别处的配置文件。

5.4 判定复现成功

  • 必须与详细报告中的差异逐项确认,包括重复诊断(duplicate diagnostics)与两侧退出码;
  • 复现间歇性严重失败时,按报告给出的运行次数在每一侧重复执行;
  • 普通诊断产生退出码 1 是正常现象,不要误判为复现失败;
  • 对 panic,通过比较 Rust panic 位置/决定性因果帧与 panic 载荷来确定稳定指纹;忽略被检查的 Python 文件路径这类非本质性的 backtrace 差异。

六、最小化:目标、缩减循环与最终审计

6.1 目标

最小化的目标是"完全最小化、保留溯源链(provenance)"的复现样本:首选单个自包含文件,没有可避免的第三方或标准库导入,没有不必要的定义、注解、分支或高级语言特性。第三方导入只有在确认 ty 行为依赖于该库的身份或其"第三方搜索路径分类"时才保留。

文档要求:在做任何生态差异最小化之前,必须先阅读并遵循配套参考 references/advanced-minimization.md,穷尽其完整缩减循环(包括第三方依赖与标准库的内联),并且只有在验证过"既不能通过删除、也不能通过内联来保持原有行为"之后,才允许保留某个导入

6.2 六阶段缩减循环

参考文档 advanced-minimization.md 给出的系统化循环(每次只做一次受控缩减,每次改动后在两个二进制上重跑,只有当原始差异与底层触发器都保持时才保留该缩减;每成功一次缩减都要回到第 1 步重新开始,因为它可能解锁更早的缩减机会):

  1. 删除无关文件;
  2. 移除导入、定义、装饰器、注解、语句和分支;
  3. 把第一方(first-party)定义内联进复现文件;
  4. 对每个必需的第三方依赖:先把整个已安装依赖(包括它提供的所有包目录与模块)完整复制为源码树内的一手代码——不要先只复制"看起来相关"的文件——调整导入、验证完整复制后差异仍可复现,然后才开始从中删减。若完整复制改变了行为(因为 ty 对该库有特殊处理,或区分第一方/第三方搜索路径),要在对应分析的 Ruff 修订号下找出相关的 ty 实现,然后才保留原导入。若不得不克隆依赖,必须使用精确的安装修订号/版本;
  5. 内联标准库定义时,使用 git -C <ruff-checkout> show <exact-revision>:crates/ty_vendored/<path>crates/ty_vendored 中被分析的精确修订号提取(merge base 与 PR 两侧定义不同时要做比较);
  6. 用更简单的等价物替换复杂构造(如能保持差异就去掉 walrus 表达式、替换 protocol)。

循环要重复到"完整过一遍所有阶段都找不到可保持差异的进一步缩减"为止。不能因为"理解了可能原因"或"复现已经很小"就停下

6.3 因果验证与完成判据

  • 诊断相同或显示类型相同并不等于原因相同。输出含糊时,要用精确修订号的 debug 输出、针对性的 reveal_type,或与对应分析修订号匹配的 Rust 调用点来识别并比较原始触发器与最小化后的触发器;
  • 最小化只有当"一条经验证的缩减链把最终复现样本连回了原始生态条目,且穷尽式检查找不到进一步缩减"时才算完成;
  • 若确实遇到外部阻塞无法完成,要报告阻塞点并把该最小化标记为不完整——原始源码摘录不算成功的最小化结果。

6.4 最终审计

参考文档的 Final Audit 一节要求:对每一个仍然保留的导入(包括标准库与第三方)都尝试删除并内联其定义,只有验证后仍不能保持行为才保留;第三方导入还要额外验证"模块身份/第三方搜索路径分类是必需的",并指出 ty 在哪实现了该行为。"图方便、熟悉的 API、类名相同、保留诊断里的模块拼写"都不构成保留理由。任何存活的导入都要记录理由(第三方导入还要记录 ty 的实现位置),这些记录作为工作证据保留,是否进入最终工件由调用方决定。最后验证缩减链连通性,并在调查结束后删除临时的项目与依赖副本。

七、交付物(Return)

调查完成时,交付内容应包含:

  1. 原始带 permalink 的报告条目;
  2. base 与 PR 两侧行为的精确描述;
  3. 最小化后的代码;
  4. 完整的诊断消息与错误码(或 panic 指纹);
  5. 复现所需的清单与命令。

若从汇总工作流(summary workflow)被调用,则 import-audit 与缩减笔记要与可进入报告的 Markdown 分开返回

八、小结:这套规程的工程设计

通读 SKILL.md 及其配套脚本与参考文档,可以归纳出 Ruff 团队在类型检查器行为回归调查上的几个一致设计原则:

  • 输入不可变:所有版本号、修订号、Python 版本、依赖截止时间都从 Actions 运行的不可变元数据(构建日志、shard 日志、uv.lock、mypy-primer 源码 AST 解析)机械提取,任何一步取不到唯一值就中止,杜绝"拿差不多版本先试试";
  • 双二进制对照:base 与 PR 两个精确修订号的 profiling 二进制是唯一的判定基准,所有缩减都在两侧同时验证,退出码与重复诊断都纳入比较;
  • 链式溯源:禁止独立手写"我觉得等价"的示例,每一步缩减都可回放到上一状态,最终审计要求还原完整链条;
  • 因果优先于表象:明确区分"诊断相同"与"原因相同",要求用 debug 输出、reveal_type 或 Rust 调用点确认底层触发器;
  • 诚实的失败语义:无法恢复修订号就报告限制,无法完成最小化就标记 incomplete,原始摘录不算成功结果。

这套由 collect_ty_ecosystem_run_metadata.pysetup_primer_project.py.github/ty-ecosystem.tomlty-ecosystem-analyzer.yaml 共同支撑的流程,为"快速类型检查器如何安全地演进行为"提供了一个可复制的调查方法论样本。

登录后查看全文
热门项目推荐
相关项目推荐