Skip to content

Roadmap

The plan to 1.0.0 and the work done towards it, from the package's ROADMAP.md.

This document tracks where the package is going. It is updated alongside meaningful code changes. Milestones are ordered, not dated. The history of what changed and why lives in NEWS.md; the statistical evidence lives in inst/validation/.

Where things stand (0.15.0)

  • On CRAN. 0.11.2 was accepted in September 2026, after one review round on 0.11.1. 0.12.0, 0.13.0, 0.14.0 and 0.15.0 are GitHub releases: the next submission is 1.0.0 (decided 2026-10-11).
  • The validation matrix is complete for every exported perform*Test(). The sweep found six broken procedures, all corrected or withdrawn, and twenty-five of the twenty-six heteroscedasticity tests hold their nominal level (inst/validation/README.md). The tests added since have studies of their own: inst/validation/harrison-mccabe-size-power.R and inst/validation/glejser-skewed-errors.R.
  • Weighted fits were outside that matrix, and wrong. Every design in it fitted an unweighted model. On a weighted fit the tests read the raw residuals, whose variance differs across observations by assumption, and rejected a correctly weighted model every time. 0.12.0 corrects that and adds weighted designs to the matrix (inst/validation/weighted-fits-size.R). In 0.13.0 the bootstrap tests accept a weighted fit too, with designs of their own in the same study, and since 0.15.0 they estimate the weights of a fitWLS() fit again in each replicate rather than take them as known.
  • Reference equivalence is asserted to 1e-8 against lmtest, car and plm wherever a reference implementation exists. lmtest::bptest() is not a reference for weighted fits: it tests the raw residuals.
  • The public surface has been reviewed. 0.14.0 gave each of the 129 exports of 0.13.0 a verdict (the export inventory under 0.14.0): 104 are core API, 22 are deprecated and warn, and 3 move to a companion package that has no name yet. With bootstrapTestStatistic(), simulate_size() and simulate_power(), and runAdvancedDiagnostics() in 0.15.0, the package exports 133 objects, and test-public-api.R asserts the 107 planned for 1.0.0.
  • The interface matches its description. In 0.15.0 every test takes (model, data, ...), performBoxMTest(data, group) being the documented exception, studentize chooses the studentized statistic throughout, ?test_results documents what every test returns, and every test refuses the same bad inputs with one message. The old call forms warn in 0.15.x and stop working in 1.0.0. The defaults changed with it: 999 replicates for every resampling function, White and Koenker as the default battery, and the corrected Glejser statistic. What stands between 0.15.0 and 1.0.0 is the evidence and internals of 0.16.0, and the freeze.

What 1.0.0 means

1.0.0 is a promise rather than a feature count. From 1.0.0:

  • Exported names, argument names and order, and defaults change only in a major release, and only after a minor release in which the old form still works and warns.
  • Every exported test returns an htest object with a documented, uniform set of fields.
  • A correction to a statistic, its degrees of freedom or its p-value is a bug fix. It is never deferred for the sake of compatibility. It bumps the minor version and gets its own NEWS.md section, as 0.11.0 did.

Version numbers follow from that: a patch release changes no reported value; a minor release adds functionality or corrects a statistic; a major release breaks the API.

Everything below exists to make that promise safe to give. The sequencing rule this file has always followed still holds: statistical correctness first, then the API, and never both in one release. 0.12.0 reopened correctness for weighted fits and closed it again, and 0.13.0 corrected what was found after it, so the API is next.

Guiding principles

  • Statistical correctness first. Every test must reproduce an established reference (or a defensible derivation) and be covered by a test that checks behaviour (size and power), not just object structure.
  • A reference comparison is not a substitute for a size check. Four of the six faults the sweep found were in procedures with no reference to compare against, and the two that had one agreed with it.
  • Guards are simulated, not stored. A stored-value regression test would have frozen each of those faults rather than caught it.
  • One consistent interface. All diagnostics return base-R htest objects and follow the perform*Test(model, data, ...) convention. (Since 0.15.0 every exported test does, except performBoxMTest(data, group), which tests the data rather than residuals; see 0.15.0.)
  • Scale and robustness as first-class concerns: streaming implementations for large data, resampling and robust variants for small or non-normal samples.

Milestones

0.11.x: CRAN maintenance (now)

  • Watch the CRAN check results across all flavours and fix anything flagged within the deadline CRAN sets.
  • README: install from CRAN first (install.packages("heteroTests")), with GitHub as the development route; add a CRAN badge and use the canonical URL https://CRAN.R-project.org/package=heteroTests.
  • The README feature list is out of date. It still advertises an HC0–HC4 covariance test and Cameron–Trivedi, both removed in 0.8.0. Bring it in line with the exports, and extend test-documentation-consistency.R so a README or vignette that names a removed function fails. The guard went into test-public-api.R, beside the list of removed exports, and also matches the names prose gives the removed tests.
  • Documentation drift: the performStudentizedBPTest() help page describes performKoenkerTest() as "the absolute-residual variant", which predates the Breusch–Pagan/Koenker correction. Koenker is the studentized n R^2 form.
  • Check that the Zenodo record picked up 0.11.2, and cite the CRAN release in inst/CITATION and CITATION.cff. Zenodo archives 0.11.2 and 0.12.0; only the 0.12.0 record links to CRAN.
  • Close tracking issue #38 as On CRAN. It was closed on 2026-09-27.
  • Reinstate a release gate. R CMD check --as-cran can stay weekly rather than per merge, but it must pass in full on the tagged commit (vignettes and --run-donttest included) before any submission, alongside win-builder (R-devel). release.yml now runs --as-cran --run-donttest on the tarball it is about to tag and creates no tag if the check warns or errors; win-builder stays a manual step (cran-comments.md).
  • The gate failed. The weekly --as-cran job stopped with an error on 2026-09-28 and on 2026-10-05: the \donttest{} example of performWhiteTestBootstrap(parallel = TRUE) starts detectCores() - 1 processes, three on the runner, and --as-cran allows two. The example is skipped on Windows, so a local check there does not see it. Fixed in 0.13.0 by changing the example, which now runs the call only where it starts at most two processes; the "CRAN submission checks" job passed on the branch on 2026-10-11 and on main on 2026-10-12. The function still uses detectCores() - 1 workers by default; since 0.15.0 n_cores sets them, and the result no longer depends on how the draws are split.

0.12.0: weighted fits and the shape of the variance (done)

This release was not on the roadmap. It took the 0.12.0 number because a corrected statistic bumps the minor version, and the milestones below moved up by one.

  • Weighted fits. On an lm fitted with weights, every residual-based test is computed from the Pearson residuals sqrt(w) * e. Goldfeld–Quandt and RESET refit with the weights. Unweighted fits return bit-identical values.
  • performVarianceFormTest(), a test whose null hypothesis is a variance function (exponential or power) rather than constant variance.
  • fitWLS() takes var_formula and form, and records the variance function it fitted.
  • Evidence. inst/validation/weighted-fits-size.R and inst/validation/variance-form-size-power.R, with simulated guards in test-weighted-fits.R and test-variance-form.R.

