Add blessCacheAs() to vouch for a file an older version wrote

Files from the 1.3.0.9000 development cycle already hold correctly
computed scores - the corrections landed there, before the release was
cut - but they say nothing about what wrote them, so the check added for
1.4.0 recomputes them. That is right in general and wrong for exactly
the files whose owners know better, and a collocation analysis they would
have to sit through again runs for hours.

blessCacheAs() records that a file holds what this version would compute.
What actually produced it is left as it stands and the blessing noted
beside it, so cacheAsInfo() goes on saying where the numbers come from
rather than claiming a provenance the file does not have.

A file recording no parameters, as those written before 1.3.0.9000 do,
has nothing left to compare against a call once blessed, and is reused
for any call naming it. Blessing one says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change-Id: I0c46322154441b11547da205b2aa0fb5cf98371d
diff --git a/R/cacheAs.R b/R/cacheAs.R
index 6643d4a..c39b305 100644
--- a/R/cacheAs.R
+++ b/R/cacheAs.R
@@ -79,7 +79,10 @@
 #' @return a sentence naming the reason, or `NULL` if the file can be used
 #' @noRd
 cacheAsRejectionReason <- function(stored, current) {
-  generation <- if (is.null(stored)) NULL else stored$scoreVersion
+  # a file vouched for by blessCacheAs() counts as computed the way this version
+  # would compute it, whatever version actually wrote it
+  confirmed <- if (is.null(stored)) NULL else stored$scoresConfirmedFor
+  generation <- if (is.null(confirmed)) stored$scoreVersion else confirmed
 
   if (is.null(generation) || package_version(generation) < package_version(cacheAsScoreVersion)) {
     writtenBy <- if (is.null(stored)) NULL else stored$packageVersion
@@ -96,6 +99,12 @@
     })
   }
 
+  # a blessed file may not say what it was computed with, in which case there is
+  # nothing to compare and the blessing has to stand for it
+  if (is.null(stored$parameters)) {
+    return(NULL)
+  }
+
   differing <- character(0)
   for (name in union(names(stored$parameters), names(current$parameters))) {
     if (!identical(stored$parameters[[name]], current$parameters[[name]])) {
@@ -116,6 +125,62 @@
   }
 }
 
+#' Vouch for a cacheAs file that an older version wrote
+#'
+#' Association scores changed in 1.4.0, so files from before it are recomputed
+#' rather than used (see [cacheAs]). Where a file is known to hold what this
+#' version would compute - because it was written by a development version that
+#' already had the corrections, for instance - this records that, and the file is
+#' used again as it is.
+#'
+#' What actually wrote a file is left as it stands; the blessing is recorded
+#' beside it, so that [cacheAsInfo()] keeps telling the truth about where the
+#' numbers come from.
+#'
+#' A file that records no parameters, as those written before 1.3.0.9000 do, has
+#' nothing left to be compared against a call once it is blessed, and is
+#' therefore reused for any call that names it. Bless such a file only if that
+#' is what you mean.
+#'
+#' @param cacheAs paths of the files to vouch for, with or without their `.rds`
+#'   extension
+#' @return the paths, invisibly
+#'
+#' @examples
+#' \dontrun{
+#' blessCacheAs("klima-ca.rds")
+#' blessCacheAs(list.files("data", pattern = "\\.rds$", full.names = TRUE))
+#' }
+#'
+#' @family cacheAs
+#' @export
+blessCacheAs <- function(cacheAs) {
+  for (file in cacheAs) {
+    file <- cacheAsFileName(file)
+    if (!file.exists(file)) {
+      stop(sprintf("Cache file '%s' does not exist.", file), call. = FALSE)
+    }
+
+    content <- readRDS(file)
+    record <- attr(content, cacheAsAttribute)
+    if (is.null(record)) {
+      record <- list()
+      message(sprintf(
+        paste0(
+          "'%s' records no parameters, so it will be reused for any call ",
+          "naming it."
+        ),
+        file
+      ))
+    }
+    record$scoresConfirmedFor <- cacheAsScoreVersion
+    record$blessedAt <- Sys.time()
+    attr(content, cacheAsAttribute) <- record
+    saveRDS(content, file)
+  }
+  invisible(cacheAs)
+}
+
 #' What produced a cacheAs file
 #'
 #' Reads back what a query function recorded in a [cacheAs] file: the parameters