Regression harness compares results across different package environments (Manifest drift), producing spurious regressions
Summary
The regression harness caches numerical baselines keyed only on (commit_hash, case_name), with no fingerprint of the package environment that produced them. Because Manifest.toml is untracked and each worktree run calls Pkg.instantiate() against a bare checkout, a baseline cached weeks ago is silently resolved against older package versions than a fresh local run. The resulting machine-epsilon (~1e-16) differences in library math (BLAS/SciML/ArrayInterface) are then amplified by the adaptive ODE step controller and by ill-conditioned near-resonant diagnostics into large apparent regressions — up to 200% — that are pure environment artifacts, not code changes.
This matters because the harness is mandated on every PR (per CLAUDE.md). A reviewer can waste significant time chasing a phantom, or — worse — "fix" correct code to mask an artifact.
Root cause (confirmed in code)
Three independent factors combine:
-
Manifest.toml is untracked. A worktree created for a cached commit has no pinned Manifest.
regression-harness/src/utils.jl:74-83 (create_worktree) does a bare git worktree add --detach.
-
Each run resolves the environment fresh. With no Manifest present, Pkg.instantiate() picks the newest versions compatible with Project.toml's [compat] at run time.
regression-harness/src/runner.jl:43-54 (RUNNER_SCRIPT_TEMPLATE) → %INSTANTIATE% expands to Pkg.instantiate() (default; only skipped with --no-instantiate).
- Invoked at
regression-harness/src/runner.jl:409-423 against --project=$worktree_path.
-
The cache has no environment key. Results are stored/looked up by (commit_hash, case_name) only.
- Schema:
regression-harness/src/database.jl:5-18 (runs table, UNIQUE(commit_hash, case_name)) — no Julia version, no Manifest hash, no package-set hash.
is_cached (regression-harness/src/database.jl:63-67) ignores the environment entirely.
Net effect: regress --refs develop,local compares a develop baseline produced under whatever package set was newest at cache-population time against a fresh local run under today's package set.
Evidence
Investigating a branch that only refactors FourierTransforms/Vacuum (no changes to EulerLagrange.jl/Riccati.jl/Fourfit.jl or the equilibrium metric), the harness reported for develop (cached) vs local:
| Quantity |
Reported "regression" |
| diiid ODE steps (saved) |
1353 → 1325 (2.07%) |
| diiid ODE steps (total) |
1990 → 1951 (1.96%) |
| diiid Chirikov parameter |
11.13% |
| diiid resonant field |
10.80% |
| diiid PE toroidal torque |
200.47% |
| solovev ODE steps (total) |
605 → 614 (1.49%) |
Controlled re-test — same source diff, but the develop worktree pinned to the identical Manifest.toml as the working tree (source code the only variable), 4 threads throughout:
| Quantity |
develop |
local |
Result |
| solovev nstep / total |
385 / 614 |
385 / 614 |
identical |
| diiid nstep / total |
1325 / 1951 |
1325 / 1951 |
identical |
| diiid Chirikov |
— |
— |
rel 2.8e-14 |
| diiid resonant field |
— |
— |
rel 6.3e-14 |
| all other datasets |
— |
— |
rel ≤ 1e-13 |
The develop worktree instantiated without pinning had resolved newer versions (e.g. 4.6.1→4.7.0, 7.25.0→7.28.1 in the SciML/ArrayInterface stack). Pinning the Manifest collapsed every reported regression to machine epsilon. Two back-to-back runs of the same environment are bit-identical, so this is not thread-schedule noise.
Mechanism: a 1e-16 change in equilibrium/EL-matrix math (from a different library version) crosses an adaptive-step acceptance boundary (384↔385 steps), which reshuffles the integration ψ-grid, and the near-resonant singular-coupling quantities (Chirikov, Δ′, torque) are ill-conditioned enough to turn that seed into double-digit-percent swings.
Proposed fixes (increasing effort)
-
Fingerprint the environment and warn on mismatch (minimum). Store a hash of the resolved Manifest.toml (or Julia version + package-set) alongside each cached run, and print a loud warning (or auto-invalidate) when a cached ref's fingerprint differs from the current run's. Cheapest change; converts a silent confound into a visible one. Requires a runs schema column + a check in is_cached/reporter.
-
Run both refs in one shared, pinned environment at compare time. Resolve a single Manifest once and reuse it for every worktree (--project pointed at a shared depot, or copy the working tree's Manifest into each worktree before instantiate). Makes source code the only variable by construction.
-
Track Manifest.toml (or a harness-specific pinned Manifest) so worktree checkouts are reproducible, and instantiate against it.
Workaround (today)
Regenerate the baseline in the current environment before trusting a report:
regress --cases diiid_n1,solovev_n1 --refs develop,local --force
--force re-runs the develop ref under the same packages as local, eliminating the cross-environment artifact.
Regression harness compares results across different package environments (Manifest drift), producing spurious regressions
Summary
The regression harness caches numerical baselines keyed only on
(commit_hash, case_name), with no fingerprint of the package environment that produced them. BecauseManifest.tomlis untracked and each worktree run callsPkg.instantiate()against a bare checkout, a baseline cached weeks ago is silently resolved against older package versions than a freshlocalrun. The resulting machine-epsilon (~1e-16) differences in library math (BLAS/SciML/ArrayInterface) are then amplified by the adaptive ODE step controller and by ill-conditioned near-resonant diagnostics into large apparent regressions — up to 200% — that are pure environment artifacts, not code changes.This matters because the harness is mandated on every PR (per
CLAUDE.md). A reviewer can waste significant time chasing a phantom, or — worse — "fix" correct code to mask an artifact.Root cause (confirmed in code)
Three independent factors combine:
Manifest.tomlis untracked. A worktree created for a cached commit has no pinned Manifest.regression-harness/src/utils.jl:74-83(create_worktree) does a baregit worktree add --detach.Each run resolves the environment fresh. With no Manifest present,
Pkg.instantiate()picks the newest versions compatible withProject.toml's[compat]at run time.regression-harness/src/runner.jl:43-54(RUNNER_SCRIPT_TEMPLATE) →%INSTANTIATE%expands toPkg.instantiate()(default; only skipped with--no-instantiate).regression-harness/src/runner.jl:409-423against--project=$worktree_path.The cache has no environment key. Results are stored/looked up by
(commit_hash, case_name)only.regression-harness/src/database.jl:5-18(runstable,UNIQUE(commit_hash, case_name)) — no Julia version, no Manifest hash, no package-set hash.is_cached(regression-harness/src/database.jl:63-67) ignores the environment entirely.Net effect:
regress --refs develop,localcompares a develop baseline produced under whatever package set was newest at cache-population time against a fresh local run under today's package set.Evidence
Investigating a branch that only refactors
FourierTransforms/Vacuum (no changes toEulerLagrange.jl/Riccati.jl/Fourfit.jlor the equilibrium metric), the harness reported fordevelop(cached) vslocal:Controlled re-test — same source diff, but the develop worktree pinned to the identical
Manifest.tomlas the working tree (source code the only variable), 4 threads throughout:The develop worktree instantiated without pinning had resolved newer versions (e.g.
4.6.1→4.7.0,7.25.0→7.28.1in the SciML/ArrayInterface stack). Pinning the Manifest collapsed every reported regression to machine epsilon. Two back-to-back runs of the same environment are bit-identical, so this is not thread-schedule noise.Mechanism: a 1e-16 change in equilibrium/EL-matrix math (from a different library version) crosses an adaptive-step acceptance boundary (384↔385 steps), which reshuffles the integration ψ-grid, and the near-resonant singular-coupling quantities (Chirikov, Δ′, torque) are ill-conditioned enough to turn that seed into double-digit-percent swings.
Proposed fixes (increasing effort)
Fingerprint the environment and warn on mismatch (minimum). Store a hash of the resolved
Manifest.toml(or Julia version + package-set) alongside each cached run, and print a loud warning (or auto-invalidate) when a cached ref's fingerprint differs from the current run's. Cheapest change; converts a silent confound into a visible one. Requires arunsschema column + a check inis_cached/reporter.Run both refs in one shared, pinned environment at compare time. Resolve a single Manifest once and reuse it for every worktree (
--projectpointed at a shared depot, or copy the working tree's Manifest into each worktree beforeinstantiate). Makes source code the only variable by construction.Track
Manifest.toml(or a harness-specific pinned Manifest) so worktree checkouts are reproducible, and instantiate against it.Workaround (today)
Regenerate the baseline in the current environment before trusting a report:
--forcere-runs the develop ref under the same packages aslocal, eliminating the cross-environment artifact.