Found on the way and left for their own changes:

  • Resampling on weighted fits. performWildBootstrapTest(), performWhiteTestBootstrap(), rbootstrap_test_statistic() and performQuantileRegressionTest() refused a weighted fit, because they refitted without the weights. Resolved in 0.13.0 for the first three: they resample the Pearson residuals, return each draw to the scale of its observation and refit with the weights. That change leaves an unweighted fit as it was; the corrections 0.13.0 also makes to unweighted refits are listed under 0.13.0 below. In inst/validation/weighted-fits-size.R, at 2000 replications with B = 499, they reject a correctly weighted fit 0.045 to 0.054 of the time in two designs. performQuantileRegressionTest() still refuses one: on the weighted scale its slopes differ across quantiles under the null hypothesis, and a version for weighted fits needs its own construction. A lead: the quantile regressions of sqrt(w) y on sqrt(w) X with a column of ones added have equal slopes under the null hypothesis, which is the location-shift form the test assumes. It needs a derivation of the reference distribution and a size study before it can ship.
  • runSurveyHeteroTests() did not use the survey weights. It called stats::lm(formula, data = data, weights = weights), where weights was a local variable. lm() looks the name up in data and then in the environment of the formula, found stats::weights, failed, and the tryCatch() refitted without weights. Resolved in 0.13.0 by making the function say what it does, because using the weights would have been worse: sampling weights are not inverse error variances, and a test on a fit that uses them rejected a homoscedastic model 97% to 100% of the time when the weights varied with the regressor. runHeteroTests(formula, design) did exactly that through a svyglm() fit, and now takes the unweighted route too. Evidence: inst/validation/survey-designs-size.R. A design-based test is under Candidate tests.
  • .ht_fit_from_formula() had the same scoping fault and always failed with invalid type (closure) for variable '(weights)', so the per-group refits built from an lm, glm or parsnip fit could not run. Fixed in 0.13.0: each group is refitted by evaluating the model's own call on it, which gives it its own rows of the weights and keeps the family of a glm.
  • tidy() returned one row per degree of freedom. A result with two parameters, such as the F test of performVarianceFormTest() or the wild bootstrap with its df and B, was tidied into two rows, and the grouped and suite methods stacked them. Fixed in 0.13.0: one row per test, with the second parameter in a column of its own, parameter2.
  • autoplot() of a test suite did not show significant results. The bars were p-values on a linear axis from 0 to 1, so a test with p = 0.002 had no visible bar, and the chart was empty exactly when the tests rejected. The highlighting of p < 0.05 never applied either: the condition was written isTRUE(df$p.value < 0.05), which is FALSE for a suite of more than one test. The grouped method had the same condition and added a facet for the suggestions column. Fixed in 0.13.0: the bars are -log10(p) on an axis labelled with p-values, with a line at alpha and the p-value printed beside each bar.
  • An additive variance function, sigma^2 = a + z'b, was implemented for fitWLS() and the form test and withdrawn before release. Under its own null, at 5000 replications, the test rejected 6.1% of the time at the 5% level with 150 and with 400 observations, outside the release gate, and the fitted variances were not all positive in 17% of samples at 50 observations and 6% at 150. It needs an estimator that keeps the fitted variances positive before it can come back.
  • Tests of constant variance on a fitWLS() fit take the estimated weights as known. With the variance function right and Gaussian errors, Koenker rejects 8% of the time at the 5% level with 150 observations and 11% with 600, White 6% and 8%, and Harvey never. That is documented, and performVarianceFormTest() is the calibrated alternative, but a correction for the estimation step would let compareModelDiagnostics() report calibrated p-values for a weighted remedy.

    The bootstrap tests hold the weights fixed and share the error: the wild bootstrap rejects 0.086 with 150 observations and 0.105 with 600, where it rejects 0.054 and 0.048 with the true weights in the same design. A bootstrap that runs fitWLS() again on every replicate is such a correction. In the same designs it rejects 0.042 to 0.060 with 150 observations for the wild bootstrap, the White bootstrap and rbootstrap_test_statistic() with Koenker's statistic, and 0.051 with 600 for the wild bootstrap (inst/validation/weighted-fits-size.R). Resolved in 0.15.0 for the bootstrap tests, as proposed: the argument reestimate_weights of the three functions refits the variance function fitWLS() recorded on the model, with the same var_formula and form, in each replicate, by default on a fit that carries that record; weights the user supplied are known and stay fixed (0.15.0, below). The tests of constant variance themselves still take the weights as known.

0.13.0: what was found after 0.12.0 (done)

This release was not on the roadmap either. It took the 0.13.0 number because it corrects statistics and adds functionality, and the milestones below moved up by one again.

  • Values that changed. The bootstrap tests keep an offset given as an argument when they refit an unweighted model, rbootstrap_test_statistic() matches the rows of the data to the fit by name and leaves out rows the fit dropped, and performWhiteTestBootstrap() regenerates a transformed response, where its p-value was 1. fitWLS() keeps the offset of the model it refits, and performVarianceFormTest() changes with it on a model with an offset. The Szroeter test that runHeteroTests() runs by name orders the observations by the fitted values instead of by the second column of the data. runHeteroTests() and runDiagnostics() no longer test a survey design through a fit weighted by its sampling weights. tidy() returns one row per test.
  • What was found on the way to 0.12.0: the resampling tests accept a weighted fit, and the survey designs, the grouped refits and autoplot() are fixed (above).
  • Two tests from the candidates: performHarrisonMcCabeTest() and performGlejserTest(robust = TRUE). Two more were measured and not adopted, the modified Wald test and stage 1 of the Dette, Neumeyer and Van Keilegom test (Candidate tests).
  • listDiagnostics(), and the release gate in release.yml (0.11.x, above).

0.14.0: review the public surface, deprecate and remove nothing (done)

Goal: every one of the 129 exports gets a verdict, which is one of core API, renamed, internal or moved out. Nothing is removed in this release. Everything that will go warns.

The author accepted the four proposals for this release on 2026-10-14: the dashboard and the benchmark suite move to a companion package and the recommendation engine stays, with its return format frozen; performCookWeisbergTest() stays, with an optional var_formula, and performNCVTest() is deprecated; test() is folded into summary.HeteroDiagnostic(); and simulate_*() and sigma_*() stay as a documented snake_case family while the one-offs are renamed. The companion package has no name yet, so the dashboard and benchmark functions are not deprecated until it has one.

  • Duplicate exports. Each duplicate below is deprecated in favour of the name it duplicates. performStudentizedBPTest() agrees with performKoenkerTest() to 1e-8 except on a model with an offset, which it drops (listed under 0.16.0). It keeps its implementation, so no value changed in 0.14.0; on a fitWLS() fit it read stale weights until 0.15.0 fixed that (listed under 0.16.0, done in 0.15.0).
    • performBreuschPaganTest() is performBPTest(), assigned at R/performBPTest.R:232.
    • performStudentizedBPTest() is a separate implementation that matches performKoenkerTest() in every column of the full sweep. Confirm the statistics agree to 1e-8, then keep one.
    • performCookWeisbergTest() delegates to performNCVTest() with the fitted values as the variance regressor. performCookWeisbergTest() becomes the one entry point for the Cook–Weisberg score test, with var_formula as an optional argument, and performNCVTest() is deprecated (decided above).
    • *Enhanced pairs: checkModel()/checkModelEnhanced(), plotResidualsFitted()/plotResidualsFittedEnhanced() and plotDiagnosticSuite()/plotDiagnosticSuiteEnhanced(). Merge each improvement into the base name.
  • Internal machinery that is exported: std_error(), std_warning(), validateTestInputs(), checkData(), TestFactory/test_factory(), the cache functions (cachedTest(), clearTestCache(), clearAnalysisCache()) and the ht_* logging functions. Unexport them unless a user-facing reason exists; for logging, keep at most ht_set_log_level(). Deprecated in 0.14.0 and unexported in 1.0.0, except the cache functions, which stay for now (decided), and ht_set_log_level().
  • The five r-prefixed exports (rbootstrap_test_statistic(), rcalculateEffectSize(), restimate_test_power(), rrunAdvancedDiagnostics() and rvalidate_against_reference()) follow no naming scheme in the package. Rename each to the house convention or unexport it. Four are done in 0.14.0: rbootstrap_test_statistic() is renamed bootstrapTestStatistic() and the other three are deprecated as internal helpers. rrunAdvancedDiagnostics() stays core API (decided), and was renamed runAdvancedDiagnostics() in 0.15.0.
  • test() is a generic with a very common name. Attached after devtools, it masks devtools::test(). Fold it into summary.HeteroDiagnostic() (decided above). Deprecated in 0.14.0, naming runDiagnostics(x$model, x$data, tests); summary.HeteroDiagnostic() calls runDiagnostics() and returns what it returned.
  • Naming convention. camelCase dominates (performXTest(), fitWLS(), plotX()). The simulation family (simulate_hetero(), sigma_*()) is declared a deliberate snake_case family and documented as one (decided above). The one-offs (run_benchmark_suite(), generate_benchmark_report(), simulate_type_I_errors(), simulate_power_analysis()) are renamed. The last two are simulate_size() and simulate_power() in 0.14.0; the first two move out with the benchmark suite instead.
  • Scope. The dashboard (R/dashboard.R, 698 lines) and the benchmark suite (R/benchmarking_system.R, 798 lines) move to a companion package (decided above), once it has a name. Between them they account for most of the optional Suggests (shiny, DT, plotly, htmlwidgets, bench). Open: the package has no name, so they are not deprecated in 0.14.0 (see Decisions needed).
  • Related model checks (performRESETTest(), performVIFDiagnostic(), performInfluenceDiagnostics(), performScatterDiagnostic()) are not heteroscedasticity tests. Keep them, grouped in the reference index as model checks, so they are not mistaken for tests of the variance.
  • Mechanism. Use base .Deprecated(), so no new dependency is needed. Each call warns and names its replacement. NEWS.md carries an old → new mapping table.
  • Exit criterion. An export inventory with a verdict for every one of the 129 exports, and test-public-api.R extended to assert the planned 1.0 export list, so that an accidental new export fails CI.

Export inventory

Every export in NAMESPACE on 2026-10-14, with a verdict. Of the 129, 104 are core API, 10 are renamed, 12 become internal and 3 move out. The S3 methods registered in NAMESPACE are not exports and are not listed; S3method(test, HeteroDiagnostic) goes with test(). The verdicts apply the decisions of 2026-10-14 as written, and the author's decisions on this inventory, taken the same day, are marked "decided".

For each renamed or internal export, the last column lists every caller found by a search of R/ (code, not comments or the definition), vignettes/ and inst/validation/, adds README.md where it names the export, and counts the lines in tests/ that call it. The notebooks in inst/tutorials/ call none of these exports. The article was not searched: it moves to the 1.0 API after 1.0.0. An export that becomes internal keeps working for its callers in R/, and for the tests, which run inside the namespace; a renamed export keeps working under its old name and warns. What has to change in each pull request are the callers outside R/, and the package's own calls of a name that will warn.

