---
title: "Policies and Channels"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Policies and Channels}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
library(getaca)

.old_options <- options(getaca.cache = file.path(tempdir(), "getaca-policies"))
.old_envvars <- Sys.getenv(c("GETACA_OFFLINE", "NOT_CRAN"), unset = NA)
Sys.setenv(GETACA_OFFLINE = "true", NOT_CRAN = "true")
```

A resolution policy answers one question: which registry state does a name
resolve through? Everything downstream of that answer deals in immutable
records, so the policy is the only place where "the same call on two days"
can legitimately produce different bytes.

This article covers the four policies, what a remote channel is allowed to
change, how pinning freezes an analysis, and how the three settings layers
combine.

## The two halves the design keeps apart

A **resource record** names exact bytes and never changes:

```{r}
rec <- resource("backbone", "2026-06",
                urls = "https://host.invalid/backbone-2026-06.zip",
                sha256 = strrep("9f", 32), license = "CC-BY-4.0")
format(rec)
```

A **channel** maps the logical name `"backbone"` onto one record, and channels
move. Three places a channel can be read from, so three ways the same name can
resolve:

| Policy | Channel comes from | Same bytes forever? |
|---|---|---|
| `bundled` | the registry inside the installed package | yes, for that install |
| `current` | the author's remote registry, bundled as fallback | no, by design |
| `pinned` | a frozen local snapshot | yes, until you re-pin |
| `offline` | the bundled registry, network never touched | yes, for that install |

`bundled` and `offline` read the same channel. They differ in what happens
when the resource is absent from the cache: `bundled` downloads it, `offline`
raises `getaca_error_offline` and says how to prefetch.

## `bundled`

The default, and the one that makes a data dependency behave like a code
dependency. The registry that shipped inside the installed package is the
whole answer; nothing is fetched to decide what a name means.

```{r}
reg <- registry(
  package = "yourpkg",
  resources = list(rec)
)

res <- resolve_resource("backbone", registry = reg, policy = "bundled")
res$id
res$source
res$digest
```

`source` and `digest` are recorded in the cache entry, so a provenance report
can state which declaration chose the bytes and which policy read it. The
digest comes from the channel that answered, so under `current` it names the
remote state rather than the registry the call was handed.

Reinstalling the package is what moves this channel. That is the property
worth having: two machines running the same installed version resolve the same
record, and an analysis is reproducible from the package version alone.

## `current`

For data that release on their own schedule. The author publishes a static
registry file somewhere they control, and `getaca` consults it before
resolving:

```{r}
remote_reg <- registry(
  package = "yourpkg",
  policy  = "current",
  remote  = "https://yourpkg.invalid/getaca-registry.rds",
  resources = list(rec)
)
remote_reg
```

The remote is a file, not a service. Publishing it is a `registry_write()`
into a directory that GitHub Pages, r-universe or an institutional web server
already serves.

An unreachable remote produces a message and the bundled registry, which is
what CRAN policy requires of a package that uses Internet resources:

```{r, eval = FALSE}
resolve_resource("backbone", registry = remote_reg, policy = "current")
#> getaca: could not reach the remote registry for 'yourpkg'; using the bundled
#> registry.
```

An unreadable remote behaves the same way. A file that arrives but does not
parse as a registry, or parses but fails validation, produces a message and
the bundled fallback rather than a hard failure. A remote that declares a
different package is refused outright, since that is a misconfiguration rather
than an outage.

The remote registry is fetched once per session and cached, so a script making
a hundred calls performs one fetch. `getaca_refresh()` forgets it, which is
how you pick up a registry you published five minutes ago without restarting.

```{r}
getaca_refresh()
```

That also forgets registries discovered from installed packages, so it is what
you call after installing a new version of a declaring package mid-session.
Cached *resources* are untouched; this forgets declarations, not data.

### What a remote channel may change

Two things:

- **mirrors**, for records that already exist. A dead host is repaired by
  publishing a registry whose record for `2026-06` lists a new URL.
- **records**, by adding new ones and moving the head onto them.

One thing it may not change: the checksum attached to a version that already
exists. That is refused, and the error names the declaring package as the
party at fault rather than the publisher. The same rule holds for a pin file,
which is the branch that can be shown here without a network:

```{r}
rewritten <- file.path(tempdir(), "rewritten.rds")
saveRDS(
  list(yourpkg = registry("yourpkg", list(
    resource("backbone", "2026-06",
             urls = "https://host.invalid/backbone-2026-06.zip",
             sha256 = strrep("ab", 32), license = "CC-BY-4.0")
  ))),
  rewritten
)
options(getaca.pin_file = rewritten)
```

```{r, error = TRUE}
resolve_resource("backbone", registry = reg, policy = "pinned")
```

```{r, include = FALSE}
options(getaca.pin_file = NULL)
```

Without that rule, one edited file would be enough to change what a published
version means, and the guarantee the design exists to provide would go with
it. A publisher who genuinely recut a file gets a new version label, which is
a record users can see and choose.

### Moving the head

Publishing `2026-09` and moving the channel head onto it is one registry:

```{r}
ahead <- registry(
  package = "yourpkg",
  current = c(backbone = "2026-09"),
  resources = list(
    resource("backbone", "2026-06",
             urls = "https://host.invalid/backbone-2026-06.zip",
             sha256 = strrep("9f", 32), license = "CC-BY-4.0"),
    resource("backbone", "2026-09",
             urls = "https://host.invalid/backbone-2026-09.zip",
             sha256 = strrep("ab", 32), license = "CC-BY-4.0")
  )
)

