fix(local): route multi-signature packages to human triage in the proposer #119

Merged
pat-s merged 1 commit from t3code/fix-ambiguous-proposals into main 2026-07-14 17:11:55 +00:00
3 changed files with 80 additions and 1 deletions

View file

@ -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(

View file

@ -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)
} }

View file

@ -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")