Export Area Verdict Evidence that the change is safe
performWhiteTest() test core API
performWhiteTestStreaming() test core API
performWhiteTestRobust() test core API
performWhiteTestBootstrap() test core API
performBPTest() test core API
performBPTestStreaming() test core API
performBPTestRobust() test core API
performBreuschPaganTest() test renamed: performBPTest(), the same object R/: none (an alias, assigned in performBPTest.R). Vignettes: using_heteroTests.Rmd:128. inst/validation/: full-sweep-size-power.R:169; 2 lines of README.md. Tests: 3 lines.
performKoenkerTest() test core API
performKoenkerTestStreaming() test core API
performStudentizedBPTest() test renamed: performKoenkerTest(), once item 2 shows they agree to 1e-8 (provisional, decided) R/: registry name "student_bp" (diagnosticsRegistry.R:70, test_factory.R:105); performBPTestRobust(studentized = TRUE) (robust_implementations.R:249); rvalidate_against_reference() (:455). Vignettes: none. inst/validation/: full-sweep-size-power.R:171, weighted-fits-size.R:138; 4 lines of README.md. Tests: 8 lines.
performGQTest() test core API
performParkTest() test core API
performSzroeterTest() test core API
performHarrisonMcCabeTest() test core API
performWildBootstrapTest() test core API
performQuantileRegressionTest() test core API
performRankPermutationTest() test core API
performHighDimensionalTest() test core API
performSpatialHeteroTest() test core API
performSpearmanTest() test core API
performLeveneTest() test core API
performGlejserTest() test core API
performBrownForsytheTest() test core API
performFlignerKilleenTest() test core API
performBartlettTest() test core API
performHarveyTest() test core API
performArchLMTest() test core API
performMcLeodLiTest() test core API
performHartleyFmaxTest() test core API
performOBrienTest() test core API
performCookWeisbergTest() test core API
performNCVTest() test renamed: performCookWeisbergTest(), which gains an optional var_formula (decided) R/: registry name "ncv" (diagnosticsRegistry.R:67); performCookWeisbergTest() delegates to it (performCookWeisbergTest.R:69); the benchmark suite (benchmarking_system.R:368). Vignettes: none. inst/validation/: full-sweep-size-power.R:165, pass-a-size-power.R:87, weighted-fits-size.R:133,196; 6 lines of README.md. Also README.md:34. Tests: 20 lines.
performSpreadLevelTest() test core API
performPesaranTest() test core API
performBPRandomEffectsTest() test core API
performDavidianCarrollTest() test core API
performVarianceFormTest() test core API
performBoxMTest() test core API
rbootstrap_test_statistic() test renamed: bootstrapTestStatistic() (decided) R/: performWhiteTestRobust() and performBPTestRobust() (robust_implementations.R:135,270). Vignettes: none. inst/validation/: offset-refits-size.R, weighted-fits-size.R, labels in make-table.R; 22 lines of README.md. Also the Decisions needed row on fitWLS() fits. Tests: 60 lines.
performScatterDiagnostic() model check core API
performVIFDiagnostic() model check core API
performRESETTest() model check core API
performInfluenceDiagnostics() model check core API
checkModel() model check core API
checkModelEnhanced() model check renamed: merged into checkModel() R/: performWhiteTestStreaming() (performance.R:267). Vignettes: none. inst/validation/: none. Tests: 8 lines.
runHeteroTests() runner core API
runHeteroTestsParallel() runner core API (decided)
runDiagnostics() runner core API
runDiagnosticPlots() runner core API
registerDiagnostic() runner core API
listDiagnostics() runner core API
registerPlot() runner core API
runMultivariateTests() runner core API
runTimeSeriesTests() runner core API
runPanelTests() runner core API
runSurveyHeteroTests() runner core API
compareModelDiagnostics() runner core API
compareTestResults() runner core API (decided)
analyzeMLResiduals() runner core API
HeteroDiagnostic() runner core API
test() runner renamed: its warning names runDiagnostics(x$model, x$data, tests); summary() is unchanged (decided) R/: summary.HeteroDiagnostic() calls it (HeteroDiagnostic.R:59). Vignettes: real_world_case_studies.Rmd:110, tutorial.Rmd:32, using_heteroTests.Rmd:100,105. inst/validation/: none. Also README.md:143. Tests: 19 lines. summary.HeteroDiagnostic() will call runDiagnostics() directly.
fitWLS() remediation core API
fitRobust() remediation core API
autoTransform() remediation core API
suggestRemediation() remediation core API
autoCompareRemediations() remediation core API
analyseDatasetCharacteristics() recommendation core API
suggestDiagnosticsForProfile() recommendation core API
interpretHeteroTestResults() recommendation core API
recommendRemediationStrategies() recommendation core API
warnDiagnosticAssumptions() recommendation core API
buildDiagnosticDecisionTree() recommendation core API
composeDiagnosticNarrative() recommendation core API
generateHeteroRecommendations() recommendation core API
simulate_hetero() simulation core API
simulate_arch1() simulation core API
sigma_linear() simulation core API
sigma_exponential() simulation core API
sigma_group() simulation core API
sigma_piecewise() simulation core API
sigma_poly() simulation core API
sigma_sin() simulation core API
sigma_multiplicative() simulation core API
sigma_spatial() simulation core API
sigma_logistic() simulation core API
sigma_inverse() simulation core API
sigma_power() simulation core API
sigma_step() simulation core API
sigma_u_shape() simulation core API
sigma_exp_decay() simulation core API
sigma_gaussian_peak() simulation core API
sigma_piecewise_linear() simulation core API
simulate_type_I_errors() simulation renamed: simulate_size() (decided) R/: none. Vignettes: none. inst/validation/: none. Tests: 3 lines.
simulate_power_analysis() simulation renamed: simulate_power() (decided) R/: none. Vignettes: named in the prose of troubleshooting.Rmd:44. inst/validation/: none. Tests: 3 lines.
plotResidualsFitted() plotting core API
plotSpreadLevel() plotting core API
plotResidualDensity() plotting core API
plotResidualQQ() plotting core API
plotBubbleVariance() plotting core API
plotDiagnosticSuite() plotting core API
plotBeforeAfter() plotting core API
plotResidualsFittedEnhanced() plotting renamed: merged into plotResidualsFitted() R/: plotDiagnosticSuiteEnhanced() (plotDiagnostics.R:279). Vignettes: none. inst/validation/: none. Tests: 5 lines.
plotDiagnosticSuiteEnhanced() plotting renamed: merged into plotDiagnosticSuite() R/: generateDiagnosticReport() (reporting.R:53); the dashboard (dashboard.R:536). Vignettes: none. inst/validation/: none. Tests: 4 lines.
theme_hetero() plotting core API
scale_colour_hetero_diagnostic() plotting core API
scale_fill_hetero_diagnostic() plotting core API
generateDiagnosticReport() reporting core API
downloadTeachingData() reporting core API
ht_set_log_level() infrastructure core API
std_error() infrastructure internal R/: 92 lines in 24 files, all internal callers, which keep working. Vignettes: none. inst/validation/: none. Tests: 9 lines.
std_warning() infrastructure internal R/: performance.R:181, performWhiteTest.R:232, validation.R:675. Vignettes: none. inst/validation/: none. Tests: none.
validateTestInputs() infrastructure internal R/: generateHeteroRecommendations() (intelligent_recommendations.R:686), the test factory (test_factory.R:80). Vignettes: none. inst/validation/: none. Tests: 21 lines.
checkData() infrastructure internal R/: 16 lines in 7 files (advancedDiagnostics.R, advancedRemediation.R, fitWLS.R, HeteroDiagnostic.R, intelligent_recommendations.R, reporting.R, runPanelTests.R). Vignettes: none. inst/validation/: none. Tests: 9 lines.
TestFactory infrastructure internal R/: only test_factory.R:98, which builds test_factory. Vignettes: none. inst/validation/: none. Tests: 2 lines.
test_factory infrastructure internal R/: through its alias .test_factory, in .run_registered_diagnostic() (performance.R:610-612) and the dashboard (dashboard.R:53). Vignettes: none. inst/validation/: none. Tests: 10 lines.
cachedTest() infrastructure core API for now (decided); revisited at 1.0.0, once 0.16.0 bounds the caches
clearTestCache() infrastructure core API for now (decided); revisited at 1.0.0, once 0.16.0 bounds the caches
clearAnalysisCache() infrastructure core API for now (decided); revisited at 1.0.0, once 0.16.0 bounds the caches
ht_enable_log_capture() infrastructure internal R/: none. Vignettes: none. inst/validation/: none. Tests: none.
ht_log_history() infrastructure internal R/: none. Vignettes: none. inst/validation/: none. Tests: none.
ht_clear_log_history() infrastructure internal R/: none. Vignettes: none. inst/validation/: none. Tests: none.
rcalculateEffectSize() infrastructure internal R/: performWhiteTestRobust(), performBPTestRobust() (robust_implementations.R:158,292), restimate_test_power() (bootstrap_methods.R:599). Vignettes: none. inst/validation/: none. Tests: 4 lines.
restimate_test_power() infrastructure internal R/: performWhiteTestRobust(), performBPTestRobust() (robust_implementations.R:159,293). Vignettes: none. inst/validation/: none. Tests: 3 lines.
rrunAdvancedDiagnostics() runner core API (decided); its name is still open, see the item on the r-prefixed exports
rvalidate_against_reference() infrastructure internal R/: none. Vignettes: none. inst/validation/: none. Tests: 2 lines. The reference comparisons live in tests/testthat/.
launchDiagnosticDashboard() dashboard and benchmarks moved out (companion package not named yet; not deprecated in 0.14.0) R/: none outside dashboard.R. Vignettes: none. inst/validation/: none.
run_benchmark_suite() dashboard and benchmarks moved out (companion package not named yet; not deprecated in 0.14.0) R/: none outside benchmarking_system.R. Vignettes: none. inst/validation/: none.
generate_benchmark_report() dashboard and benchmarks moved out (companion package not named yet; not deprecated in 0.14.0) R/: none outside benchmarking_system.R. Vignettes: none. inst/validation/: none.

