#!/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)) }