Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/blastradius.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: realdiff-findings
path: ${{ env.REALDIFF_FINDINGS }}
path: |
${{ env.REALDIFF_FINDINGS }}
${{ env.REALDIFF_WORK }}/readiness.json
if-no-files-found: error

- name: Delete traces and worktrees
Expand Down
26 changes: 23 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ env:
DOTNET_CLI_TELEMETRY_OPTOUT: '1'

jobs:
readiness:
name: Readiness contracts (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [windows-2022, ubuntu-24.04]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.x
- name: Verify readiness policy and safe diagnostics
shell: pwsh
run: pwsh -NoProfile -File tools/verify-readiness.ps1

rust-engine:
name: Rust engine
runs-on: windows-2022
Expand Down Expand Up @@ -249,8 +265,12 @@ jobs:
run: pwsh -NoProfile -File tools/verify-demo-fixtures.ps1

package-proof:
name: Installed binary package proof
runs-on: windows-2022
name: Installed binary package proof (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [windows-2022, ubuntu-24.04]
runs-on: ${{ matrix.os }}
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -306,5 +326,5 @@ jobs:
- name: Upload package
uses: actions/upload-artifact@v4
with:
name: realdiff-tool
name: realdiff-tool-${{ matrix.os }}
path: artifacts/packages/*.nupkg
89 changes: 86 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,95 @@ RealDiff finds **runtime behavior changes that ordinary source review misses** b
## How it works

1. Check out both branches of the pull request.
2. Build each branch with runtime instrumentation woven in.
2. Check prerequisites for each revision, then build with runtime instrumentation.
3. Run the same test suite on both, recording each observed method call's arguments and return value.
4. Diff the two execution traces instead of inferring behavior from the source diff.

This is dynamic behavior comparison, not mutation testing, static analysis, or coverage. RealDiff mutates nothing, modifies no test, and does not analyze source to predict behavior. It observes only code that the existing tests execute; source information is used to build, instrument, and map those observations back to the pull request.

## Readiness before analysis

Use `doctor` to inspect the current checkout before attempting a full analysis:

```powershell
realdiff doctor C:\src\my-service
realdiff doctor C:\src\my-service --json --out C:\temp\readiness.json
realdiff doctor C:\src\my-service --probe
```

```text
Repository -> selected tools + tracer assets + compatibility evidence
|
+-----------------+----------------+
| |
ready failed / unknown
| |
continue analysis stop with remediation
```

The same readiness service covers .NET, Java, Node/TypeScript, Go, Rust, and
Python. It checks only the selected language, not every installed compiler.
Normal analysis checks base and PR separately before build commands and baseline
trace-cache reuse; `warm` checks its target. The combined report is retained as
`<work>/readiness.json`, even when analysis is blocked.

**Required unknown compatibility blocks analysis.** Missing tools, incomplete
tracer packages, incompatible versions, and unresolved toolchain selection must
be addressed first. Fast inspection does not run project builds, tests, setup
scripts or repository wrappers, and does not install toolchains. Opaque custom
build/test shell commands are not a verifiable toolchain selection and are
reported as unknown; this is an intentional change from attempting them blindly.

`--probe` explicitly authorizes execution of a bundled instrumentation fixture
with installed tools in a temporary directory. Compiled languages compile and
run a small fixture; Node/Python exercise runtime attachment. A successful probe
requires actual trace and manifest evidence, not just exit code zero. It does
not build the user's project, provision tools, or fetch missing dependencies.
Prepare the required local package caches separately if an offline probe reports
missing dependencies. Temporary directories are **not a security sandbox**.

The initial fixture routes are xUnit, installed Maven/Gradle with JUnit, Node's
`node --test` or direct Node scripts (CommonJS/ESM, with a locally installed
TypeScript compiler when selected), `go test`, `cargo test`, and pytest/unittest. Repository Java wrappers,
opaque custom commands, and unsupported Node runners remain blocked rather than
being qualified by an unrelated fixture. Dynamic project metadata can also leave
a required check unresolved.

The direct-Node fixture exercises the production test-root hook, not arbitrary
framework integration. A successful fixture never substitutes for valid
correlated traces from the project's actual test run.

On Unix, NuGet extraction can discard native payload executable permissions.
If doctor reports a non-executable engine or rewriter, restore its executable
permission as an installation step; fast inspection intentionally does not
change file permissions.

To authorize those fixture probes for the actual analysis revisions:

```powershell
realdiff C:\src\my-service --base origin/main --pr HEAD --readiness-probe
```

A previous `doctor` result is not reused as permission for a different checkout.
Toolchain identity and execution context are part of baseline cache keys, so
compiler/runtime changes invalidate old baseline recordings. Docker compiler
versions are distribution defaults, not proof that every other version is
unsupported. The Go and Rust rewriters still have parser and injected-runtime
compatibility boundaries; doctor does not make them universally compiler-independent.

Reports use `realdiff.readiness/1` for one checkout and
`realdiff.readiness-set/1` for an analysis run. Check states are `passed`,
`failed`, `unknown`, and `skipped`; overall status is `ready`, `blocked`, or
`error`. JSON output is written alone to stdout; diagnostics go to stderr.
Standalone doctor exits 0 when prerequisites pass, 3 when blocked, and 4 on an
internal/reporting failure. **Exit 0 is not a clean PR verdict.** A fixture proves
only its exercised capability; normal instrumented builds and trace validation
remain necessary for real project code.

The GitHub action exposes `readiness-probe` (default `false`) and a `readiness`
artifact-path output. MCP and the supplied CI workflows retain readiness
diagnostics when no behavioral findings can be produced.

## Worked example

Suppose a pull request tries to remove the allocation made by `OrderBy`. In this diff, lines beginning with `-` are the stable base implementation, lines beginning with `+` are the proposed in-place sort, and the highlighted behavioral change is the new `ordered.Sort(...)` call:
Expand Down Expand Up @@ -355,13 +438,13 @@ baseline:

Configuration overrides inference field by field; detection fills fields left unset. `workdir` must remain inside the repository. `test_projects` selects .NET test projects by repository-relative glob, while `source_roots` supplies repository-relative Java source directories. Include/exclude values augment tracing scope, redaction values augment the corresponding environment rules, and the nested baseline uses the same schema as `.realdiff/baseline.yml`.

The effective build and test commands run unchanged for both base and PR revisions. Custom tests do not replace instrumentation: .NET receives the woven/injected environment, Java receives the javaagent through `JAVA_TOOL_OPTIONS`, Node receives the loader/hooks through `NODE_OPTIONS`, and Go/Rust tests execute in their rewritten caches. Go `exclude_namespaces` entries may name exact repository-relative `.go` files; excluded functions remain executable through passthrough companions but are recorded as `ExcludedByScope` and emit no events. A command that exits successfully but produces zero trace events is refused with exit `3` and reports the command and trace/manifest counts.
Readiness currently blocks opaque custom build/test commands because their toolchain selection cannot be verified. For supported inferred commands, .NET receives the woven/injected environment, Java receives the javaagent, Node receives the loader/hooks through `NODE_OPTIONS`, and Go/Rust tests execute in their rewritten caches. Go `exclude_namespaces` entries may name exact repository-relative `.go` files; excluded functions remain executable through passthrough companions but are recorded as `ExcludedByScope` and emit no events. A command that exits successfully but produces zero trace events is refused with exit `3` and reports the command and trace/manifest counts.

Automatic detection recognizes conventional root or unambiguous nested `.sln`/`.csproj`, Java `pom.xml`/`build.gradle`/`build.gradle.kts`, Node `package.json`, Go `go.mod`, Cargo `Cargo.toml`, and Python `pyproject.toml`/`setup.py`/`requirements.txt` entry points. Java execution prefers `mvnw`/`gradlew` and falls back to Maven/Gradle on `PATH`; Node execution selects npm, pnpm, Yarn, or Bun from its single lockfile and refuses missing or ambiguous lockfiles. Mixed-language repositories, monorepos, and multiple entry points are refused rather than guessed; set `language` and `workdir` (plus both commands when the language normally has a build step and no conventional entry point exists) to resolve them.

### Base trace cache

RealDiff caches the three validated noise-baseline traces when `--cache-dir` is supplied. Persistence is opt-in. The key contains the target SHA, language, a content fingerprint of the installed tracer, and the effective scope/redaction configuration. A tracer, scope, or redaction change therefore cannot reuse stale evidence. The storage boundary is pluggable; this release includes the local-directory backend, which can be placed on a CI-native or S3-compatible mounted cache. Entries expire after one day by default; use `--cache-retention` to state a different window.
RealDiff caches the three validated noise-baseline traces when `--cache-dir` is supplied. Persistence is opt-in. The key contains the target SHA, language, a content fingerprint of the installed tracer, effective scope/redaction configuration, and verified execution context (compiler/runtime/runner identities, platform, commands and selected settings). Changes to these inputs force a miss; legacy entries are not reused. The storage boundary is pluggable; this release includes the local-directory backend, which can be placed on a CI-native or S3-compatible mounted cache. Entries expire after one day by default; use `--cache-retention` to state a different window.

On a hit, PR analysis restores the three baseline samples and performs only the PR instrumented run. A missing, malformed, or unavailable cache entry is reported as a miss and falls back to the existing four-run path. The console and `findings.json.baseTraceCache` report `hit`, `miss`, or `disabled`, the cache key/backend, and measured baseline wall-clock time saved.

Expand Down
7 changes: 7 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ inputs:
description: Include lower-confidence and nondeterministic findings in comments.
required: false
default: 'false'
readiness-probe:
description: Explicitly run bundled offline instrumentation probes before analysis.
required: false
default: 'false'
outputs:
analysis-exit:
description: RealDiff analysis exit code before posting policy.
Expand All @@ -46,6 +50,8 @@ outputs:
description: Findings artifact status.
verdict:
description: Findings artifact verdict.
readiness:
description: Path to the readiness report, including when analysis is blocked.
runs:
using: docker
image: docker://ghcr.io/issacnitin/realdiff:main
Expand All @@ -59,3 +65,4 @@ runs:
- ${{ inputs.gate }}
- ${{ inputs.post }}
- ${{ inputs.strict }}
- ${{ inputs.readiness-probe }}
7 changes: 6 additions & 1 deletion azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,20 @@ jobs:
realdiff "${args[@]}"
analysis_exit=$?
set -e
if [[ -f '$(behaviorDiffWork)/readiness.json' ]]; then
cp '$(behaviorDiffWork)/readiness.json' '$(Build.ArtifactStagingDirectory)/realdiff/readiness.json'
fi

echo "##vso[task.setvariable variable=behaviorDiffAnalysisExit]$analysis_exit"
if [[ ! -f '$(behaviorDiffFindings)' ]]; then
echo "##vso[task.logissue type=error]RealDiff exited $analysis_exit without findings.json."
if (( analysis_exit == 0 )); then exit 4; fi
exit "$analysis_exit"
fi

if (( analysis_exit != 0 && analysis_exit != 1 )); then
echo "##vso[task.logissue type=warning]RealDiff produced a non-verdict (exit $analysis_exit)."
echo "##vso[task.logissue type=error]RealDiff could not analyze (exit $analysis_exit); inspect readiness.json."
exit "$analysis_exit"
fi
displayName: Analyze runtime behavior

Expand Down
12 changes: 12 additions & 0 deletions docker/action-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ cache_retention="${5:-1d}"
gate="${6:-warn-only}"
post="${7:-true}"
strict="${8:-false}"
readiness_probe="${9:-false}"

mkdir -p "$(dirname "$findings")" "$cache_dir"

Expand All @@ -17,6 +18,9 @@ analysis_args=("$repo" --ci=github --work "$work" --findings "$findings"
if [[ "${strict,,}" == "true" ]]; then
analysis_args+=(--strict)
fi
if [[ "${readiness_probe,,}" == "true" ]]; then
analysis_args+=(--readiness-probe)
fi

set +e
realdiff "${analysis_args[@]}"
Expand All @@ -36,11 +40,19 @@ if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
echo "findings=$findings"
echo "status=$status"
echo "verdict=$verdict"
echo "readiness=$work/readiness.json"
} >> "$GITHUB_OUTPUT"
fi

if [[ ! -f "$findings" ]]; then
echo "RealDiff exited $analysis_exit without writing $findings" >&2
echo "Readiness diagnostics: $work/readiness.json" >&2
if (( analysis_exit == 0 )); then exit 4; fi
exit "$analysis_exit"
fi

if (( analysis_exit != 0 && analysis_exit != 1 )); then
echo "Analysis did not complete; readiness diagnostics: $work/readiness.json" >&2
exit "$analysis_exit"
fi

Expand Down
1 change: 1 addition & 0 deletions src/RealDiff.Cli/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
[assembly: InternalsVisibleTo("RealDiff.AnthropicLive")]
[assembly: InternalsVisibleTo("RealDiff.CommentPreview")]
[assembly: InternalsVisibleTo("RealDiff.CrossLanguageConsumerProof")]
[assembly: InternalsVisibleTo("RealDiff.ReadinessProof")]
Loading