Implement steps 3 + 4 of issue #115 on top of the failure classifier, now that bincraft v4.4.3 applies registry patches/makevars/configure_args to the target build (not just dependencies), so a trial patched build is meaningful. - refactor the classify helpers to expose a pure build_triage_report() and a list-returning entry builder; failing-builds-report.R now renders from it - add local/propose-patches.R (step 3, "propose, do not apply"): emit a pre-filled registry.json entry for each classified, safe, unregistered failure, validate the candidate set against a temporary merged registry, and (only on request) --write it plus a proposals ledger, or --open-issue a Forgejo tracking issue; the human gate and validator/trial-build acceptance stay, and novel source diffs / unknown signatures are never proposed - add local/trial-build-patch.R: isolated bincraft build of one package with the registry applied (no upload/archive/metadata) as the pre-merge gate - add local/proposal-tracking.R + local/proposal-tracking-lib.R (step 4): signature hit rate, proposed-vs-merged, and retirement candidates, with the pure helpers covered by tests - teach validate-patches.R optional PATCH_DIR/REGISTRY_FILE overrides so a candidate registry can be validated without touching the real one - document the propose/trial-build/tracking workflow in local/patches/README.md
135 lines
3.6 KiB
R
135 lines
3.6 KiB
R
#!/usr/bin/env Rscript
|
|
|
|
# Feedback loop for the failure classifier (issue #115, step 4): report the
|
|
# signature hit rate, proposed-vs-merged status, and registry entries that look
|
|
# retirable, so the rule set can improve and stale entries can be pruned.
|
|
# Read-only: queries `single_builds` and reads the registry + proposals ledger.
|
|
#
|
|
# Usage:
|
|
# PGPASS=... Rscript local/proposal-tracking.R [--json PATH]
|
|
|
|
options(error = function() {
|
|
cat("ERROR:", geterrmessage(), "\n", file = stdout())
|
|
q(status = 1)
|
|
})
|
|
|
|
suppressPackageStartupMessages({
|
|
library(DBI, quietly = TRUE)
|
|
library(RPostgres, quietly = TRUE)
|
|
library(jsonlite, quietly = TRUE)
|
|
})
|
|
|
|
script_path <- local({
|
|
a <- commandArgs(trailingOnly = FALSE)
|
|
f <- sub("^--file=", "", a[grepl("^--file=", a)])
|
|
if (length(f) == 1L && nzchar(f)) normalizePath(f) else NA_character_
|
|
})
|
|
script_dir <- if (is.na(script_path)) "local" else dirname(script_path)
|
|
source(file.path(script_dir, "failing-builds-classify.R"))
|
|
source(file.path(script_dir, "proposal-tracking-lib.R"))
|
|
|
|
args <- commandArgs(trailingOnly = TRUE)
|
|
json_out <- local({
|
|
i <- match("--json", args)
|
|
if (!is.na(i) && i < length(args)) args[[i + 1L]] else NA_character_
|
|
})
|
|
|
|
if (nchar(Sys.getenv("PGPASS")) == 0L) {
|
|
stop("PGPASS env var is not set; a DB password is required.")
|
|
}
|
|
|
|
registry_file <- file.path(script_dir, "patches", "registry.json")
|
|
ledger_file <- file.path(script_dir, "patches", "proposals-log.json")
|
|
|
|
con <- DBI::dbConnect(
|
|
RPostgres::Postgres(),
|
|
dbname = "build_metadata",
|
|
host = "r-binaries.devxy.io",
|
|
port = 15432,
|
|
user = "rpkgs",
|
|
password = Sys.getenv("PGPASS"),
|
|
sslmode = "require"
|
|
)
|
|
on.exit(DBI::dbDisconnect(con), add = TRUE)
|
|
|
|
failures <- DBI::dbGetQuery(
|
|
con,
|
|
paste0(
|
|
"SELECT name, tag, platform, arch, r_version, timestamp, error_text ",
|
|
"FROM single_builds WHERE error_occurred = TRUE AND removed = FALSE"
|
|
)
|
|
)
|
|
|
|
existing_entries <- if (file.exists(registry_file)) {
|
|
jsonlite::fromJSON(registry_file, simplifyVector = FALSE)
|
|
} else {
|
|
list()
|
|
}
|
|
registered_pkgs <- unique(vapply(
|
|
existing_entries,
|
|
function(e) as.character(e$package %||% ""),
|
|
character(1L)
|
|
))
|
|
ledger <- if (file.exists(ledger_file)) {
|
|
jsonlite::fromJSON(ledger_file, simplifyVector = FALSE)
|
|
} else {
|
|
list()
|
|
}
|
|
|
|
report <- build_triage_report(failures, registered_pkgs)
|
|
failing_pkgs <- unique(unlist(lapply(report, function(g) g$packages)))
|
|
|
|
hit <- signature_hit_rate(report, registered_pkgs)
|
|
pvm <- proposed_vs_merged(ledger, registered_pkgs)
|
|
retire <- retirement_candidates(existing_entries, failing_pkgs)
|
|
|
|
cat(sprintf(
|
|
"Feedback loop over %d failing builds, %d registry entries, %d ledger records.\n",
|
|
nrow(failures),
|
|
length(existing_entries),
|
|
length(ledger)
|
|
))
|
|
|
|
cat("\nSignature hit rate:\n")
|
|
cat(strrep("-", 60L), "\n", sep = "")
|
|
for (h in hit) {
|
|
cat(sprintf(
|
|
" %-26s %3d builds | %2d pkgs | %2d addressed | %2d open%s\n",
|
|
h$signature,
|
|
h$builds,
|
|
h$packages,
|
|
h$addressed,
|
|
h$open,
|
|
if (isTRUE(h$auto_proposable)) " [auto]" else ""
|
|
))
|
|
}
|
|
|
|
cat(sprintf("\nProposed vs merged: %d / %d merged.\n", pvm$merged, pvm$total))
|
|
for (r in pvm$records) {
|
|
cat(sprintf(" %-24s %-26s %s\n", r$package, r$signature, r$status))
|
|
}
|
|
|
|
cat(sprintf(
|
|
"\nRetirement candidates (%d): registry entries with no current failure.\n",
|
|
length(retire)
|
|
))
|
|
if (length(retire) > 0L) {
|
|
cat(paste0(" - ", retire, "\n"), sep = "")
|
|
} else {
|
|
cat(" (none)\n")
|
|
}
|
|
|
|
if (!is.na(json_out)) {
|
|
jsonlite::write_json(
|
|
list(
|
|
signature_hit_rate = hit,
|
|
proposed_vs_merged = pvm,
|
|
retirement_candidates = retire
|
|
),
|
|
json_out,
|
|
auto_unbox = TRUE,
|
|
pretty = TRUE,
|
|
null = "null"
|
|
)
|
|
cat(sprintf("\nWrote metrics to %s\n", json_out))
|
|
}
|