fix(local): route multi-signature packages to human triage in the proposer #119
3 changed files with 80 additions and 1 deletions
|
|
@ -6,6 +6,37 @@
|
||||||
# Kept free of DB/HTTP/clock so it can be sourced by the proposer, the tracker
|
# Kept free of DB/HTTP/clock so it can be sourced by the proposer, the tracker
|
||||||
# entrypoint, and the unit tests. Timestamps are passed in by callers.
|
# entrypoint, and the unit tests. Timestamps are passed in by callers.
|
||||||
|
|
||||||
|
# Split proposal candidates into the ones safe to emit and the ambiguous ones.
|
||||||
|
# A candidate is a list with at least `package` and `signature`. A package that
|
||||||
|
# maps to more than one distinct auto-proposable signature is genuinely
|
||||||
|
# ambiguous (two conflicting fix tiers, e.g. makevars vs source patch): emitting
|
||||||
|
# both would create colliding registry entries, so those are routed to human
|
||||||
|
# triage instead of guessed at. Returns list(keep = ..., ambiguous = ...), where
|
||||||
|
# `ambiguous` is a named list of package -> the distinct signatures seen.
|
||||||
|
dedupe_candidates <- function(candidates) {
|
||||||
|
if (length(candidates) == 0L) {
|
||||||
|
return(list(keep = list(), ambiguous = list()))
|
||||||
|
}
|
||||||
|
pkgs <- vapply(candidates, function(c) as.character(c$package), character(1L))
|
||||||
|
by_pkg <- split(candidates, pkgs)
|
||||||
|
keep <- list()
|
||||||
|
ambiguous <- list()
|
||||||
|
for (pkg in names(by_pkg)) {
|
||||||
|
cs <- by_pkg[[pkg]]
|
||||||
|
sigs <- unique(vapply(
|
||||||
|
cs,
|
||||||
|
function(c) as.character(c$signature),
|
||||||
|
character(1L)
|
||||||
|
))
|
||||||
|
if (length(sigs) == 1L) {
|
||||||
|
keep[[length(keep) + 1L]] <- cs[[1L]] # one signature -> take the first
|
||||||
|
} else {
|
||||||
|
ambiguous[[pkg]] <- sigs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list(keep = keep, ambiguous = ambiguous)
|
||||||
|
}
|
||||||
|
|
||||||
# Stable identity of a ledger record: one proposal per (package, signature).
|
# Stable identity of a ledger record: one proposal per (package, signature).
|
||||||
ledger_key <- function(record) {
|
ledger_key <- function(record) {
|
||||||
paste0(
|
paste0(
|
||||||
|
|
|
||||||
|
|
@ -135,9 +135,25 @@ for (r in report) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# A package that maps to more than one auto-proposable signature is ambiguous
|
||||||
|
# (conflicting fix tiers) and would collide on the same registry key; route it
|
||||||
|
# to human triage instead of emitting both.
|
||||||
|
split_candidates <- dedupe_candidates(candidates)
|
||||||
|
candidates <- split_candidates$keep
|
||||||
|
if (length(split_candidates$ambiguous) > 0L) {
|
||||||
|
cat("\nAmbiguous (multiple signatures) -> human triage, not proposed:\n")
|
||||||
|
for (pkg in names(split_candidates$ambiguous)) {
|
||||||
|
cat(sprintf(
|
||||||
|
" %s: %s\n",
|
||||||
|
pkg,
|
||||||
|
toString(split_candidates$ambiguous[[pkg]])
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (length(candidates) == 0L) {
|
if (length(candidates) == 0L) {
|
||||||
cat(
|
cat(
|
||||||
"No auto-proposable candidates (nothing classified, safe, and unregistered).\n"
|
"\nNo auto-proposable candidates (nothing classified, safe, unregistered, and unambiguous).\n"
|
||||||
)
|
)
|
||||||
q(status = 0)
|
q(status = 0)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,38 @@ test_that("merge_ledger handles an empty/NULL starting ledger", {
|
||||||
expect_length(merge_ledger(list(), new), 1L)
|
expect_length(merge_ledger(list(), new), 1L)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test_that("dedupe_candidates keeps single-signature pkgs, routes conflicts to triage", {
|
||||||
|
candidates <- list(
|
||||||
|
list(package = "StanHeaders", signature = "tbb-stddef-removed"),
|
||||||
|
list(package = "hmmTMB", signature = "tbb-stddef-removed"),
|
||||||
|
list(package = "hmmTMB", signature = "rcppparallel-bundled-tbb") # conflict
|
||||||
|
)
|
||||||
|
out <- dedupe_candidates(candidates)
|
||||||
|
kept <- vapply(out$keep, function(c) c$package, character(1L))
|
||||||
|
expect_identical(sort(kept), "StanHeaders") # hmmTMB dropped as ambiguous
|
||||||
|
expect_true("hmmTMB" %in% names(out$ambiguous))
|
||||||
|
expect_setequal(
|
||||||
|
out$ambiguous$hmmTMB,
|
||||||
|
c("tbb-stddef-removed", "rcppparallel-bundled-tbb")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test_that("dedupe_candidates collapses a package repeated under one signature", {
|
||||||
|
candidates <- list(
|
||||||
|
list(package = "rstan", signature = "tbb-stddef-removed"),
|
||||||
|
list(package = "rstan", signature = "tbb-stddef-removed")
|
||||||
|
)
|
||||||
|
out <- dedupe_candidates(candidates)
|
||||||
|
expect_length(out$keep, 1L)
|
||||||
|
expect_length(out$ambiguous, 0L)
|
||||||
|
})
|
||||||
|
|
||||||
|
test_that("dedupe_candidates handles the empty list", {
|
||||||
|
out <- dedupe_candidates(list())
|
||||||
|
expect_length(out$keep, 0L)
|
||||||
|
expect_length(out$ambiguous, 0L)
|
||||||
|
})
|
||||||
|
|
||||||
test_that("signature_hit_rate splits addressed vs open per signature", {
|
test_that("signature_hit_rate splits addressed vs open per signature", {
|
||||||
report <- build_triage_report(mk_failures(), registered_pkgs = "RcppParallel")
|
report <- build_triage_report(mk_failures(), registered_pkgs = "RcppParallel")
|
||||||
hit <- signature_hit_rate(report, registered_pkgs = "RcppParallel")
|
hit <- signature_hit_rate(report, registered_pkgs = "RcppParallel")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue