feat(local): detect dependency-cascade failures generally, not just RcppParallel

The first trial-build gate run correctly went red: the 10 auto-proposed
tbb-stddef packages (BFpack, BayesERtools, GMLTM, ...) all fail while building
their shared `rstan` dependency, not in their own code -- the `tbb/tbb_stddef.h`
error is in rstan's Module.cpp. So a per-package makevars entry is useless for
them; they are blocked on rstan (which already has an entry). This is the same
cascade the RcppParallel `applies_to` guard catches, but the tbb-stddef
signature is generic and had no such pin.

Add `failing_dependency(error_text, package)`: when the log shows a DIFFERENT
package failed to compile ("Failed to build source package X", "compilation
failed for package 'X'", ...), that package is the real cause. build_triage_report
now blocks any package whose every failing build is such a cascade -- reported
as blocked_on the dependency, never proposed a bogus entry. A package that fails
in its OWN compilation is still proposed. This unifies the applies_to and
data-driven cases into one `blocked_packages` / `blocked_on` model.

- report/proposer/blocked_summary now count the actually-blocked packages
- blocked note shows even when a group also has genuine proposals
- tests cover the rstan-cascade vs own-compile split (with the real log strings)
This commit is contained in:
Patrick Schratz 2026-07-15 21:33:54 +00:00
commit c98aaa548f
No known key found for this signature in database
GPG key ID: 62050D5BC68AB6DC

View file

@ -155,6 +155,44 @@ fingerprint_error <- function(error_text, package = NULL, max_chars = 200L) {
fp
}
# ---------------------------------------------------------------------------
# Dependency-cascade detection
# ---------------------------------------------------------------------------
# If a build's error_text shows the failure was actually in a DIFFERENT package
# (a dependency that would not compile), return that dependency's name;
# otherwise NA. Used to avoid proposing a per-package fix for a package that
# only fails because a shared dependency does not build (e.g. the ~73 Stan
# packages that fail while building `rstan`). Generalises the RcppParallel
# `applies_to` guard to any dependency named in the log.
failing_dependency <- function(error_text, package) {
if (length(error_text) == 0L || is.na(error_text) || !nzchar(error_text)) {
return(NA_character_)
}
x <- as.character(error_text)
# optional opening quote before the package name: apostrophe, double-quote,
# backtick, or curly quotes -- written as \u escapes so the pattern stays
# valid UTF-8 regardless of source encoding.
q <- "[\u0027\u0022\u0060\u2018\u2019]?"
name <- "([A-Za-z][A-Za-z0-9._]+)"
# Markers R/pak emit naming the package that actually failed to compile.
pats <- c(
paste0("compilation failed for package ", q, name),
paste0("Failed to build source package ", q, name),
paste0("Error in building package ", q, name),
paste0("dependenc(?:y|ies) ", q, name, q, "?[^\\n]*not available")
)
deps <- character(0L)
for (p in pats) {
hits <- regmatches(x, gregexpr(p, x, perl = TRUE))[[1L]]
if (length(hits) > 0L) {
deps <- c(deps, sub(p, "\\1", hits, perl = TRUE))
}
}
deps <- sub("[._]+$", "", deps) # drop a trailing sentence period (e.g. "rstan.")
deps <- setdiff(unique(deps), package) # a package failing on its OWN code is not a cascade
if (length(deps) == 0L) NA_character_ else deps[[1L]]
}
# ---------------------------------------------------------------------------
# Classification
# ---------------------------------------------------------------------------
@ -291,18 +329,38 @@ build_triage_report <- function(
unregistered <- setdiff(pkgs, registered_pkgs)
auto_proposable <- isTRUE(sig$auto) && sig$matched
# A signature whose fix is package-specific (`applies_to`) may only be
# proposed for that package. Everything else matching it is a downstream
# failure blocked on that package (e.g. RcppParallel dependents carrying
# RcppParallel's own error text) -- never propose those a bogus entry.
# Decide, per package, whether it is genuinely fixable or merely blocked on
# a dependency (so a per-package entry would be useless). Two blocking modes:
# 1. `applies_to`: a package-specific patch (e.g. RcppParallel's) is only
# valid for its own package; other matches are downstream of it.
# 2. data-driven cascade: the package's error_text shows a *different*
# package failed to compile (e.g. the ~73 Stan packages blocked on rstan).
# `blocked_map` maps a blocked package -> the dependency it waits on.
g$blocked_dep <- vapply(
seq_len(nrow(g)),
function(i) failing_dependency(g$error_text[[i]], g$name[[i]]),
character(1L)
)
proposable <- unregistered
blocked_on <- NULL
blocked_map <- list()
if (!is.null(sig$applies_to)) {
proposable <- intersect(unregistered, sig$applies_to)
downstream <- setdiff(pkgs, sig$applies_to)
if (length(downstream) > 0L) {
blocked_on <- sig$applies_to
for (p in setdiff(pkgs, sig$applies_to)) {
blocked_map[[p]] <- sig$applies_to
}
proposable <- intersect(proposable, sig$applies_to)
}
for (p in proposable) {
deps <- g$blocked_dep[g$name == p]
if (!any(is.na(deps))) {
# every failing build of p is a cascade -> blocked, not fixable here
blocked_map[[p]] <- unique(deps)
}
}
proposable <- setdiff(proposable, names(blocked_map))
blocked_packages <- names(blocked_map)
blocked_on <- unique(unlist(blocked_map, use.names = FALSE))
if (length(blocked_on) == 0L) {
blocked_on <- NULL
}
proposed <- list()
@ -326,6 +384,7 @@ build_triage_report <- function(
suggested_fix = sig$fix,
applies_to = sig$applies_to,
blocked_on = blocked_on,
blocked_packages = blocked_packages,
fingerprint = names(fp_tab)[[1L]],
fingerprint_variants = length(fp_tab),
build_count = nrow(g),