Notes on the verdicts:

  • performStudentizedBPTest() is provisional. Item 2 first asserts that it agrees with performKoenkerTest() to 1e-8 over the designs of the full sweep, weighted, rank-deficient and fitWLS() fits included. Disagreement anywhere is a finding, not a rename, and is reported before anything else.
  • test() warns and names runDiagnostics(x$model, x$data, tests), which returns what test() returns. summary.HeteroDiagnostic() keeps its return value, so no value changes in 0.14.0.
  • Registry names. "student_bp" and "ncv" stay registered and keep calling what they call now, so their output does not change. Each warns once per call of runHeteroTests() or runDiagnostics(), not once per group or bootstrap replicate, and goes in 1.0.0 with the function behind it.
  • The cache functions (cachedTest(), clearTestCache(), clearAnalysisCache()) stay exported and are not deprecated in 0.14.0. They are revisited at 1.0.0, once 0.16.0 bounds the caches.
  • rrunAdvancedDiagnostics(), runHeteroTestsParallel() and compareTestResults() stay core API. The name of rrunAdvancedDiagnostics() is still open under the item on the r-prefixed exports.
  • The simulation renames. simulate_size() and simulate_power() join the documented snake_case family, since both simulate under the sigma_*() functions.
  • bootstrapTestStatistic() takes a test function as its first argument, so it is not a perform*Test(model, data, ...); the new name says what it does in the house camelCase.
  • The dashboard and the benchmark suite are not deprecated in 0.14.0, because the companion package has no name yet. They call plotDiagnosticSuiteEnhanced(), the test factory and performNCVTest(), which change before the code moves.
  • analyzeMLResiduals() and analyseDatasetCharacteristics() spell the verb two ways. Both are core API; the roadmap names no change for them.

0.15.0: make the interface match its description (done)

