ty 生态差异最小化:基于 Ruff 仓库 SKILL 文档的完整工作流解析
本文以 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 时,需要一名调查者:
- 先精确复现报告中的差异(用与 CI 完全一致的修订号、依赖截止时间和分析模式);
- 再把差异最小化为一个尽量小的、自包含的复现文件。
这份 SKILL 文档就是指导整个过程的"操作规程",其核心立场可以概括为:先复现、后解释;一切候选样本必须从上一个已验证样本派生;保留的是触发原因,而不是某条诊断规则或显示类型。
二、五条不变量(Invariants)
文档开头给出五条必须全程遵守的不变量,这是整篇规程的骨架:
- 使用精确的运行输入:Actions 运行中实际使用的 Ruff 修订号、用户级 PR 配置、依赖截止(
EXCLUDE_NEWER)、mypy-primer 修订号、项目 Python 版本与分析严格度,全部照搬,不许替换为"大概的版本"; - 先复现,后解释:在解释差异或手写更小的示例之前,必须先复现报告中报告的原始项目差异;
- 复制的二进制与配置视为只读:每一次缩减都要同时在 base 与 PR 两个二进制上验证;
- 候选必须链式派生:每个候选样本都从上一个已验证的候选派生而来,绝不用独立构造的示例顶替;
- 保留底层触发器(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 tyjob 的老运行;源码里 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(...)的location、name_override、min_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_BINARY 与 TY_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-comment、division-by-zero、missing-type-argument、possibly-unresolved-reference、unsound-return-statement、unsound-yield、unsupported-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_HOME与RUST_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 步重新开始,因为它可能解锁更早的缩减机会):
- 删除无关文件;
- 移除导入、定义、装饰器、注解、语句和分支;
- 把第一方(first-party)定义内联进复现文件;
- 对每个必需的第三方依赖:先把整个已安装依赖(包括它提供的所有包目录与模块)完整复制为源码树内的一手代码——不要先只复制"看起来相关"的文件——调整导入、验证完整复制后差异仍可复现,然后才开始从中删减。若完整复制改变了行为(因为 ty 对该库有特殊处理,或区分第一方/第三方搜索路径),要在对应分析的 Ruff 修订号下找出相关的 ty 实现,然后才保留原导入。若不得不克隆依赖,必须使用精确的安装修订号/版本;
- 内联标准库定义时,使用
git -C <ruff-checkout> show <exact-revision>:crates/ty_vendored/<path>从 crates/ty_vendored 中被分析的精确修订号提取(merge base 与 PR 两侧定义不同时要做比较); - 用更简单的等价物替换复杂构造(如能保持差异就去掉 walrus 表达式、替换 protocol)。
循环要重复到"完整过一遍所有阶段都找不到可保持差异的进一步缩减"为止。不能因为"理解了可能原因"或"复现已经很小"就停下。
6.3 因果验证与完成判据
- 诊断相同或显示类型相同并不等于原因相同。输出含糊时,要用精确修订号的 debug 输出、针对性的
reveal_type,或与对应分析修订号匹配的 Rust 调用点来识别并比较原始触发器与最小化后的触发器; - 最小化只有当"一条经验证的缩减链把最终复现样本连回了原始生态条目,且穷尽式检查找不到进一步缩减"时才算完成;
- 若确实遇到外部阻塞无法完成,要报告阻塞点并把该最小化标记为不完整——原始源码摘录不算成功的最小化结果。
6.4 最终审计
参考文档的 Final Audit 一节要求:对每一个仍然保留的导入(包括标准库与第三方)都尝试删除并内联其定义,只有验证后仍不能保持行为才保留;第三方导入还要额外验证"模块身份/第三方搜索路径分类是必需的",并指出 ty 在哪实现了该行为。"图方便、熟悉的 API、类名相同、保留诊断里的模块拼写"都不构成保留理由。任何存活的导入都要记录理由(第三方导入还要记录 ty 的实现位置),这些记录作为工作证据保留,是否进入最终工件由调用方决定。最后验证缩减链连通性,并在调查结束后删除临时的项目与依赖副本。
七、交付物(Return)
调查完成时,交付内容应包含:
- 原始带 permalink 的报告条目;
- base 与 PR 两侧行为的精确描述;
- 最小化后的代码;
- 完整的诊断消息与错误码(或 panic 指纹);
- 复现所需的清单与命令。
若从汇总工作流(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.py、setup_primer_project.py、.github/ty-ecosystem.toml 与 ty-ecosystem-analyzer.yaml 共同支撑的流程,为"快速类型检查器如何安全地演进行为"提供了一个可复制的调查方法论样本。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00