### Task A1: Registry loading and normalization **Files:** - Create: `R/patches.R` - Test: `tests/testthat/test-patches.R` **Interfaces:** - Produces: `load_patch_registry(patches_dir)` → `list()` of normalized entries; each entry is a named list with `package`, `versions`, `platforms` (character vector), `env` (named list), `configure_args` (character), `makevars` (named list), `reason`, and `patch_path` (absolute path or `NULL`). `normalize_patch_entry(entry, patches_dir)` → one normalized entry; errors on missing required field or missing patch file. - [ ] **Step 1: Write the failing test** ```r # tests/testthat/test-patches.R test_that("load_patch_registry parses and normalizes entries", { dir <- withr::local_tempdir() writeLines("--- a patch ---", file.path(dir, "fix.patch")) jsonlite::write_json( list(list( package = "RcppParallel", versions = "*", platforms = list("alpine", "ubuntu-2604"), env = list(RCPP_PARALLEL_USE_TBB = "0"), patch = "fix.patch", reason = "bundled TBB fails" )), file.path(dir, "registry.json"), auto_unbox = TRUE ) reg <- load_patch_registry(dir) expect_length(reg, 1L) expect_identical(reg[[1L]]$package, "RcppParallel") expect_identical(reg[[1L]]$platforms, c("alpine", "ubuntu-2604")) expect_identical(reg[[1L]]$env$RCPP_PARALLEL_USE_TBB, "0") expect_identical(reg[[1L]]$configure_args, character(0L)) expect_true(file.exists(reg[[1L]]$patch_path)) }) test_that("load_patch_registry returns empty list when no registry", { expect_identical(load_patch_registry(NULL), list()) expect_identical(load_patch_registry(withr::local_tempdir()), list()) }) test_that("normalize_patch_entry errors on missing required field", { expect_error( normalize_patch_entry(list(package = "x"), tempdir()), "missing required field" ) }) test_that("normalize_patch_entry errors on missing patch file", { expect_error( normalize_patch_entry( list(package = "x", versions = "*", platforms = "alpine", reason = "r", patch = "nope.patch"), tempdir() ), "does not exist" ) }) ``` - [ ] **Step 2: Run test to verify it fails** Run: `Rscript -e 'devtools::load_all("."); testthat::test_file("tests/testthat/test-patches.R")'` Expected: FAIL with "could not find function load_patch_registry". - [ ] **Step 3: Write minimal implementation** ```r # R/patches.R #' Load and validate the patch registry #' #' Reads `registry.json` from `patches_dir` and returns normalized entries. #' #' @param patches_dir Directory containing `registry.json` and any patch files, #' or `NULL` to disable patching. #' @return A list of normalized patch entries (possibly empty). #' @keywords internal load_patch_registry <- function(patches_dir) { if (is.null(patches_dir)) { return(list()) } registry_file <- file.path(patches_dir, "registry.json") if (!file.exists(registry_file)) { log_warn(sprintf( "Patch directory {.path %s} has no registry.json; patching disabled.", patches_dir )) return(list()) } raw <- jsonlite::fromJSON(registry_file, simplifyVector = FALSE) lapply(raw, normalize_patch_entry, patches_dir = patches_dir) } #' Normalize and validate a single patch registry entry #' #' @param entry A list parsed from `registry.json`. #' @param patches_dir Directory used to resolve a relative `patch` path. #' @return The entry with defaults filled and `patch_path` resolved. #' @keywords internal normalize_patch_entry <- function(entry, patches_dir) { required <- c("package", "versions", "platforms", "reason") missing <- setdiff(required, names(entry)) if (length(missing) > 0L) { stop( sprintf("Patch entry is missing required field(s): %s", toString(missing)), call. = FALSE ) } entry$platforms <- as.character(unlist(entry$platforms)) entry$env <- if (is.null(entry$env)) list() else entry$env entry$configure_args <- if (is.null(entry$configure_args)) { character(0L) } else { as.character(unlist(entry$configure_args)) } entry$makevars <- if (is.null(entry$makevars)) list() else entry$makevars if (!is.null(entry$patch)) { patch_path <- file.path(patches_dir, entry$patch) if (!file.exists(patch_path)) { stop( sprintf( "Patch file '%s' for package '%s' does not exist.", patch_path, entry$package ), call. = FALSE ) } entry$patch_path <- patch_path } else { entry$patch_path <- NULL } entry } ``` - [ ] **Step 4: Run test to verify it passes** Run: `Rscript -e 'devtools::load_all("."); testthat::test_file("tests/testthat/test-patches.R")'` Expected: PASS (4 tests). - [ ] **Step 5: Commit** ```bash git add R/patches.R tests/testthat/test-patches.R git commit -m "feat(patches): load and validate the patch registry" ``` ---