These changes break existing calls. They have to happen before 1.0.0, not after it.

  • The (model, data, ...) convention. Nine exported tests did not follow it:

    • no data argument: performArchLMTest(), performMcLeodLiTest(), performCookWeisbergTest(), performNCVTest(), performSpearmanTest(), performSpreadLevelTest(), performDavidianCarrollTest() and performHarveyTest();
    • no model argument: performBoxMTest(data, group).

    Decided on 2026-10-15 as proposed: the eight take data = NULL in second position, recovered from the model frame when omitted, and performBoxMTest() is the documented exception, since it tests the data rather than residuals. A call that passes the old second argument by position (performArchLMTest(m, 3), performCookWeisbergTest(m, ~x), performHarveyTest(m, "fitted")) is recognised by its type, works in 0.15.x, warns with the new call and stops working in 1.0.0; NEWS.md has the table. No value changes: every old and new form returns what the old form returned in 0.14.0, checked with identical() against 0.14.0. Extended on 2026-10-15 to every other test: performRESETTest(model, power) as the eight, and the 29 tests that required data now default it to NULL, the model frame. A call that worked returns what it returned, except performRESETTest(m, NULL) and performRESETTest(m, <data frame>), which read the second argument as power and returned NaN or a meaningless F. - [x] Argument names. Harvey used studentize and performBPTestRobust() studentized. Standardised on studentize, which matches lmtest::bptest(); studentized is accepted with a deprecation warning in 0.15.x and stops working in 1.0.0, and the robust_details$studentized field of the result keeps its name. No value changes. - [x] The name of rrunAdvancedDiagnostics(). Renamed runAdvancedDiagnostics() (decided 2026-10-15), the house convention for a runner. The old name warns with .Deprecated() in 0.15.x and goes in 1.0.0 with the names deprecated in 0.14.0. No value changes. - [x] Resampling defaults. B was 499 for the wild bootstrap, 999 for rank permutation, and 1000 for the White bootstrap and bootstrapTestStatistic(). Under the (1 + #) / (B + 1) convention, a test at level alpha is exact only when alpha * (B + 1) is an integer, and 1000 fails that at 5%. Done (decided 2026-10-15: B = 999): every resampling function draws 999 by default, the robust tests with bootstrap = TRUE and runAdvancedDiagnostics() (500 before) and performSpatialHeteroTest() (499 permutations before) included. Default p-values change; NEWS.md has an example for each. Of the recorded evidence only the spatial row of the full sweep used a default, and it was regenerated: size 0.045 to 0.050, power 0.362 to 0.350. - [x] Wild bootstrap multiplier (Rademacher or Mammen). Settle it by a size study at small n with skewed errors, and record the result in inst/validation/.

    Measured on 2026-10-15 in inst/validation/wild-multiplier-size.R: the cross-sectional design of Pass A at 30, 50 and 100 observations, with Gaussian, t5 and centred exponential errors, 5000 replications and B = 999, both multipliers on the same samples. The decision rule was written into the script before the full run started; the script was first committed with its results. Neither multiplier holds the level in more cells, and no paired difference is outside its 99% interval (within Monte Carlo error). Both are inside the release gate in the same six of the nine size cells, those with Gaussian and t5 errors, and outside it in the same three, those with exponential errors, where Rademacher's multiplier rejects 0.082, 0.073 and 0.061 of the time and Mammen's 0.078, 0.070 and 0.059: Mammen's rate is lower in all three cells, each difference inside its 99% interval. The interval around 0.049, the rate of an exact test with B = 999, gives the same split. Against sigma^2 = exp(0.4 x1) the power of the two differs by less than one point at each n. Done (decided 2026-10-16: keep "rademacher", as the study recommended); no value changes. The help page said the bootstrap controls the size for heavy-tailed errors; that holds for the t5 errors of the study and not for the skewed ones, with either multiplier, and #125 corrected the page. A wild bootstrap without a multiplier, which may hold the level better under skewed errors, is to be measured in 0.16.0 (below). - [x] Reproducibility under parallel = TRUE. A search of R/ finds no clusterSetRNGStream() and no L'Ecuyer set-up on the mclapply() and parLapply() paths, so set.seed() does not make parallel resampling results reproducible. Done: replicate i draws from stream i of a L'Ecuyer-CMRG generator seeded by one draw from the session's, on both paths, in bootstrapTestStatistic(), performWhiteTestBootstrap() and runHeteroTestsParallel(), so the value does not depend on the number of workers. Serial calls are unchanged; parallel and serial values differ and are each valid (test-parallel-reproducibility.R). runHeteroTestsParallel() with one worker runs in the session on the same streams, so its value does not depend on n_cores either, and changes. The White bootstrap runs on a socket cluster on Windows, where it stopped, and gains n_cores. The forked path is exercised by CI on Linux and macOS only. - [x] Bootstrap tests on a fitWLS() fit. Accepted for 0.15.0 as proposed under Decisions needed, and moved from 0.14.0 because it changes values. performWildBootstrapTest(), performWhiteTestBootstrap(), bootstrapTestStatistic() and its deprecated name take reestimate_weights. NULL, the default, runs fitWLS() again on each replicate of a fit that carries its record of the variance function and holds the weights of any other model fixed; FALSE holds them fixed, as 0.13.0 and 0.14.0 did; TRUE on a model without the record is an error. Each replicate is fitted as the model fitWLS() was given, the recorded variance function is fitted again on the recorded regressors, and the replicate is refitted with the weights that gives; one whose variance function cannot be fitted counts as a failed replicate. The default p-values on a fitWLS() fit change, announced in bold in NEWS.md. An lm, a weighted lm and a Gaussian glm return identical() values, and so does a fitWLS() fit with FALSE. In inst/validation/weighted-fits-size.R, at 2000 replications with B = 499, the wild bootstrap rejects 0.0605 and 0.0515 of the time at 150 and 600 observations, the White bootstrap 0.0415 and bootstrapTestStatistic() with Koenker's statistic 0.045, against 0.0865, 0.105, 0.064 and 0.0825 with the weights held fixed. Those four rows were measured by the prototype of 0.12.0 and not run again: the package reproduces it on 200 samples of each, p-values, replicates and random draws (inst/validation/weighted-fits-refit-reproduction.csv). The wild bootstrap at 150 observations is outside the release gate's interval [0.042, 0.058] and the White bootstrap just below it; a study at 5000 replications is listed under 0.16.0. - [x] The default battery. runHeteroTests(), test()/summary() on HeteroDiagnostic, the advanced diagnostics and the dashboard all defaulted to c("white", "breusch_pagan"), and "breusch_pagan" is the classical statistic. The package's own sweep shows it rejecting 28.75% of the time under a t5 null, against 4.75% for Koenker. Done (decided 2026-10-15: White and Koenker): the default is c("white", "koenker") in those functions and in runDiagnostics(), compareModelDiagnostics(), compareTestResults() and runSurveyHeteroTests(), and the classical test runs by name. suggestRemediation() reads "koenker" as well as "breusch_pagan", and suggestDiagnosticsForProfile() recommends Koenker's test as the baseline. runAdvancedDiagnostics() already computed the studentized statistic by default and is unchanged. No recorded number moves. - [x] The default of robust in performGlejserTest(). It was FALSE, Glejser's (1969) statistic, which in inst/validation/glejser-skewed-errors.R rejects a true null hypothesis 8.8% to 17.7% of the time at the 5% level when the errors are skewed, at every sample size from 50 to 1000. Done (decided for 0.15.0 as proposed: TRUE): the default is the corrected statistic of Im (2000) and Machado and Santos Silva (2000), added in 0.13.0. From 150 observations it holds its level under every error distribution measured, inside the release gate in all 27 of its null cells; at 50 observations it rejects 5.7% to 6.6% of the time under skewed errors, four of those six cells above the gate, and gives up 1.1 and 1.9 points of power under Gaussian errors. Default values change; NEWS.md has an example. robust = FALSE returns what the default returned in 0.14.0, checked with identical() against 0.14.0. Of the recorded evidence the full sweep and the weighted fits call the default, and their two rows were regenerated from the same seeds: size 0.0600 to 0.0575 and 0.0475 to 0.0450. Pass A and the Glejser study name Glejser's statistic and now give robust = FALSE; their numbers do not move. - [x] Return contract. Document the fields every perform*Test() returns, both the htest core and the package's additions, on one help page. Check every exported test against it in a single parametrised test. This also retires the hand-maintained printer and metadata drift listed under the old technical debt. Done: ?test_results and test-return-contract.R. Five tests gained the name df of their parameter and performBoxMTest() its data.name; no number changed. - [x] Uniform input validation across all exported tests. The README lists its absence as a known limitation. test-input-validation.R gives every test a glm that is not a least-squares fit, a model with an NA coefficient, data that lack rows of the fit and data that lack a variable, and every test stops on each with the same message. Refusing the inputs some tests returned a value for was decided on 2026-10-15: 36 test names refuse a glm of a family other than gaussian or a link other than the identity, five refuse an NA coefficient, and the variance-form, Pesaran and random-effects tests refuse data that cannot be matched to the fit (NEWS.md lists the calls). A Gaussian glm with the identity link is accepted and gives the result of the lm. With prior weights the GQ, RESET, White bootstrap and wild bootstrap tests, which read the weights of an lm, read them since then, decided on 2026-10-15, where they took the glm for an unweighted fit, and the quantile regression test refuses it as it refuses a weighted lm; those calls change value. No recorded number moves.

0.16.0: statistics evidence, dependencies and internals (no API change)

  • Re-run the full sweep at N_MC = 5000, so that the release gate in inst/validation/README.md ([0.042, 0.058]) applies to every exported test, not only Passes A and B. At 400 replications the Monte Carlo standard error is 1.1%, so the Gaussian sizes between 0.068 and 0.072 (Cook–Weisberg and NCV, Spearman, Levene) cannot be told apart from noise. Pass A, at 5000 replications, put Cook–Weisberg at 0.044.
  • The bootstraps that estimate the weights again, at the release gate. The 0.15.0 default of reestimate_weights rests on rates measured at 2000 replications with B = 499 (inst/validation/weighted-fits-size.csv), where the 99% interval around 0.05 is [0.037, 0.063] and all four cells are inside it. Against the release gate's interval at 5000 replications, [0.042, 0.058], the wild bootstrap with 150 observations, 0.0605, is outside and the White bootstrap, 0.0415, just below it; against [0.041, 0.057], around 0.049, the rate of an exact test with B = 999, the wild bootstrap is outside. Measure the four cells of block 4 at 5000 replications with B = 999, the default, beside the true weights in the same design. From the time per sample measured in 0.15.0 that costs about 36 to 50 CPU-hours for the four cells, and about 7.6 for the two cells of the wild bootstrap alone. If the wild bootstrap stays outside the gate at 150 observations, say so on its help page.
  • performBoxMTest() is conservative, with size 0.012. It uses the chi-square approximation. Compare it against Box's F approximation, then either switch or document the conservatism on the help page.
  • performSzroeterTest() on a weighted fit. In inst/validation/harrison-mccabe-size-power.R, on a correctly weighted fit with Gaussian errors, it rejects 0.067 of the time at 40 observations, outside the release gate, and 0.056 and 0.055 at 100 and 400; without weights it is inside the gate at all three sizes. Its null mean and variance, (n + 1) / 2 and (n^2 - 1) / (6 n), do not depend on the design, and in that weighted design the start of the ordering has the larger leverages. Either take its null distribution from the weighted design, as performHarrisonMcCabeTest() does, or document the restriction on the help page.
  • The wild bootstrap without a multiplier. performWildBootstrapTest() already resamples the leverage-standardised residuals i.i.d., which imposes the null hypothesis, and multiplies them by v on top. In inst/validation/wild-multiplier-size.R it is outside the release gate with centred exponential errors at 30, 50 and 100 observations with either multiplier. An exploratory check by the reviewer of that study, on seeds of its own and not to evidence grade, suggests that the excess comes from the multiplier step: with no multiplier (v = 1, a plain residual bootstrap) the rejection rates were 0.048, 0.053 and 0.062 where Rademacher's gave 0.064, 0.067 and 0.077 on the same samples, with exponential errors at 50 and 30 observations, paired differences of about -1.4 to -1.6 points with standard errors of about 0.0018. Measure a no-multiplier option to evidence grade in the design of that study. The option, and still more a default, changes values the function returns, so it is the author's decision. The help page's claim about heavy-tailed errors is corrected in #125.
  • Provenance of a study run in chunks. Commit the script before a chunked run is launched, and record its md5 in the settings of each part, so that MERGE refuses parts written by another version of the script. wild-multiplier-size.R predates the convention: its parts carry no md5, and adding one now would make MERGE refuse them.
  • runAdvancedDiagnostics(tests = "bp") stops with "Unsupported test requested: bp", although its help page offers c("white", "bp") as a subset. The check compares the names asked for with the values of its lookup table ("white", "breusch_pagan") rather than with its names, which include "bp". Found on 2026-10-15 and recorded, not fixed, in 0.15.0.
  • runAdvancedDiagnostics(auto_enhance = FALSE) silently runs the classical Breusch–Pagan test. auto_enhance also sets studentize for performBPTestRobust(), so turning the small-sample bootstrap off turns Koenker's studentization off with it, and the result, named breusch_pagan either way, then assumes normal errors, which the default battery stopped doing in 0.15.0. Either separate the two settings or say on the help page what auto_enhance = FALSE runs. Found on 2026-10-15 and recorded, not fixed, in 0.15.0.
  • checkModel(<glm>, data, enhanced = TRUE) stops with "missing value where TRUE/FALSE needed", Gaussian and binomial alike: the enhanced check reads the R-squared of summary(), which a glm does not have. Since 0.15.0 performWhiteTestStreaming() refuses a glm before calling it; the check itself should refuse it with a clear message or skip that step. Found on 2026-10-15 and recorded, not fixed.
  • A weighted fit without data. On a fit lm(..., weights = w) whose w is a column of the data, the tests that rebuild the design with model.matrix(model, data = data) (performWhiteTest(), performBPTest(), performKoenkerTest() and others) stop with "object 'w' not found" when data is omitted, as they do when given the model frame, which holds the weights as (weights). Found on 2026-10-15; no value changes. The eight are performWhiteTest(), performWhiteTestRobust(), performWhiteTestBootstrap(), performBPTest() and performBreuschPaganTest(), performKoenkerTest(), performRankPermutationTest() and performHighDimensionalTest(), and a fit with offset = o, held as (offset), stops them the same way (found in the 0.15.0 release audit). Through runHeteroTests() and runDiagnostics() such a fit does not stop: White's and Koenker's tests fail and both names hold the Cook-Weisberg test the runner falls back on (below). A fix changes what those runner calls return.
  • The fallback of runHeteroTests(). When a test stops, the runner runs the substitute registered for it in .ht_fallback_registry (for "white": White's test without cross products, then the classical Breusch-Pagan, Koenker and Cook-Weisberg tests) and returns it under the name asked for, tagged by attr(, "diagnostic") and attr(, "extras")$status == "fallback" and announced in a WARN log line. $white can then hold another test. Documented on ?runHeteroTests since the 0.15.0 release audit; whether a failed test should rather be returned as failed is an interface decision for 1.0.0. Removing the fallback changes what such calls return.
  • A glm of another family through the registry. The registry names deprecated in 0.14.0, "ncv" and "student_bp", called .ncv_test() and .studentized_bp_test() directly, and the refusal of a glm that is not a least-squares fit was in performNCVTest() and performStudentizedBPTest() only. So runHeteroTests(m, d, tests = "ncv"), cachedTest("ncv", m, d) and runHeteroTestsParallel() returned a value for m <- glm(high ~ x1 + x2, family = binomial), and, because "ncv" is the last fallback of "white", "koenker" and "breusch_pagan", runHeteroTests(m, d) with its default battery returned the Cook-Weisberg statistic under the names white and koenker. Found in the 0.15.0 release audit. Done in 0.15.0 (decided 2026-10-16): the two registry entries, the "student_bp" entry of test_factory and the "studentized_bp" key of rvalidate_against_reference() refuse such a fit with the message of those functions, so runHeteroTests(m, d) stops with White's refusal and no name holds a fallback computed on it. On an lm, a Gaussian glm and a weighted lm every value is identical() to before.
  • The panel tests on a fit that dropped rows. On lm(y ~ x1 + x2, data = d) with NA in three rows of x2, performPesaranTest(m, d, "id", "time") stops with "requires data with 117 observations to match the fitted model, got 120" (since the 0.15.0 guard on the order of the rows; "arguments imply differing number of rows: 120, 117" before it), and with only the complete rows with "number of items to replace is not a multiple of replacement length"; performBPRandomEffectsTest(m, d, "id") refuses the full data with "requires data with 117 observations to match the fitted model, got 120", where the other tests match the rows by name. As in 0.14.0; found in the 0.15.0 release audit. A fix makes such calls return a value.
  • The deprecation messages of 0.14.0. The names deprecated in 0.14.0 that have a replacement (performBreuschPaganTest(), performNCVTest(), performStudentizedBPTest(), the three *Enhanced functions, rbootstrap_test_statistic(), test(), simulate_type_I_errors(), simulate_power_analysis(), and "student_bp" and "ncv" in runHeteroTests()) warn with the default text of .Deprecated(), which does not say that they go in 1.0.0, as the internal helpers, the 0.15.0 forms and rrunAdvancedDiagnostics() do. Found in the 0.15.0 release audit; no value changes.
  • The wild bootstrap in the advice for a small sample. 0.15.0 corrected the warning of warnDiagnosticAssumptions() for a sample of fewer than 100 observations, but two other strings still recommend the wild bootstrap for such a sample: suggestDiagnosticsForProfile() adds it with the rationale "Bootstrap inference performs well in small samples." (R/intelligent_recommendations.R:183), and step 1 of buildDiagnosticDecisionTree() says "Emphasise wild bootstrap and rank permutation tests." (:575). In inst/validation/wild-multiplier-size.R the wild bootstrap does not hold its level with skewed errors at 30, 50 and 100 observations: with centred exponential errors it rejects 0.082, 0.073 and 0.061 of the time at the 5% level with the Rademacher multiplier. Say what the warning says, or drop the claim. Recorded, not fixed, in 0.15.0; only the text changes.
  • ht_analyse_failure() labels a failure by its message, ignoring case. It calls a failure a rank deficiency when the message matches "rank|singular" and a data-quality issue when it matches "NA|NaN|missing|Inf", both with ignore.case = TRUE (R/error_handling.R:269-275), and ht_data_cleaning_suggestions() picks its advice by the same patterns. The refusals of a glm that is not a least-squares fit name the test, and two names match: on m <- glm(am ~ wt + hp, data = mtcars, family = binomial), runHeteroTests(m, mtcars, tests = "rank_permutation") stops with "Rank deficiency detected for Rank Permutation: performRankPermutationTest() needs a model fitted by stats::lm(): ..." and the advice to remove collinear predictors, and tests = "high_dimensional" with "Data quality issue detected", from the "na" of performHighDimensionalTest(), and the advice to remove missing values. The other tests refuse it as "Diagnostic execution failed". Match whole words, and NA only in capitals, or give the refusal of such a glm an issue of its own. Recorded, not fixed, in 0.15.0; only the message and the recorded issue change.
  • performFlignerKilleenTest() and a tie at the median. The statistic depends on rounding when the number of residuals is even: the two in the middle tie at exactly half their difference from the median, and which ranks first decides about 1e-6 of the statistic. Found on 2026-10-15 comparing a Gaussian glm with the same lm. Average the ranks of values tied to within a tolerance. Changes a value, by that much.
  • A Gaussian glm with a zero prior weight. The tests that read the Pearson residuals through rpearson_residuals() refuse an lm with a zero weight, whose Pearson residual is an artificial zero, but not the same glm: rpearson_residuals() reads rprior_weights(), which returns no weights for a glm. The Koenker, White, Glejser, Spearman, GQ and wild bootstrap tests return a value for it, and performWhiteTestBootstrap() stops with "Model residuals must be finite". Suggested fix: have rpearson_residuals() read rleast_squares_weights() instead of rprior_weights(). Two related gaps: plotDiagnostics() reads rprior_weights() too, so a weighted Gaussian glm is not labelled "(weighted residuals)", and performRESETTest() does not refuse a zero weight, even on an lm. Found on 2026-10-15 and recorded, not fixed, in 0.15.0, as decided that day. A fix changes what such fits return.
  • Defects found during the 0.12.0 and 0.13.0 work, left for this release so that 0.14.0 changes no value. Each fix that changes a value is announced in bold in NEWS.md.
    • fitWLS() keeps the original weights in the model frame of its result, so performStudentizedBPTest() on a fitWLS() fit reads stale weights: 14.81, against 1.73 for the equivalent lm(weights = estimated) fit. Done in 0.15.0 (decided 2026-10-16, in #129): .studentized_bp_test() reads the weights of the fit, through rleast_squares_weights(), rather than those of its model frame, which is unchanged. It changes the values of performStudentizedBPTest(), performBPTestRobust() and the registered "student_bp" on a fitWLS() fit only, and so the Breusch-Pagan entry of runAdvancedDiagnostics() on one.
    • .studentized_bp_test(), behind performStudentizedBPTest(), performBPTestRobust(studentize = TRUE) and the registered "student_bp", drops every offset, given as an argument or written in the formula: it refits the model with lm.wfit() and no offset. On lm(y ~ x + z, offset = o) in the design of tests/testthat/test-fitwls-reestimation.R it gives 19.91 where performKoenkerTest() gives 18.79. Found in the review of #129. Changes a value.
    • performWildBootstrapTest() and performWhiteTestBootstrap() accepted a non-Gaussian glm and returned a meaningless result. Done in 0.15.0 with the input validation: they refuse it, and read the prior weights of a Gaussian glm.
    • The single tests and performWhiteTestBootstrap() match a reordered tibble or data.table to the fit by position, through prepare_model_data_for_test(). Since 0.15.0 they refuse such data instead of returning wrong values: rcheck_data_rows(), called by rdata_or_model_frame(), compares every column of the model frame, on the rows the row names pick, with the model frame: the columns data holds under their own names value by value, and the others (log(y), log(x), poly(), ns(), offset(o), (weights), (offset)) computed from data through the predvars of the terms and the weights and offset expressions of the call. A term that depends on the rows it is computed from (I(x - mean(x)), I(x > median(x)), rank(x)), found by computing it again on half the rows, is not compared when it differs, and neither is a column that names a variable data lacks or whose computation fails; the compared columns must then leave no two rows of the fit that tie on them and differ in such a column computed from variables of data, and at least one column must be compared. The tests stop when a compared column differs, when such ties remain, when row names repeat, or when a fit with model = FALSE rebuilds a frame whose response or fitted values are not the fit's; runHeteroTests() and runDiagnostics() do not fall back on another test for it. The model context replaces this guard, so that such data give the value on the data of the fit. Changes a value for such data. Its cost on a valid call grows with the columns it computes again: at 100,000 rows it takes about 5 ms on lm(y ~ x1 + x2 + g) and about 43 ms on a weighted fit with log() and poly() terms, against 50 to 70 ms for the Glejser and Bartlett tests and about 200 ms for Koenker's on main. The review of 0.15.0 measured it at about a third of the Glejser (100 ms) and Bartlett (120 ms) calls at that size, before factors were compared by their codes; the model context, which reads the design from the fit, does not need it. Not covered by the guard:
      • fitWLS(data, var_formula) and performVarianceFormTest() with var_formula or against read the variables of the second formula through rvariance_frame(), which matches by name and, when no name matches and the counts agree, by position, unchecked (a reordered tibble changes their value).
      • The tests that read nothing from data (performSpearmanTest(), performHarveyTest(), performArchLMTest(), performMcLeodLiTest(), performCookWeisbergTest() without var_formula, performNCVTest(), performSpreadLevelTest(), performDavidianCarrollTest(), performVarianceFormTest() without a second formula, performRESETTest()) accept reordered data and return the value of the fit.
      • The columns a test reads besides the model (group, order_by, variable, id, time) are not compared. When every column of the model frame is compared, two rows that tie on all of them (response, regressors, weights, offset) can pass for each other, as with a binary or integer response; they then have the same design row, weight, offset and residual, so exchanging them moves only those other columns between identical observations. The review of the guard found that swapping such rows of an unweighted fit with no offset left the Bartlett, Glejser, Szroeter and GQ statistics unchanged.
      • A column computed from a vector of the workspace (weights = wvec, offset = ovec, log(x + k)) is not compared; no test reads it from data, so it needs nothing more.
      • The guard refuses data it cannot confirm, even the data of the fit: (i) when no column of the model frame can be compared, as with lm(d$y ~ d$x); (ii) when the compared columns tie on rows of the fit that differ in a term that depends on the rows or in a term of variables of data that cannot be computed, as with a discrete response and a row-dependent regressor on the rows an na.omit fit kept (lm(score ~ g + I(x > median(x)) + z2)), or y01 ~ myf(x) after rm(myf) and ns() after detach("package:splines"). The message names the columns; pass the full data the model was fitted on, with the rows it dropped, or the variables as columns of data.
      • A term that depends on the rows is recognised by computing it again on the odd and even rows and on each half, so every row is left out of one of them; one that gives the same values on all four by chance is compared, and refused on the rows a subset = or na.omit fit kept.
      • Data that reproduce every column of the model frame exactly but are another data set, with another extra column the test reads (g in T1b of the review), give a wrong value without an error, as in 0.14.0.
      • performScatterDiagnostic() refuses a tibble in the order of the fit of a model fitted to data with character row names, which it cannot match by name.
      • cut(x, quantile(x)) in the formula makes Koenker's test stop with "factor has new levels", on main and with the guard alike.
      • The second review of the guard measured it at about 20 to 40% of a cheap test (Bartlett, Glejser) at 100,000 rows.
      • A model = FALSE fit whose data changed after the fit is refused by the tests that read data, with or without it, but the tests that read only the model rebuild its frame too: performRESETTest() takes its response from that frame and stays wrong, as in 0.14.0.
      • bootstrapTestStatistic() and rbootstrap_test_statistic() keep their own check (0.13.0), and the panel tests and performScatterDiagnostic() refuse data with row names of their own in another order instead of matching it by name.
      • The (weights) column is compared with the weights expression of the call computed on data. If the fitWLS() defect above is fixed by writing the estimated weights into (weights) (decision 11 of the model-context design) while the call still says weights = w, every test given data on a fitWLS() fit of a weighted model would be refused; the model context, which takes the weights from the fit, avoids it, and this guard must then skip that column for such fits.
    • The caches of runHeteroTests(use_cache = TRUE) (about 33 KB per new model) and cachedTest() (about 25 KB per call) grow without bound, as the validation cache did before #97. No value changes.
    • A multivariate lm, cbind(y, y2) ~ ..., fails with a coercion error instead of a clear refusal. No value changes.
    • The unexported performWhiteTestEnhanced(bootstrap = TRUE), through bootstrap_white_test(), takes the log of a log response twice, drops an offset given as an argument and takes rows by position. Remove it or fix it. No exported value changes.
    • About 0.8 KB per test call on a new model still persists after the validation cache is cleared, as seen in long simulations. No value changes.
    • On a rank-deficient fit (z2 = 2 * z, the design in tests/testthat/test-koenker-studentized.R) both performKoenkerTest() and performStudentizedBPTest() refuse with "Model coefficients must be finite…", where lmtest::bptest() returns a statistic (20.87, df 2). Decide whether to compute on the estimable subspace or keep the refusal with a clearer message.
  • Dependencies.
    • Move curl to Suggests: only downloadTeachingData() uses it.
    • R6 goes if TestFactory leaves the API, since it is the only user.
    • Suggests shrinks with the companion split.
    • Settle whether digest belongs in Imports or Suggests, depending on whether caching stays exported.
  • Pass R CMD check with _R_CHECK_DEPENDS_ONLY_=true. With 39 packages in Suggests, every test, example and vignette that uses one must degrade gracefully. Audit the skip_if_not_installed() coverage; 20 of the 69 test files currently use it.
  • The R floor. DESCRIPTION claims R >= 4.1, but CI tests only devel, release and oldrel-1. Either add R 4.1 to the matrix or raise the floor to what is tested.
  • Replace non-ASCII characters in R/ with escapes (89 lines across 27 files).
  • Split R/validation.R (1,441 lines) into cache, result-type, assumption-checking and requirement-dispatch units, and factor out the repeated boilerplate: scalar validators, model and data preparation, and intercept stripping.
  • Retire TODO.md, SUGGESTIONS.md and cran.md. The open items in the first two all concern setup.sh and bootstrapping renv, and cran.md is a generic guide. With CRAN now the install route, decide whether setup.sh, renv.lock and the Dockerfile remain supported. If they do, complete renv.lock against Imports/Suggests so the Docker build uses it instead of install_local(). If they do not, remove them.
  • Keep the CRAN test run short. Long simulations run under skip_on_cran(), and the full versions run on CI.

1.0.0: freeze

  • Remove everything deprecated in 0.14.0 and 0.15.0, or turn it into .Defunct() stubs that name the replacement.
  • test-public-api.R asserts the frozen export list exactly.
  • Reference index of the documentation site (tools/reference-index.R) grouped by the frozen API: tests by family (auxiliary regression, group-wise, rank-based, time series, panel and spatial, resampling), remediation, simulation, plotting and reporting.
  • Vignettes. There are eight, and tutorial, using_heteroTests and comprehensive_guide overlap. Consolidate them into a short set that does not:
    • getting started;
    • choosing a test, turning the t5 and power tables into advice;
    • remediation;
    • theory and references.
  • Publish the stability policy from What 1.0.0 means in the README and on the documentation site.
  • NEWS.md for 1.0.0 with the complete old → new migration table.
  • Full --as-cran on every CI flavour, plus win-builder, then submit.
  • Update the R Journal manuscript, kept in its own repository, to the 1.0 API and submit it. Submitting on the frozen API avoids publishing function names that are about to be deprecated.

After 1.0.0

  • First-class panel and spatial heteroscedasticity workflows.
  • Helpers that bridge detection to conditional-variance modelling (ARCH/GARCH) for the time-series path.
  • An experiment/report artefact (model-card style) summarising a diagnostic run.
  • Streaming for more tests where it is meaningful, under a single chunk_threshold_mb policy.
  • Stronger feasible-WLS and auto-transform helpers: iterated feasible GLS, an estimator of the additive variance function that keeps its fitted variances positive, and a Box–Cox search.
  • A form of performVarianceFormTest() that does not need the standardized errors to have a constant fourth moment, following the robust statistic of Wooldridge (1991).
  • A single parametrised accuracy-validation test, with reference coverage broadened to skedastic and sandwich.
  • The companion package for the dashboard and the benchmark suite (decided on 2026-10-14; not yet named).

Candidate tests

None of these is scheduled. Each would be a new export, so each has to clear the release gate in inst/validation/README.md before it ships: agreement to 1e-8 with an established implementation or with a reconstruction of the primary reference, simulated size inside the gate, and power against the alternative it is built for.

The list is arranged by where the test can be found today, because that decides both what there is to validate against and what the package would add. Availability was checked on 2026-10-09 by searching CRAN, the R documentation sites and the web for each test by name. "No R implementation found" reports that search. It is not proof that none exists.

No R implementation found

A first implementation in R. Where another system has one, it is the reference; where none does, the guard is a reconstruction of the primary reference and simulated size.

  • Modified Wald test for groupwise heteroscedasticity in a fixed-effects panel (Greene 2000; Baum 2001). The panel tests cover an individual effect and cross-sectional dependence, not unequal variances across units. Reference implementation: Stata's xttest3. The Python package panelbox (1.0.2) has a function of that name that computes a different statistic, a likelihood-ratio form. A second search on 2026-10-10 found no R implementation either. Measured for 0.13.0 and not adopted: it does not hold its level. In the designs of inst/validation/modified-wald-size-power.R, at 5000 replications, the test as xttest3 computes it rejects a true null hypothesis at the 5% level this often:

    Errors Units T = 5 T = 10 T = 30 T = 100
    Gaussian 10 0.927 0.681 0.282 0.100
    Gaussian 30 1.000 0.966 0.584 0.183
    Gaussian 100 1.000 1.000 0.928 0.361
    t5 10 0.966 0.876 0.621 0.367
    t5 30 1.000 0.999 0.944 0.717
    t5 100 1.000 1.000 1.000 0.982

    Two unbalanced panels are at 0.809 and 1.000 under Gaussian errors. Each unit's term is divided by a variance estimated from the same few residuals, and the excess grows with the number of units. In long panels the error reverses, because the statistic tends to a chi-squared with one degree of freedom fewer than its reference: with 2 units and 1000 periods it rejects 0.014 of the time. Three of the study's 40 null cells fall inside the release gate, all at 300 or 1000 periods, where the two errors happen to cancel, and under t5 errors it still rejects 0.129 of the time with 10 units and 0.235 with 30 at 1000 periods. Before a test of this hypothesis can ship it needs a different variance estimate or a small-sample correction, which makes it a different test. Two leads, neither part of the study: Bartlett's test on the within residuals held its level under Gaussian errors in an exploratory run but, being a normal-theory test, not under t5 errors; and Juhl and Sosa-Escudero (2014, Journal of Econometrics 178, 484-494) derive studentized tests for heteroscedasticity after fixed-effects estimation with the number of periods fixed, whose fit to this hypothesis has not been checked. Greene's page 598, which Baum follows, could not be obtained, so whether Greene's own form differs from Baum's was not checked. - A joint LM test of homoscedasticity in a one-way error-component model (Baltagi, Bresson and Pirotte 2006), against heteroscedasticity in the individual effect and in the remainder error together. - Pagan–Hall (1983), the test of constant variance after instrumental-variables estimation, where Breusch–Pagan and White are valid only under further conditions. Reference implementation: Stata's ivhettest. The package accepts no instrumental-variables fit today, so this one starts with the input layer. - Kelejian–Robinson (1998), a joint test for spatial autocorrelation and heteroscedasticity. sphet::kpjtest() is a different test by Kelejian and Piras. - Newey–Powell (1987), a test built on asymmetric least squares: under constant variance the expectile regressions have equal slopes. expectreg fits expectiles and has no such test. - Tests in nonparametric regression, which have power against any smooth departure from constant variance and need no variance regressors to be named: Eubank and Thomas (1993), Dette and Munk (1998), Zheng (2009), and Chown and Müller (2018). The cost is a bandwidth or a difference sequence to choose. - A consistent test of a variance function: Dette, Neumeyer and Van Keilegom (2007), Wang and Zhou (2007), and Samarakoon and Song (2011). performVarianceFormTest() has power against the terms it is given; these have it against any departure, at the cost of smoothing. Stage 1 of the first measured for 0.13.0, and not exported: as defined, it does not hold its level. The 2007 article could not be obtained; a prototype in plain R follows the authors' submitted manuscript of 2005, whose Tables 4.1 and 4.5 it reproduces in level and to within a few points in power. In the designs of inst/validation/variance-form-size-power.R, with the variance function in one variable and the mean a linear model, the Cramér-von Mises statistic is inside the release gate in 4 of its 12 Gaussian null cells at 5000 replications, all four with constant variance at 150 or 400 observations; it rejects a true exponential or power variance function 0.019 to 0.036 of the time at 50 to 400 observations. The bootstrap draws its data from the least-squares fit of the variance function, on which the statistic's null distribution depends: drawn from the true shape, on the same samples, three of the four such cells come to 0.047 to 0.051. Against a departure the added terms of performVarianceFormTest() do not span it has the power that test lacks, 0.750 against 0.089 at 400 observations; against the other alternatives it has less. Design note, prototype and evidence: inst/validation/variance-form-dnvk-design.md and the variance-form-dnvk-* files beside it. What to do next is under Decisions needed. Wang and Zhou (2007) assume a parametric mean, which is this package's setting; Samarakoon and Song (2011) could not be obtained. Stage 1 of Wang and Zhou (2007) is parked on the branch feat/wang-zhou-stage1, to be resumed after 1.0.0: pass 1 of its size and power study is done, and the Gaussian null cells at 5000 replications that decide it have not been run. The branch's wip/wang-zhou-stage1/README.md says how to resume. - Bai, Pan and Yin (2018), a test of homoscedasticity when the number of regressors grows with the sample. Li–Yao, below, is a different test for the same setting. - A design-based test of constant variance for survey data. The tests assume an independent sample with equal weights, and runSurveyHeteroTests() rejects a homoscedastic model 37% to 100% of the time when selection depends on the response or the sample is clustered. The natural candidates regress the squared residuals of a survey::svyglm() fit on the variance regressors using the design, as a Wald test (survey::regTermTest()) or as a Rao score test. Both were measured in inst/validation/survey-designs-size.R and neither clears the gate: 6% to 25% under the null at 300 and 1000 observations when the weights vary with the regressor. The variance estimate needs a small-sample correction first.

In another R package, not in this one

These add no test to R. They add it to the common interface, and each comes with a reference implementation to agree with.

  • Sign and size bias tests (Engle and Ng 1993): whether the squared residuals respond differently to negative and to positive lagged residuals. The time-series tests detect ARCH effects and say nothing about asymmetry. rugarch::signbias() computes the tests for a fitted GARCH model.
  • A break in the variance at an unknown date: the CUSUM of squares test (Brown, Durbin and Evans 1975), the statistic of Inclán and Tiao (1994), or the kappa tests of Sansó, Aragó and Carrion (2004), which stay valid under heavy tails and conditional heteroscedasticity. Goldfeld–Quandt and Szroeter need the ordering and the split to be chosen in advance. Reference implementation for the last: micss::kappa_test().
  • Simonoff–Tsai (1994) and Verbyla (1993). Versions of the Cook–Weisberg score test built on modified profile likelihood and on residual maximum likelihood, for small samples and high-leverage designs. Reference implementations: skedastic::simonoff_tsai() and skedastic::verbyla().
  • Bickel (1978) and Anscombe (1961). Tests of the variance against the fitted values. Bickel's is the robust one. Reference implementations: skedastic::bickel() and skedastic::anscombe().
  • Li–Yao (2019) for regressions with many regressors. performHighDimensionalTest() regresses the squared residuals on principal components and cites no heteroscedasticity test as its source. Reference implementation: skedastic::li_yao().
  • A spatial Breusch–Pagan test (Anselin 1988) for fitted spatial models. performSpatialHeteroTest() applies Moran's I to the squared residuals, which detects variance that clusters in space and does not test it against regressors. Reference implementation: spatialreg::bptest.Sarlm().
  • The rest of skedastic. Of its 25 tests, the package has counterparts for eight. Besides those named above, skedastic provides bamset(), carapeto_holt(), diblasi_bowman(), dufour_etal(), evans_king(), godfrey_orme(), honda(), horn(), rackauskas_zuokas(), wilcox_keselman(), yuce() and zhou_etal(). None is a priority by itself; each is a candidate when a user asks for it.

Order of work

The first group is where the package can contribute something R does not have, which is also what a reader of the article asks of it. Within that group:

  1. The modified Wald test. The panel functions exist, and it is the test Stata users look for under the name xttest3. Measured for 0.13.0 and not adopted, because it does not hold its level (above).
  2. The Dette, Neumeyer and Van Keilegom test. It removes the main limitation of performVarianceFormTest(), and is the most work of the two. Stage 1 done for 0.13.0: as the manuscript defines it, it does not hold its level when the variance is not constant (above).

Done

  • A Glejser test that is valid under skewed errors (Im 2000; Machado and Santos Silva 2000), in 0.13.0, as performGlejserTest(robust = TRUE). In the designs of inst/validation/glejser-skewed-errors.R the uncorrected statistic rejects a true null hypothesis 9% to 18% of the time at the 5% level when the errors are skewed, at every sample size, and skedastic::glejser() uses the uncorrected absolute residuals as well. The corrected statistic is the default since 0.15.0 (the 0.15.0 milestone, above).
  • Harrison–McCabe (1979), in 0.13.0, as performHarrisonMcCabeTest(), beside Goldfeld–Quandt and Szroeter. The 1979 article was not read; the definition rests on Harrison (1981), by its first author, whose published exact critical values the function reproduces, and on lmtest::hmctest(), with which the statistic agrees to 1e-8. The p-value is exact under normal errors given the regressors. Neither reference computes that p-value: lmtest::hmctest() simulates it from a regression on a constant alone, and skedastic::harrison_mccabe() 2.0.3 drops repeated eigenvalues and, with deflator, leaves the design in the order of the rows. In the designs of inst/validation/harrison-mccabe-size-power.R it holds its level under Gaussian errors, rejects 12% to 16% of the time under t5 errors as the other two ordered tests do, and has less power than either.

Decisions needed

Question Proposal Needed by
Ties in the ordering of performSzroeterTest() Average the ranks within ties and adjust the null variance for them. Today ties keep the order of the rows the model was fitted to, which holds the level but makes the statistic depend on that order when order_by is discrete, and for the registered test on a model whose design rows repeat, as with factors, binary or other discrete regressors. It changes the values the function returns 0.16.0
Supported R floor Test the floor in CI, or raise it to oldrel-1 0.16.0
The name of the companion package for the dashboard and the benchmark suite None recorded. Once it has one, launchDiagnosticDashboard(), run_benchmark_suite() and generate_benchmark_report() warn here for a minor release, naming it, and the code moves 0.16.0
A consistent test of the variance function after stage 1 Do not ship the Dette, Neumeyer and Van Keilegom test as defined: its prototype rejects a true non-constant variance function 0.019 to 0.036 of the time. Choose between a new stage 1 for a modified procedure (a better estimate of the variance function for the bootstrap data, or a studentized statistic) and one for Wang and Zhou (2007), and compare the published article with the manuscript first. If a test ships, a function of its own rather than performVarianceFormTest(against = "any") Before any stage 2
setup.sh / renv / Docker Keep Docker for reproducibility; drop setup.sh now CRAN is the install route 0.16.0
Register the tests now callable as (model, data) None recorded. Since 0.15.0 performArchLMTest(), performMcLeodLiTest(), performSpearmanTest(), performDavidianCarrollTest() and performHarveyTest() can be called as (model, data) with no further argument, which is when a test goes in R/diagnosticsRegistry.R; registering one adds a name runHeteroTests() accepts and changes the article 1.0.0