resolve_resource("backbone", registry = ahead)$id
resolve_resource("backbone", registry = ahead, version = "2026-06")$id
```

Users on `current` follow the head on their next session. Users on `bundled`
stay where their installed package put them. Users who named a version keep
that version under every policy, which is the escape hatch an analysis needs.

### Signing a remote channel

The rules above bound what a remote registry can do. They say nothing about who
wrote it, and a registry sits on a host that can change hands. Signing answers
that, and it works because of where the key lives: the public key travels
inside the registry your package *ships*, which a user installs from CRAN,
while the remote registry comes from your own host. Someone who takes the host
does not thereby have the key.

Make a key once, and keep the secret half outside the package source tree:

```{r}
secret <- file.path(tempdir(), "yourpkg-signing.key")
public <- registry_keygen(secret)
substr(public, 1, 24)
```

Declare the public half in the registry the package ships:

```{r}
signed <- registry(
  package = "yourpkg",
  remote = "https://host.invalid/yourpkg.rds",
  keys = public,
  resources = list(
    resource("backbone", "2026-06",
             urls = "https://host.invalid/backbone-2026-06.zip",
             sha256 = strrep("9f", 32), license = "CC-BY-4.0")
  )
)
```

Then write and sign whatever you publish. Sign after writing: writing is what
stamps the publication time, and the signature binds it.

```{r}
path <- file.path(tempdir(), "yourpkg.rds")
registry_write(signed, path)
registry_sign(path, key = secret)

cat(readLines(paste0(path, ".sig"))[1:4], sep = "\n")
```

Upload the `.sig` beside the registry; getaca fetches it from the registry's
own URL with `.sig` appended. `registry_verify()` runs the same check a user's
session will, which is worth doing before publishing:

```{r}
registry_verify(path)
```

Editing the registry after signing breaks the link, which is the whole point:

```{r, error = TRUE}
moved <- signed
moved$resources[[1]]$urls <- "https://elsewhere.invalid/backbone-2026-06.zip"
saveRDS(moved, path, version = 3)
registry_verify(path)
```

Three consequences worth knowing before you declare a key:

- **It is a commitment.** Once your shipped registry names a key, an unsigned
  or unverifiable remote is refused rather than used. Users are not stranded,
  since the bundled declaration still resolves, but the remote channel stops
  working until the signature does.
- **A signature expires.** `registry_sign()` dates it 90 days out by default.
  Re-signing an unchanged registry extends it, and is the routine maintenance
  this feature asks of you. `expires = NA` opts out and leaves nothing bounding
  how long a stale declaration can be served in your name.
- **Rotating a key needs a release.** The keys getaca trusts are the ones in
  the installed package, so publish the new key beside the old one, keep
  signing with the old, release, and retire the old key once that release is
  out.

A package that declares no keys behaves exactly as it always did, and never
fetches a signature at all.

## `pinned`

A pin file records, for each named package, the registry state in force at the
moment you pinned. Under the `pinned` policy that snapshot is what resolution
reads, so an analysis keeps resolving what it was written against even after
the package is reinstalled and the remote registry has moved on.

```{r}
pins <- file.path(tempdir(), "getaca.pins.rds")
saveRDS(list(yourpkg = registry("yourpkg", list(rec))), pins)
options(getaca.pin_file = pins)
```

```{r}
moved_on <- registry(
  package = "yourpkg",
  current = c(backbone = "2026-09"),
  resources = list(
    rec,
    resource("backbone", "2026-09",
             urls = "https://host.invalid/backbone-2026-09.zip",
             sha256 = strrep("ab", 32), license = "CC-BY-4.0")
  )
)

