Do not link to undocumented helpers from @noRd blocks

devtools::document() reported six links it could not resolve, all of them
pointing from one @noRd block to another: a function marked @noRd has no
topic to link to. They are plain code spans now. Links to
queryStringToLabel() stay as they are: that one is exported and does have
a topic.

Nothing catches this by itself, which is why it went unnoticed for a
week. The links never reach a manual page - no Rd file changes when they
go - so R CMD check is silent and comparing the generated files finds
nothing either. And roxygen2 reports them as messages rather than
warnings, so they scroll past. A workflow of its own now documents the
package and looks for that message, failing when it appears. Verified
both ways: it passes as things stand and fails once a link is put back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change-Id: Ifb5795ba2058c2f5a3b8d660a1d61f127ad82599
diff --git a/.github/workflows/document.yaml b/.github/workflows/document.yaml
new file mode 100644
index 0000000..6c5bddc
--- /dev/null
+++ b/.github/workflows/document.yaml
@@ -0,0 +1,63 @@
+# Rebuilds the documentation and fails on links roxygen2 cannot resolve.
+#
+# Those arise from linking to a function marked @noRd, which has no topic to
+# link to. They never reach a manual page, so R CMD check says nothing and a
+# comparison of the generated files finds nothing either - the only trace is a
+# message while documenting, which is easy to miss.
+on:
+  push:
+    branches: [main, master]
+    paths:
+      - 'R/**'
+      - 'man/**'
+      - 'NAMESPACE'
+      - 'DESCRIPTION'
+      - '.github/workflows/document.yaml'
+  pull_request:
+    branches: [main, master]
+
+name: document
+
+jobs:
+  document:
+    runs-on: ubuntu-latest
+
+    env:
+      GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - uses: r-lib/actions/setup-r@v2
+        with:
+          use-public-rspm: true
+
+      - uses: r-lib/actions/setup-r-dependencies@v2
+        with:
+          extra-packages: any::roxygen2
+
+      - name: Document, and refuse links that do not resolve
+        shell: Rscript {0}
+        run: |
+          messages <- character()
+          withCallingHandlers(
+            roxygen2::roxygenise(),
+            message = function(m) {
+              messages <<- c(messages, conditionMessage(m))
+              invokeRestart("muffleMessage")
+            }
+          )
+          cat(messages, sep = "")
+
+          # reported as a message rather than a warning, so it has to be looked for
+          unresolved <- grep("Could not resolve link", messages, value = TRUE)
+          if (length(unresolved) > 0) {
+            stop(
+              sprintf(
+                "roxygen2 could not resolve %d link%s. A function marked @noRd has no topic to link to; write it as `code` instead.",
+                length(unresolved),
+                if (length(unresolved) == 1) "" else "s"
+              ),
+              call. = FALSE
+            )
+          }
diff --git a/R/cacheAs.R b/R/cacheAs.R
index 10726d0..63204bf 100644
--- a/R/cacheAs.R
+++ b/R/cacheAs.R
@@ -270,11 +270,11 @@
 #' Read back a result stored in a cache file, if it is the one being asked for
 #'
 #' Warns and returns `NULL` where the file exists but does not match, so that
-#' the caller recomputes and [writeCacheAs()] overwrites it.
+#' the caller recomputes and `writeCacheAs()` overwrites it.
 #'
-#' @param cacheAs cache file name, already passed through [cacheAsFileName()]
+#' @param cacheAs cache file name, already passed through `cacheAsFileName()`
 #' @param kco [KorAPConnection()] object, for its `verbose` flag
-#' @param record what the call at hand computes, from [cacheAsRecord()]
+#' @param record what the call at hand computes, from `cacheAsRecord()`
 #' @param what name of the result, for the log and warning messages
 #' @return the cached result, or `NULL` if there is none to use
 #' @noRd
@@ -329,9 +329,9 @@
 
 #' Store a result in a cache file, together with what produced it
 #'
-#' @param cacheAs cache file name, already passed through [cacheAsFileName()]
+#' @param cacheAs cache file name, already passed through `cacheAsFileName()`
 #' @param kco [KorAPConnection()] object, for its `verbose` flag
-#' @param record what produced the result, from [cacheAsRecord()]
+#' @param record what produced the result, from `cacheAsRecord()`
 #' @param what name of the result, for the log message
 #' @param result the result to store
 #' @return `result`, invisibly and unchanged
diff --git a/R/misc.R b/R/misc.R
index e120ea4..2294bdf 100644
--- a/R/misc.R
+++ b/R/misc.R
@@ -185,7 +185,7 @@
 
 #' Labels for a vector of virtual corpora, always giving one
 #'
-#' Like [vcLabels()], but falling back to [queryStringToLabel()] where the
+#' Like `vcLabels()`, but falling back to [queryStringToLabel()] where the
 #' vector carries no names, for callers that label unconditionally.
 #'
 #' @param vc character vector of virtual corpus definitions