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
82 lines
2.1 KiB
R
82 lines
2.1 KiB
R
#!/usr/bin/env Rscript
|
|
# Validate local/patches/registry.json: schema, referenced patch files, and
|
|
# ambiguous overlaps. Exits 1 on any problem. Used by pre-commit and CI.
|
|
#
|
|
# Defaults to local/patches/registry.json. To validate a candidate registry
|
|
# without touching the real one (e.g. from the patch proposer), set:
|
|
# PATCH_DIR directory patch-file paths resolve against (default local/patches)
|
|
# REGISTRY_FILE registry.json to validate (default <PATCH_DIR>/registry.json)
|
|
|
|
dir <- Sys.getenv("PATCH_DIR", unset = "local/patches")
|
|
registry_file <- Sys.getenv(
|
|
"REGISTRY_FILE",
|
|
unset = file.path(dir, "registry.json")
|
|
)
|
|
if (!file.exists(registry_file)) {
|
|
cat("No registry.json found; nothing to validate.\n")
|
|
quit(status = 0L)
|
|
}
|
|
|
|
or_q <- function(x) if (is.null(x)) "?" else x
|
|
|
|
reg <- jsonlite::fromJSON(registry_file, simplifyVector = FALSE)
|
|
required <- c("package", "versions", "platforms", "reason")
|
|
errs <- character(0L)
|
|
|
|
for (i in seq_along(reg)) {
|
|
e <- reg[[i]]
|
|
missing <- setdiff(required, names(e))
|
|
if (length(missing) > 0L) {
|
|
errs <- c(
|
|
errs,
|
|
sprintf(
|
|
"entry %d (%s): missing %s",
|
|
i,
|
|
if (is.null(e$package)) "?" else e$package,
|
|
toString(missing)
|
|
)
|
|
)
|
|
}
|
|
if (!is.null(e$patch)) {
|
|
p <- file.path(dir, e$patch)
|
|
if (!file.exists(p)) {
|
|
errs <- c(
|
|
errs,
|
|
sprintf("entry %d (%s): patch file '%s' missing", i, e$package, p)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
# Ambiguous overlap: two entries for the same package with identical platforms
|
|
# and versions.
|
|
keys <- vapply(
|
|
reg,
|
|
function(e) {
|
|
sprintf(
|
|
"%s|%s|%s",
|
|
or_q(e$package),
|
|
paste(sort(as.character(unlist(e$platforms))), collapse = ","),
|
|
or_q(e$versions)
|
|
)
|
|
},
|
|
character(1L)
|
|
)
|
|
dups <- keys[duplicated(keys)]
|
|
if (length(dups) > 0L) {
|
|
errs <- c(
|
|
errs,
|
|
sprintf("ambiguous duplicate entries: %s", toString(unique(dups)))
|
|
)
|
|
}
|
|
|
|
if (length(errs) > 0L) {
|
|
cat("Patch registry validation FAILED:\n")
|
|
cat(paste0(" - ", errs, "\n"))
|
|
quit(status = 1L)
|
|
}
|
|
cat(sprintf(
|
|
"Patch registry OK (%d %s).\n",
|
|
length(reg),
|
|
if (length(reg) == 1L) "entry" else "entries"
|
|
))
|