resolve_resource("backbone", registry = moved_on)$id
resolve_resource("backbone", registry = moved_on, policy = "pinned")$id
```

The installed registry has moved to `2026-09`; the pin holds the analysis at
`2026-06`.

In practice the pin file is written rather than hand-built:

```{r, eval = FALSE}
getaca_pin(c("yourpkg", "otherpkg"))
```

That writes `getaca.pins.rds` in the working directory by default, which puts
it beside `renv.lock` in a project. Under `current`, `getaca_pin()` records
the remote state rather than the bundled one, so pinning captures what you are
actually resolving through at that moment. Point it somewhere else with the
`path` argument, and tell a session where to look with the
`getaca.pin_file` option.

A pin file is subject to the same immutability rule as a remote registry. A
snapshot that disagrees with the installed registry about what `2026-06` means
is refused rather than trusted, so a stale or edited pin cannot quietly serve
different bytes.

Pinning a package that ships no registry warns and skips it, so pinning a
project's whole dependency list does not fail on the ones that need nothing:

```{r}
getaca_pin("stats", path = file.path(tempdir(), "pins.rds"))
```

```{r, include = FALSE}
options(getaca.pin_file = NULL)
```

### Pins and `renv`

They answer adjacent questions. `renv` records which package versions an
analysis used; a `getaca` pin records which data versions those packages
resolved to. Together they pin both halves. Neither subsumes the other,
because a package version does not determine a data version once the package
is on `current`, and a data version says nothing about the code that read it.

Commit both files. Restoring is `renv::restore()` followed by
`getaca_prefetch()` on a connected machine, after which the analysis runs
offline.

## `offline`

Never reaches for the network. Cached copies and bundled declarations only.

```{r}
err <- tryCatch(getaca("backbone", registry = reg, policy = "offline"),
                getaca_error = function(e) e)
class(err)[1]
```

```{r}
cat(conditionMessage(err))
```

This is the policy a check run gets, whatever is set elsewhere, and it is
worth setting deliberately in two other situations: an air-gapped machine,
where the message is more useful than a transfer timeout, and a shared
analysis where you want a missing input to fail loudly rather than pull four
gigabytes onto someone's laptop.

## How the policy in force is decided

Four layers, most specific first:

1. the `policy` argument to `getaca()` or `resolve_resource()`

2. the `getaca.policy` option, set by `getaca_policy()`

3. the `GETACA_POLICY` environment variable

4. the declaring registry's own `policy` field, defaulting to `bundled`

Two overrides sit above all of them and force `offline`:

- `R CMD check`, detected from the check environment variables

- `GETACA_OFFLINE` set to `1`, `true` or `yes`

```{r}
getaca_policy()
```

The check clamp is released by `NOT_CRAN=true`, which is how a package's own
CI runs tests against real resources while a CRAN machine never does.

```{r, eval = FALSE}
getaca_policy("current")                                     # this session
getaca("backbone", package = "yourpkg", policy = "bundled")  # this call
Sys.setenv(GETACA_POLICY = "pinned")                         # this process
```

Setting the policy per call is the form worth reaching for in package code.
A function that must not vary its answer can pass `policy = "bundled"`
explicitly and stop caring what the user's session is set to.

## Choosing a default as an author

The registry's `policy` field is your recommendation, and users can override
it. Three questions decide it.

**Do the data release independently of your package?** If a new release
appears twice a year and your CRAN releases are annual, `current` is what
keeps users on data you consider correct without a release cycle in between.
If the data are static, `bundled` costs nothing and removes a moving part.

**Can you host a registry file reliably enough?** Reliably here is a low bar,
since an outage falls back to bundled with a message. GitHub Pages is
sufficient. If you have nowhere to put a file you control, `bundled` is the
honest choice.

**Would a user be surprised?** A package whose results shift because the data
moved needs that to be visible. `current` is right when the
declaring package surfaces the resolved version in its own output, and
questionable when it does not.

`bundled` is the default for a reason. Under it a data dependency behaves the
way a package dependency behaves: the installed version determines the answer.
Each of the others relaxes that in one direction, and the direction is the
reason to pick it.

## What is recorded

Whichever policy resolved a resource is kept in its cache entry, alongside the
digest of the registry state that supplied the record:

```{r, eval = FALSE}
getaca_info("backbone", package = "yourpkg")
#> <getaca cache entry> yourpkg/backbone@2026-06
#>   ...
#>   resolved by current registry sha256:8b31e0da54cf (published 2026-07-22)
#>   source url  https://host.invalid/backbone-2026-06.zip
```

`getaca_catalogue()` reports the same two fields as `source` and
`registry_digest` columns across every cached resource, so "which of these
came from a remote channel" is one filter rather than an audit.

## Where to go next

- `vignette("declaring")` for publishing a remote registry and moving a head

- `vignette("checks")` for the check clamp, `NOT_CRAN` and CI

- `vignette("failures")` for the conditions each policy can raise

```{r, include = FALSE}
options(.old_options)
Sys.unsetenv(names(.old_envvars)[is.na(.old_envvars)])
.restore <- .old_envvars[!is.na(.old_envvars)]
if (length(.restore)) do.call(Sys.setenv, as.list(.restore))
```
