feat(local): auto-propose registry patches and track the feedback loop (#117)
Implements steps 3 + 4 of #115, building on the classifier merged in #116. Now that bincraft **v4.4.3** applies registry `patch`/`makevars`/`configure_args` to the *target* package build (previously deps-only), a trial patched build is a meaningful acceptance gate, so the "propose" half is viable. ## Step 3 — propose, do not apply - **`local/propose-patches.R`** — for each classified, safe fix affecting a package with no current registry entry, emits a pre-filled `registry.json` entry and validates the candidate set against a *temporary* merged registry (the real one is never touched unless asked). - default: print candidates + validation, **take no action** - `--write`: append entries to `registry.json` + the proposals ledger (you commit + open the PR) - `--open-issue`: post/update a Forgejo tracking issue (reuses the weekly-audit `httr2` + `FORGEJO_TOKEN` pattern) - **`local/trial-build-patch.R`** — isolated bincraft build of one package with the registry applied (no upload/archive/metadata; `patchhash` keeps it out of the real cache). Exit 0/1, so it gates a CI step or manual pre-merge check. The human gate stays: nothing merges. Acceptance = `validate-patches.R` passes (checked automatically) **and** the trial build succeeds. Novel source diffs and unknown signatures are never proposed (they carry `auto = FALSE`). ## Step 4 — feedback loop - **`local/proposal-tracking.R`** (read-only) — signature hit rate (builds/pkgs/addressed/open per signature), proposed-vs-merged (a proposal counts merged once its package is in the registry), and retirement candidates (registry entries whose package no longer fails, i.e. likely fixed upstream). - **`local/proposal-tracking-lib.R`** — the pure metric/ledger helpers. ## Supporting changes - Refactored the classify helpers to expose a pure `build_triage_report()` + a list-returning entry builder; `failing-builds-report.R` now renders from the shared function (no behaviour change). - `validate-patches.R` gains optional `PATCH_DIR`/`REGISTRY_FILE` overrides (backward-compatible) so a candidate registry can be validated in isolation. - Documented the propose/trial-build/tracking workflow in `local/patches/README.md`. ## Verification - 71 unit tests pass (incl. new `test-proposal-tracking-lib.R`) under the Dockerized R 4.5.3 build env. - All pre-commit hooks pass (`air-format`, `validate-patches`, prettier, etc.). - Smoke-tested all three entrypoints end-to-end with a stubbed DB: dry-run, `--write` (produces a registry that passes the canonical validator + a valid ledger, then reverted), and the tracker. Closes #115 Reviewed-on: #117
This commit is contained in:
parent
f11ba7172f
commit
1c297c01bf
1 changed files with 990 additions and 119 deletions
105
local/tests/test-proposal-tracking-lib.R
Normal file
105
local/tests/test-proposal-tracking-lib.R
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
source(file.path("..", "proposal-tracking-lib.R"))
|
||||
source(file.path("..", "failing-builds-classify.R"))
|
||||
|
||||
mk_failures <- function() {
|
||||
data.frame(
|
||||
name = c("StanHeaders", "rstan", "RcppParallel", "somepkg"),
|
||||
platform = c("alpine-321", "alpine-321", "alpine-320", "redhat-9"),
|
||||
arch = c("amd64", "arm64", "amd64", "amd64"),
|
||||
error_text = c(
|
||||
"fatal error: tbb/tbb_stddef.h: No such file or directory",
|
||||
"In file: tbb/tbb_stddef.h: No such file or directory",
|
||||
"Error: USE_TBB=Linux is not supported; bundled TBB on musl",
|
||||
"some unmatched failure"
|
||||
),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
}
|
||||
|
||||
test_that("merge_ledger appends new proposals and preserves existing history", {
|
||||
existing <- list(list(
|
||||
package = "fs",
|
||||
signature = "system-libuv-link-leak",
|
||||
status = "merged"
|
||||
))
|
||||
new <- list(
|
||||
list(
|
||||
package = "fs",
|
||||
signature = "system-libuv-link-leak",
|
||||
status = "proposed"
|
||||
),
|
||||
list(
|
||||
package = "StanHeaders",
|
||||
signature = "tbb-stddef-removed",
|
||||
status = "proposed"
|
||||
)
|
||||
)
|
||||
merged <- merge_ledger(existing, new)
|
||||
expect_length(merged, 2L) # fs is deduped, StanHeaders added
|
||||
fs <- Filter(function(r) r$package == "fs", merged)[[1L]]
|
||||
expect_identical(fs$status, "merged") # existing status preserved, not clobbered
|
||||
})
|
||||
|
||||
test_that("merge_ledger handles an empty/NULL starting ledger", {
|
||||
new <- list(list(package = "x", signature = "s"))
|
||||
expect_length(merge_ledger(NULL, new), 1L)
|
||||
expect_length(merge_ledger(list(), new), 1L)
|
||||
})
|
||||
|
||||
test_that("signature_hit_rate splits addressed vs open per signature", {
|
||||
report <- build_triage_report(mk_failures(), registered_pkgs = "RcppParallel")
|
||||
hit <- signature_hit_rate(report, registered_pkgs = "RcppParallel")
|
||||
tbb <- Filter(function(h) h$signature == "tbb-stddef-removed", hit)[[1L]]
|
||||
expect_identical(tbb$packages, 2L) # StanHeaders + rstan
|
||||
expect_identical(tbb$addressed, 0L)
|
||||
expect_identical(tbb$open, 2L)
|
||||
expect_true(tbb$auto_proposable)
|
||||
|
||||
rcpp <- Filter(function(h) h$signature == "rcppparallel-bundled-tbb", hit)[[
|
||||
1L
|
||||
]]
|
||||
expect_identical(rcpp$addressed, 1L) # already registered
|
||||
expect_identical(rcpp$open, 0L)
|
||||
|
||||
# Unclassified failures never appear as a signature.
|
||||
expect_false(
|
||||
"unclassified" %in% vapply(hit, function(h) h$signature, character(1L))
|
||||
)
|
||||
})
|
||||
|
||||
test_that("proposed_vs_merged marks a package merged once it is registered", {
|
||||
ledger <- list(
|
||||
list(
|
||||
package = "StanHeaders",
|
||||
signature = "tbb-stddef-removed",
|
||||
status = "proposed"
|
||||
),
|
||||
list(
|
||||
package = "rstan",
|
||||
signature = "tbb-stddef-removed",
|
||||
status = "proposed"
|
||||
)
|
||||
)
|
||||
pvm <- proposed_vs_merged(ledger, registered_pkgs = "StanHeaders")
|
||||
expect_identical(pvm$total, 2L)
|
||||
expect_identical(pvm$merged, 1L)
|
||||
stan <- Filter(function(r) r$package == "StanHeaders", pvm$records)[[1L]]
|
||||
expect_identical(stan$status, "merged")
|
||||
})
|
||||
|
||||
test_that("retirement_candidates flags entries whose package no longer fails", {
|
||||
entries <- list(
|
||||
list(package = "RcppParallel"),
|
||||
list(package = "oldpkg")
|
||||
)
|
||||
# RcppParallel still fails; oldpkg does not -> only oldpkg is retirable.
|
||||
out <- retirement_candidates(
|
||||
entries,
|
||||
failing_pkgs = c("RcppParallel", "StanHeaders")
|
||||
)
|
||||
expect_identical(out, "oldpkg")
|
||||
expect_length(
|
||||
retirement_candidates(entries, failing_pkgs = c("RcppParallel", "oldpkg")),
|
||||
0L
|
||||
)
|
||||
})
|
||||
Loading…
Reference in a new issue