Show HN: RealDiff – 针对拉取请求的运行时行为差异分析(支持六种语言)
摘要
RealDiff 是一款开源工具,它通过比较测试输出来检测拉取请求中的运行时行为变更,甚至能在未直接编辑的文件中高亮显示非预期影响。
查看缓存全文
缓存时间: 2026/08/27 15:24
issacnitin/RealDiff 来源:https://github.com/issacnitin/RealDiff
RealDiff CI (https://github.com/issacnitin/RealDiff/actions/workflows/ci.yml) Release (https://github.com/issacnitin/RealDiff/releases/tag/v0.4.0)
许可证:MIT
.NET 8 (https://dotnet.microsoft.com/download/dotnet/8.0)
RealDiff 能够发现普通源码审查遗漏的运行时行为变化。它会构建两个 Git 修订版本,观察其测试表现,基于三次基线运行学习噪声基线,并报告每个调用树中首次发生的行为变化。
源码差异告诉你修改了什么,而 RealDiff 告诉你这些修改产生了什么效果,包括影响了那些在拉取请求中未触及的文件。
为何使用它?
一个看似无害的重构可能改变远离被编辑文件的行为:
public static List<(int Priority, T Value)> ByPriority(
this IEnumerable<(int Priority, T Value)> src)
-{
- var list = src.ToList();
- list.Sort((a, b) => a.Priority.CompareTo(b.Priority));
- return list;
-}
+ this IEnumerable<(int Priority, T Value)> src) =>
+ src.OrderBy(item => item.Priority).ToList();
List.Sort 是不稳定排序;OrderBy 是稳定排序。在包含的演示中,这一行基础设施代码的修改改变了未被编辑的定价引擎:
RealDiff: 发现 1 处位于此差异之外的行为变化
DiscountEngine.SelectDiscount 原返回 "CLEARANCE_40",现返回 "SEASONAL_15"。
CheckoutTotals.Compute 原返回 60,现返回 85。
执行此逻辑的 3 个测试中,有 2 个未对变化添加断言。
被编辑的辅助方法位于 Infrastructure.Collections;观察到的效果出现在 Commerce.Pricing。
运行以下包含的演示,或查看维护的公共 .NET 演示拉取请求 (https://github.com/issacnitin/realdiff-sort-dotnet/pull/1) 及其成功托管运行记录 (https://github.com/issacnitin/realdiff-sort-dotnet/actions/runs/33064892219)。
五分钟 .NET 演示
此 .NET 演示的前置要求:Git、.NET 8 SDK 和 PowerShell 7。
Java 分析额外需要 JDK 和仓库的 Maven/Gradle 包装器或相应系统工具;Node 分析需要 Node.js 及其锁文件指定的包管理器。
git clone https://github.com/issacnitin/RealDiff.git
cd RealDiff
dotnet build RealDiff.sln -c Release
pwsh -File tools/verify-diffs.ps1 -Mutate -Change sort
该证明会创建一个临时的提议变更树,仅修改 SortingExtensions.cs,运行两次基线测试加上一次变更测试,并写入 findings.json。它验证:
- 被编辑的文件贡献了零个被追踪的成员;
- 边界位于未编辑项目
Commerce.Pricing.DiscountEngine.SelectDiscount中; - 两个调用点发生了变化但未有断言响应;
- 五个分歧键合并为三个边界节点;
- 等优先级选择在全新的进程中是确定性的。
运行所有维护的演示模式:
pwsh -File tools/verify-demo-fixtures.ps1
这涵盖了排序稳定性、重试策略和配置解析。
公开的可执行文件是一个轻量级的 Rust 启动器,负责参数路由、仓库配置加载和 detect。它会启动一个并行的自包含托管组件,用于引用解析、构建、缓存、插桩和发布。架构设计采用一个语言无关的追踪契约、每个运行时一个追踪器,以及一个单遍流式 Rust 差异、边界和发现引擎:
flowchart LR
L[Rust argv, config, detect] --> O[Managed orchestration]
O --> D
O --> J
O --> N
O --> G
O --> R
O --> P
D[.NET / Cecil] --> T[realdiff.trace/1]
J[Java / javaagent + ASM] --> T
N[Node / CJS + ESM + Babel] --> T
G[Go / stable AST rewrite] --> T
R[Rust / stable syn rewrite cache] --> T
P[Python / PEP 669 sys.monitoring] --> T
T --> E[Rust matching, noise, frontier, and findings]
E --> F[findings.json]
F --> P[GitHub, Azure DevOps, MCP]
TRACE-FORMAT.md 是追踪器与引擎之间的契约。维护的 .NET、Java、Node、Go、Rust 和 Python 网关应用相同的符合性规则:相同的方法集、每个键的事件计数和进入序号、源代码触发点、摘要证明以及零引擎分歧(对于非空运行)。
状态:早期预览版。
统一的 CLI 可从常规根标记检测 .NET、Maven/Gradle Java、npm/pnpm/Yarn/Bun Node、Go 模块、Cargo Rust 和 Python 3.12+ 仓库。
支持的语言
| 语言 | 插桩方式 | 测试/源码集成 | 当前限制 |
|---|---|---|---|
| .NET 8 | Mono.Cecil 构建时 IL 织入 | xUnit 和可移植 PDB | 属性、事件和运算符是策略排除项。类型初始化器在结构上不可观察,因为钩子在 CLR 类型初始化锁下运行,可能导致启动死锁。 |
| Java | java.lang.instrument 代理配合 ASM | Maven/Gradle、JUnit/TestNG 注解、推断或配置的源根 | Gradle 源集推断覆盖字面量 srcDir/srcDirs 声明;动态源集配置需要 source_roots。集合形状规则需要 java.util 模块访问。类初始化器在结构上不可观察,因为钩子在 JVM 类初始化锁下运行,可能导致启动死锁。 |
| Node / TypeScript | CommonJS require hook 和 ESM 加载器配合 Babel | npm、pnpm、Yarn Classic/Berry、Bun、直接 JavaScript 位置、TypeScript 源映射、Jest/Vitest 适配器 | 需要恰好一个支持的锁文件;工作线程超出范围;生成器和不支持的调用对象将被跳过。 |
| Go | 稳定的模块感知 AST 重写至构建缓存 | go test、原始 .go 解析器位置 | 动态接口/函数边界和未重写的 goroutine 边界是显式跳过的。 |
| Rust | 稳定的 syn/quote 重写至 SHA-256 构建缓存 | cargo test、结构化 #[test] 根、原始 .rs 解析器位置 | 宏展开、外部/常量调用对象、联合体、trait 对象和依赖拥有的值在结构上不可达,因为稳定的源码重写无法进入展开的/编译器拥有的代码或将读取器注入到依赖源中。MIR 原型发出零运行时事件。 |
| Python 3.12+ | PEP 669 sys.monitoring 在进程启动时附加;无构建、字节码织入或 AST 重写 | pytest 和 unittest 结构根;co_filename/co_firstlineno;源码 AST 清单 | 原生/C 调用对象在结构上不可观察,因为它们没有用于 sys.monitoring 的 Python 帧;没有仓库源码的合成代码以及模块/类设置主体是显式不支持的边界。Python 3.11 及更早版本被拒绝;没有 sys.settrace 回退。 |
未解析的 TypeScript 源映射不被视为追踪器限制:RealDiff 拒绝猜测原始路径,将源记录为未解析,并降低归属置信度而不是声称可能错误的文件。
每个追踪器发出相同的进程范围 NDJSON 契约和协调的覆盖清单。报告为已插桩的成员必须具备发出事件的能力,每个模块必须满足 discovered = instrumented + skipped 且零补丁失败。
Python 与编译型追踪器不同,因为没有构建命令可以注入。RealDiff 将其暂存的 sitecustomize.py 添加到 PYTHONPATH 前部,在目标导入之前附加 sys.monitoring,并运行仓库测试(不作更改)。一个无副作用的 AST 传递仅为覆盖清单盘点源码成员;运行时事件完全来自 PEP 669。
基线追踪缓存保持有效:其键包括暂存 Python 追踪器目录指纹、Python major.minor.micro、有效包含/排除范围和编辑配置。
对于源解析,仓库下方的真实 co_filename 加 co_firstlineno 是 debugInfo;缺失/合成文件名是 debugInfoMissing;不可规范化或外部的真实路径是 unresolved。Python 不需要 generatedState,因为挂起的调用对象保留其原始代码对象,并且没有安全的 declaringType 回退。
Python 值支持是显式的:
| 置信度 | 形状 |
|---|---|
| 精确 | None、缺失值、布尔值、任意整数、浮点数(包括规范 NaN 和不同的 -0.0)、字符串、字节,以及精确的内置 list、tuple、dict、set 和 frozenset;当没有其他状态通道存在时,完整的实例 __dict__ 状态。 |
| 部分 | 属性、__slots__、__getattr__、覆盖的 __getattribute__、容器子类、不可读字段、深度/广度限制和显示截断。每个不可读区域发出一个计数的 、、或 标记。 |
| UnsupportedShape | 没有 Python 代码对象的原生/C 调用对象、没有仓库源码的动态/合成代码,以及可执行的模块/类设置主体。 |
规范化器从不调用属性、描述符、用户迭代器、repr、相等性、哈希、格式化回调、__getattr__ 或覆盖的 __getattribute__。编辑在完整规范值被哈希后应用,匹配其他语言契约。
跨语言发布演示
v0.4.0 版本通过六个公共排序稳定性拉取请求进行演练。每个夹具恰好有三个测试,修改一个配置文件,两个广泛的断言通过,而一个精确的平局获胜者断言发生反应。在每个打包运行中,被编辑的文件贡献零个被追踪的成员,边界位于未编辑的定价代码中,至少一个调用点未被测试,且没有工具或清单噪声存在。
| 语言 | 匹配的键数 | 边界合并 | 拉取请求 | 托管工作流 |
|---|---|---|---|---|
| .NET | 319 | 9 到 3 (3.0x) | realdiff-sort-dotnet#1 (https://github.com/issacnitin/realdiff-sort-dotnet/pull/1) | workflow (https://github.com/issacnitin/realdiff-sort-dotnet/actions/workflows/realdiff.yml) |
| Node | 129 | 117 到 3 (39.0x) | realdiff-sort-node#1 (https://github.com/issacnitin/realdiff-sort-node/pull/1) | workflow (https://github.com/issacnitin/realdiff-sort-node/actions/workflows/realdiff.yml) |
| Java | 132 | 117 到 3 (39.0x) | realdiff-sort-java#1 (https://github.com/issacnitin/realdiff-sort-java/pull/1) | workflow (https://github.com/issacnitin/realdiff-sort-java/actions/workflows/realdiff.yml) |
| Go | 315 | 9 到 3 (3.0x) | realdiff-sort-go#1 (https://github.com/issacnitin/realdiff-sort-go/pull/1) | workflow (https://github.com/issacnitin/realdiff-sort-go/actions/workflows/realdiff.yml) |
| Rust | 312 | 9 到 3 (3.0x) | realdiff-sort-rust#1 (https://github.com/issacnitin/realdiff-sort-rust/pull/1) | workflow (https://github.com/issacnitin/realdiff-sort-rust/actions/workflows/realdiff.yml) |
| Python | 310 | 6 到 3 (2.0x) | realdiff-sort-python#1 (https://github.com/issacnitin/realdiff-sort-python/pull/1) | workflow (https://github.com/issacnitin/realdiff-sort-python/actions/workflows/realdiff.yml) |
每种语言的两次全新打包运行产生了相同的行为规范化产物。每个托管工作流也恰好发布了一条 RealDiff 拉取请求评论。
安装 CLI
使用全语言容器
已发布的 Linux 镜像包含 RealDiff CLI、默认的 Rust 差异引擎、.NET 8 SDK/追踪器、Java 17 代理、Node 24 追踪器、Go 重写器、稳定的 Rust 工具链/追踪器以及带有 pytest 的 Python 3.12 sys.monitoring 追踪器。主机只需要 Docker:
docker pull ghcr.io/issacnitin/realdiff:v0.4.0
docker run --rm \
--volume "$PWD:/workspace" \
ghcr.io/issacnitin/realdiff:v0.4.0 \
/workspace --base origin/main --pr HEAD \
--findings /workspace/.realdiff/artifacts/findings.json
常规镜像入口点是 realdiff;不涉及 PowerShell 包装器。
统一的 CLI 协调 .NET、Java、Node/TypeScript、Go、Rust 和 Python 仓库。当前本地验证的 Linux/amd64 镜像大小为 926,593,250 字节(通过 Docker 镜像检查),包含稳定的 Rust 1.98、带 pytest 的 Python 3.12 和本地链接器。容器工作流报告每个已发布构建的确切大小。
安装 GitHub 版本
从 v0.4.0 版本 (https://github.com/issacnitin/RealDiff/releases/tag/v0.4.0) 下载 linux-x64、linux-arm64、darwin-arm64、darwin-x64 或 win-x64 的归档文件。
对照 SHA256SUMS 验证,解压,并将解压后的目录放在 PATH 上。
可执行文件是自包含的;主机不需要 .NET 运行时。
sha256sum --check SHA256SUMS --ignore-missing
tar -xzf realdiff-v0.4.0-linux-x64.tar.gz -C "$HOME/.local/lib/realdiff"
ln -s "$HOME/.local/lib/realdiff/realdiff" "$HOME/.local/bin/realdiff"
realdiff --help
Get-FileHash .\realdiff-v0.4.0-win-x64.zip -Algorithm SHA256
Expand-Archive .\realdiff-v0.4.0-win-x64.zip "$env:LOCALAPPDATA\RealDiff"
& "$env:LOCALAPPDATA\RealDiff\realdiff.exe" --help
完整的解压目录必须保持在一起,因为 Rust realdiff 启动器会在旁边启动 realdiff-managed,且该目录还包含原生引擎、语言追踪器和单独启动的 .NET Weaver。
NuGet 工具包仍然作为框架依赖的托管兼容性分发可用:
dotnet tool install --global RealDiff.Tool --version 0.4.0 --add-source .
realdiff --help
从源码构建和安装
git clone https://github.com/issacnitin/RealDiff.git
cd RealDiff
pwsh -File tools/package-cli.ps1
dotnet tool install --global RealDiff.Tool `
--add-source ./artifacts/packages
realdiff --help
打包包装器在打包工具之前,构建并暂存带阴影的 Java 代理、带生产依赖的 Node 追踪器、当前 RID 的稳定 Rust 追踪器以及当前主机的 Rust 差异引擎。普通的 dotnet build 仍然独立于 Maven、npm 和 Cargo。
要更新现有的源码安装:
dotnet tool update --global RealDiff.Tool `
--add-source ./artifacts/packages
你也可以直接运行构建的 DLL:
dotnet build src/RealDiff.Cli/RealDiff.Cli.csproj -c Release
dotnet src/RealDiff.Cli/bin/Release/net8.0/realdiff.dll --help
运行分析
RealDiff 需要仓库路径和两个 Git 引用。目标仓库必须在当前环境中可以构建。
realdiff C:\src\my-service `
--base origin/main `
--pr HEAD `
--findings C:\temp\realdiff\findings.json
有用的选项:
--work 覆盖临时工作目录
--findings 写入规范的机器可读发现结果
--cache-dir 覆盖本地基线追踪缓存目录
--cache-retention 在指定时间窗口后过期缓存的追踪,例如 12h 或 7d
--keep-traces 选择保留工作追踪一段指定时间
--keep 保留临时的 Git 工作树;追踪仍默认删除
--ci=github 从 GitHub pull_request 事件解析引用
--ci=azuredevops 从 Azure Pipelines 变量解析引用
流式 Rust 引擎实现追踪加载、匹配、噪声过滤、差异构建、边界检测、归属、基线抑制和发现生成。REALDIFF_RUST_ENGINE 可以覆盖打包的原生可执行文件用于开发诊断。
PR 评论默认使用高置信度策略。一个发现只有在以下情况下才是高置信度:其边界已验证、每个比较的摘要都是精确的、一个祖先或后代差异通过调用树将其连接到变更、并且相同的成员没有表现出基线运行或清单的不确定性。一个贡献零个被追踪成员的编辑文件本身并不会取消该发现的资格。
每个发现都保留在 findings.json 中,包含 confidence、confidenceFactors、nondeterminism 和 commentSuppressionReasons;评论显示有多少较低置信度的发现在产物中被保留。传递 --strict 以将所有未抑制的发现包含在评论中。GitHub Action 通过 strict: 'true' 暴露相同的行为,Azure Pipelines 通过 behaviorDiffStrict: 'true'。
退出代码:
| 代码 | 含义 |
|---|---|
| 0 | 分析完成;没有意外的行为变化 |
| 1 | 分析完成;检测到意外的行为变化 |
| 2 | 分析因错误或无效输入而失败 |
相似文章
Show HN: Codiff,本地差异审查工具
Codiff 是一款轻量级本地 diff 查看器,用于审查 Git 暂存和未暂存的更改,支持基于 LLM 的逐步讲解和内联审查评论。
DiffsHub
DiffsHub 是一款工具,通过在 URL 中将 github.com 替换为 diffshub.com,即可提供快速、虚拟化的界面来查看任何公共 GitHub 差异(PR、提交等)。
差异只是摘要,会话跟踪才是实际发生的过程。
一篇文章强调,AI智能体会话跟踪揭示了最终PR差异中缺失的隐藏操作(例如未声明的密钥、范围蔓延),并警告大多数开发者只审查差异,而不审查完整跟踪,导致未审查的风险。
Linear Diffs
Linear Diffs 是一项新功能,可让您直接在 Linear 中审查拉取请求。
@sashimikun_void: https://github.com/nkzw-tech/codiff… 酷炫的代码差异查看器,顺便一提……
Codiff 是一个美观的本地差异查看器,适用于 Git 仓库,具有基于大语言模型的代码讲解和行内审查评论功能,旨在简化提交前的代码审查流程。