---
title: "Writing a projectData adapter"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Writing a projectData adapter}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

This tutorial shows how to teach projectData to find and open a specialized
storage format. It builds a small working adapter, `tinydisk`, step by step,
then covers distribution, manifests, and the behavior you should know about
before shipping one. You should be comfortable writing R packages. You do not
need prior experience with projectData, although the
[main vignette](projectData.html) explains the collection features used here
in more detail.

# 1. Why adapters exist

Most R data is restored by reading an `.rda` or `.rds` file, and projectData
does that itself. Some data cannot be restored that way: a matrix stored in
files and read lazily, a handle to a database, a memory-mapped file. These
objects are restored with the format's own functions, and often those
functions come from a large package that most users of projectData do not
need.

An *adapter* is the piece of code that knows one such format. Three parties
are involved, each with a distinct job.

| Component | Responsibility |
|---|---|
| **Data package** | Distributes documented datasets, an optional manifest, and any backing files (under `inst/`). |
| **projectData** | Registers collections, inventories resources, resolves names and conflicts, defers access, records provenance, and caches each opened resource once. |
| **Adapter** | Understands one storage format: finds its resources, checks that they are intact, and restores them on request. |

Keeping the adapter separate means the format's dependencies stay out of the
projectData core. An adapter is a package of its own (or a script) that you
choose to install; projectData never installs one for you.

You do **not** need an adapter in these cases:

* The data are ordinary R datasets in `data/`. projectData reads them with its
  built-in `projectData::dataset` loader.
* The data are a single serialized R object. A manifest entry using the
  built-in `projectData::rds` loader describes an `.rds` file under `extdata/`
  without any adapter code.

Write an adapter when restoring the resource needs format-specific code, or
when you want projectData to discover resources of a given format
automatically.

# 2. How the adapter architecture works

## Resources and objects

A *resource* is one loadable unit, such as a directory or file. Opening a
resource produces one or more named R *objects*. Resource names, file names,
and object names are separate concepts and need not match. Catalog and
conflict resolution work on the recorded mapping from resources to objects,
and an unknown mapping stays unknown until it is inspected.

## The loader

An adapter contributes one or more *loaders*, built with `data_loader()`. Each
loader has a namespaced identifier such as `"tinydisk::tinymat"` and up to
three callbacks:

| Operation | Callback | Receives | Returns |
|---|---|---|---|
| Discovery | `discover(root, context)` | the package root and a context list | a list of descriptors, or `NULL` |
| Validation | `validate(descriptor, root, context)` | one descriptor | `TRUE`, or a character message that rejects it |
| Opening | `open(descriptor, paths, context)` | one descriptor, resolved `paths`, and context | a **named list** of the objects produced |

Only `open` may read data. Discovery and validation work from file names and
small metadata and must not restore the dataset. This is what lets projectData
build a catalog of a large collection without touching it.

## When each operation runs

```
 install data package
        |
        v
 register collection ---- discovery (only for loaders that provide it,
        |                  and only if the resource is not in a manifest)
        |                 validation (if the loader is already registered)
        v
 catalog / data_query()   metadata only, nothing opened
        |
        v
 first access: data_get(), or a bare name in the attached environment
        |
        +-- loader missing? resolve it from the manifest's AdapterPackage
        +-- validation again, against the loader about to open
        +-- open()  -> objects cached under the resource
        v
 later access: cached; no further validation or opening
```

*Discovery* answers "what resources does this package contain?" It returns
descriptors, which are plain lists with these fields:

| Field | Meaning |
|---|---|
| `resource` | Resource name, unique within the collection. |
| `source` | Path relative to the installed package root (for example `extdata/counts.tinymat`). |
| `objects` | Names of the objects the resource is expected to produce. |
| `loader` | The loader id (set for you by discovery; required in manifests). |
| `options` | A named list of strings the loader may interpret. |
| `title`, `help_topic`, `recorded_class` | Optional, recorded metadata. |
| `adapter` | Optional `list(package =, export =)` saying where to find the loader. |

*Validation* answers "is this descriptor well formed, and are the files it
needs present?" It runs when a collection is registered, if the loader is
already registered, and again before **every uncached opening attempt**
against the loader selected for that attempt. This means a collection can be
registered while an optional adapter is missing, and an adapter that arrives
later is still validated before it opens anything. A rejection marks the
resource `failed` without calling `open`; fix the problem and call
`data_retry()`.

*Opening* restores the resource when someone first asks for an object from it.
The result is cached, so later access does not repeat validation or opening.

## Manifests and discovery are alternatives

There are two ways a resource becomes known:

* **Discovery.** The adapter scans the package and reports what it finds, using
  a file-naming convention you define.
* **A manifest.** The data package author lists the resource in
  `inst/projectData/manifest.dcf` (section 5). The manifest describes the
  resource completely, so no adapter needs to discover it, and a loader
  without a `discover` callback works fine.

They can be combined. If both name the same resource, the manifest entry wins.
Details are in the advanced section.

# 3. Building a minimal adapter: tinydisk

The example adapter lives in `inst/examples/tinydisk.R`. It is used by the
package tests and is not part of the projectData API, so the vignette sources
it. The sections below explain how it is built.

## The format

A tinymat is a directory named `<name>.tinymat/` with two files: `dim.txt`
(`"nrow ncol"`) and `values.bin` (column-major doubles). An opened tinymat is
a small object holding the directory path and dimensions. It reads values
from disk only when you subset it with `[`. That makes it a miniature version
of a disk-backed matrix.

## The loader declaration

```r
projectData::data_loader(
  "tinydisk::tinymat",
  storage = "disk-backed",
  needs_work_dir = TRUE,
  discover = function(root, context) { ... },
  validate = function(descriptor, root, context) { ... },
  open = function(descriptor, paths, context) { ... }
)
```

The id has the form `package::name`, and the `projectData::` prefix is
reserved for built-in loaders. Using your package name as the prefix keeps ids
stable and avoids collisions with other adapters.

`storage = "disk-backed"` tells projectData that the returned object reads
from files lazily. The catalog and the first-access message then avoid
suggesting that data are in memory. `needs_work_dir = TRUE` asks for a
writable directory to be provided at opening, explained below.

## Discovery

```r
discover = function(root, context) {
  dirs <- list.files(file.path(root, "extdata"), pattern = "\\.tinymat$")
  lapply(sub("\\.tinymat$", "", dirs), function(nm) {
    list(resource = nm, source = paste0("extdata/", nm, ".tinymat"), objects = nm)
  })
}
```

`root` is the installed package root, where `data/` and the contents of
`inst/` sit side by side. The callback uses an explicit convention (the
`.tinymat` suffix), and it only lists names. It does not read `dim.txt`.
projectData does not assume that every file under `extdata/` is a public data
object, so a convention like this is how an adapter avoids claiming unrelated
files. Here the object is named after the resource, but that is a choice of
this adapter, not a requirement.

## Validation

```r
validate = function(descriptor, root, context) {
  src <- file.path(root, descriptor$source)
  ok <- file.exists(file.path(src, "dim.txt")) && file.exists(file.path(src, "values.bin"))
  if (ok) TRUE else "tinymat directory is missing dim.txt or values.bin"
}
```

The validator checks that the files the format requires exist, and returns
either `TRUE` or a message explaining why not. An error raised inside
`validate` also rejects the descriptor. It never opens the data, so it is
cheap enough to run before every opening attempt, and it turns a damaged
installation into a clear message instead of a failure deep inside the
format's reader.

## Opening

```r
open = function(descriptor, paths, context) {
  src <- paths$source
  if (identical(descriptor$options$work_copy, "true")) {
    dest <- file.path(paths$work_dir, basename(src))
    file.copy(src, paths$work_dir, recursive = TRUE)
    src <- dest
  }
  stats::setNames(list(tinymat_open(src)), descriptor$objects)
}
```

projectData resolves the paths for you, so `open` never builds a library
location itself:

* `paths$root` is the installed package root, and `paths$source` is the
  descriptor's `source` resolved to an absolute path (`NA` if there is none).
* `paths$work_dir` is a directory that is unique to this opening, provided
  only when the loader declared `needs_work_dir = TRUE` (otherwise `NULL`).
  Installed files are read-only, so a loader that must write or copy anything
  does it here.
* `context` carries `collection`, `package`, `version`, `library`, `root`, and
  `resource`, which is useful for messages and diagnostics.
* `descriptor$options` carries the strings the data package author supplied.
  The example uses `work_copy = "true"` to demonstrate a private copy.

`open` must return a **named** list, with one element for each object the
resource produces, so projectData can cache them together. Here, `tinymat_open()`
reads only `dim.txt` and records the path. It returns a restored R object, but
the values stay on disk until the user subsets them. Restoring an object is not
the same as materializing its data; for a disk-backed format the first is cheap
and the second is not. The catalog state `ready` therefore means only that the
loader returned.

# 4. Registering and using an adapter

The next chunks walk through the whole workflow. First, prepare a data
resource: the fixture helper installs a small package into a temporary library
(not your user library) containing one tinymat directory under `extdata/`.

```{r prepare}
library(projectData)
source(system.file("examples", "tinydisk.R", package = "projectData"))
source(system.file("examples", "fixtures.R", package = "projectData"))

lib <- fixture_library()
fixture_package(lib, "diskData", "1.0.0", lazy = FALSE, datasets = list(),
  files = list("extdata/counts.tinymat" = function(path) {
    tinymat_write(matrix(as.double(1:12), 3, 4), path)
  }))
```

Next, register the adapter. `data_loader_add()` registers one loader
explicitly, which is required here because the adapter is not in a package
that projectData could find. Registration stores the declaration only: no
callback runs.

```{r register-loader}
data_loader_add(tinydisk_loader())
data_loaders()
```

Querying the package shows its catalog without attaching anything. The `tinymat` was found by the adapter's discovery callback and its
object is still unopened.

```{r query}
data_query("diskData", lib.loc = lib)[, c("object", "loader", "storage", "state")]
```

Attaching registers the collection (inventory only, still no `open`) and binds
its names in an environment on the search path. The suffix below adds a second
name for each object, to show that aliases refer to the same resource.

```{r attach}
data_attach("diskData", lib.loc = lib, suffix = ".disk")
data_catalog()[, c("exposed_name", "object", "resource", "state")]
```

Both names point to one resource record, so the first access does the only
opening. This is when `validate` runs and then `open`:

```{r access}
counts <- data_get("counts")
dim(counts)
counts[2, 3]
```

`dim()` answered from the recorded dimensions, and `counts[2, 3]` read one
column from disk. Accessing the object by a different name, or qualifying the
collection, returns the same cached object without another opening:

```{r cache}
identical(counts, data_get("counts.disk"))
identical(counts, data_get("counts", collection = "diskData"))
data_catalog()[, c("exposed_name", "state")]
```

`data_get()` also works without attaching, which avoids adding names to the
search path (`data_get("counts", package = "diskData", lib.loc = lib)`). It
shares the same resource records, so there is still exactly one opening per
resource whichever route comes first.

Detaching removes the bindings and forgets the registration with its cached
references. It does not delete backing files, so `counts` stays usable:

```{r detach}
data_detach()
counts[1, 1]
```

Summary of what happens when:

| Step | Discovery | Validation | Opening |
|---|---|---|---|
| `data_loader_add()` | no | no | no |
| `data_query()` / `data_attach()` / `data_catalog()` | yes | if the loader is registered | no |
| First access to an object | no | yes | yes |
| Later access | no | no | no |

# 5. Distributing adapters and declaring resources

## An adapter package is not a data package

An *adapter package* contains code for one format and has no data. A *data
package* contains data and can say which adapter it needs, but contains no
loader code. Many data packages can use one adapter, and the adapter can
change without rebuilding them as long as the file format stays compatible.

## Packaging the adapter

An adapter package exports a function that returns a `data_loader()`, or a
list of them. By default projectData looks for an export named
`projectData_loaders`:

```r
# in the adapter package, e.g. tinydiskAdapter
#' @export
projectData_loaders <- function() tinydisk_loader()
```

Use a stable id with your package name as prefix (`"tinydisk::tinymat"`),
because data packages record it. Put optional heavy dependencies in `Suggests`
or `Imports` of the adapter, not of projectData or of the data packages.

Users register an adapter in one call:

```r
data_loader_adapter("tinydiskAdapter")
```

`data_loader_adapter()` loads the namespace without attaching it, calls the
export with no arguments, checks the whole declaration, and registers
everything or nothing. It never calls `discover`, `validate` or `open`. It
fails with a `projectData_loader_unavailable` error if the package or export is
missing.

## Declaring resources in a manifest

A data package author can describe resources in `inst/projectData/manifest.dcf`.
A manifest holds data only: relative paths and loader references, never code.

```
ManifestVersion: 1

Resource: counts
Loader: tinydisk::tinymat
Source: extdata/counts.tinymat
Objects: counts
Options: work_copy=true
AdapterPackage: tinydiskAdapter
AdapterExport: projectData_loaders
```

`Resource` and `Loader` are required. `Objects` is a comma-separated list of
names the resource produces, `Options` is `key=value` pairs separated by `;`,
and `Title`, `HelpTopic`, and `RecordedClass` are optional recorded metadata.
`AdapterPackage` (and optionally `AdapterExport`, default
`projectData_loaders`) tell projectData where to find the loader. If the loader
is not registered when the object is first accessed, projectData loads that
namespace (without attaching it) and registers the loader.

The same fields are described by the `descriptor` lists above, so a manifest
is a serialization of descriptors. Because it holds no code, projectData can
validate it without running anything.

You can write the file by hand, but projectData can also draft and write it.
`data_manifest_draft()` asks the **registered** adapters what they would claim
under a root, using discovery only. `data_manifest_write()` validates and
writes the descriptors, then reads the text back to confirm it reproduces
them. `data_manifest_validate()` checks an existing file.

### Authoring root: source tree vs. installed layout

`Source` paths (and what `discover()`/`validate()` callbacks see) are always
relative to the **installed** package root, where `data/` and the contents of
`inst/` sit side by side. The recommended entry point for authoring is a
package **source tree** (a directory with `DESCRIPTION`, `data/` and `inst/`
kept apart): `data_manifest_draft()`/`data_manifest_write()` build a temporary
view with the installed shape by creating directory links to `data/` and to
each top-level entry of `inst/`, hand that view to the callbacks, and remove it
afterward. Nothing is read, executed or copied to build this view.

That view requires a file system and R session that can create directory
links (symlinks or, on Windows, junctions). Some environments — including
some CRAN and CI Windows builds — do not allow this. projectData never falls
back to copying a data collection, since that copy could be large; if a link
cannot be created, the call fails with a `projectData_staging_error` naming the
entry, and nothing is written. In that situation, author from an environment
that supports links, or pass an **installed package** directory, or an
explicitly prepared **installed-layout** directory (one where `data/` and the
contents of `inst/` already sit side by side, as they would after
installation) as `root`, with `layout = "installed"` or `"standalone"`. No
staging is needed for either, because the shape callbacks expect is already
there.

The example below builds such an installed-layout directory directly, so it
runs the same way regardless of link support:

```{r authoring-source}
pkg <- tempfile("pkg-")
dir.create(file.path(pkg, "data"), recursive = TRUE)
dir.create(file.path(pkg, "extdata"), recursive = TRUE)
example_obj <- 1:3
save(example_obj, file = file.path(pkg, "data", "example.rda"))
tinymat_write(matrix(as.double(1:6), 2, 3),
              file.path(pkg, "extdata", "counts.tinymat"))

drafts <- data_manifest_draft(pkg, layout = "standalone")
vapply(drafts, function(d) d$source, "")
```

(Authoring directly against a package source tree, with `data/` and `inst/`
kept apart, looks the same except that `root` is the source directory and
`layout` defaults to `"auto"`, which detects it from `DESCRIPTION`.)

The `.rda` dataset is a standard dataset and needs no manifest entry. The
adapter found the tinymat. A draft does not know which package supplies the
loader, so add that, then write the manifest. Inside an actual package source,
the conventional location is `inst/projectData/manifest.dcf`:

```{r authoring-write}
drafts <- lapply(drafts, function(d) {
  d$title <- "Counts"
  d$options <- list(work_copy = "true")
  d$adapter <- list(package = "tinydiskAdapter")
  d
})
manifest <- file.path(pkg, "projectData", "manifest.dcf")
data_manifest_write(drafts, manifest, root = pkg, layout = "standalone")
writeLines(readLines(manifest))
```

The result records whether the loader's own `validate()` ran. Without a
registered loader, the descriptor is checked for structure only and a
`projectData_validation_skipped` message says so. Register the adapter and run
`data_manifest_validate()` before shipping. A manifest holds less than a
descriptor can, so the writer refuses rather than change anything silently:

```{r authoring-errors, error = TRUE}
data_manifest_write(list(resource = "counts", loader = "tinydisk::tinymat",
                         source = "../elsewhere", objects = "counts"),
                    file.path(pkg, "other.dcf"), root = pkg, layout = "standalone")
```

## When the adapter is not installed

Data packages can use an adapter without making it a hard dependency. If the
adapter named by a manifest is not installed, only the resources that need it
are affected. The next example installs a package with an ordinary dataset and
a tinymat whose manifest names an adapter that does not exist. The loader is
removed first so projectData has to resolve it from the manifest.

```{r missing-adapter}
data_loader_remove("tinydisk::tinymat")
fixture_package(lib, "mixedData", "1.0.0", lazy = FALSE,
  datasets = list(plain = list(x = 1:3)),
  files = list("extdata/m.tinymat" = function(path) {
    tinymat_write(matrix(1, 2, 2), path)
  }),
  manifest = c("ManifestVersion: 1", "",
               "Resource: m", "Loader: tinydisk::tinymat",
               "Source: extdata/m.tinymat", "Objects: m",
               "AdapterPackage: noSuchAdapter"))
data_attach("mixedData", lib.loc = lib)
data_get("x")
```

```{r missing-adapter-error, error = TRUE}
data_get("m")
```

```{r missing-adapter-catalog}
data_catalog()[, c("exposed_name", "origin", "state")]
data_detach()
```

The ordinary dataset opened normally and the catalog marks the other resource
`unavailable-loader`. Installing the adapter and retrying fixes it without
re-registering the collection.

# 6. Disk-backed resources and lifecycle considerations

`storage = "disk-backed"` is a statement about the *returned object*: it keeps
references to files and reads from them lazily. projectData reports a resource
as `ready` once `open` returns, and accurately says "opened, not necessarily
loaded into memory." Whether the complete matrix is ever in memory depends on
the format and on what the user does next.

Things to decide and document in your adapter:

* **Where the files live.** An object that references installed files may stop
  working if the installed package is replaced or removed. A working copy
  (`needs_work_dir = TRUE` plus copying into `paths$work_dir`) is independent of
  the installation only if every file reference the object needs was restored
  to the copy, and only while the copy lasts. By default `work_dir` is under
  the session temporary directory.
* **Lifetime.** Backing files must stay available for as long as the returned
  object. `data_detach()` forgets the registration and cached references but
  never deletes backing files, so objects you already hold keep working. That
  does not protect against the installation itself changing.
* **Read-only installs.** Never write into the installed package. Use
  `paths$work_dir`, which is unique per opening.
* **Serialization.** Use the format's own save and restore API in `open`. Do
  not store live connections or external pointers and expect them to survive
  serialization.

# 7. Advanced behavior and troubleshooting

## Resource identity and caching

Each resource has one record per collection. Aliases (suffix names), the
unqualified name, and `data_get(collection = )` all point to that record, so a
successful opening happens once and is shared. Reentrant opening of a resource
that is already being opened is rejected rather than started twice. If
opening fails, the resource is marked `failed`, nothing is cached, and
`data_retry()` allows another attempt. Resources already `ready` are never
validated or opened again. Replacing a loader with `data_loader_add(replace =
TRUE)` affects only resources opened afterwards, and removing one with
`data_loader_remove()` marks unopened resources `unavailable-loader` and leaves
ready objects alone.

## Discovery precedence

Discovery uses metadata only. For each resource name, in this order:

1. A manifest entry replaces every other candidate for that resource. A
   manifest can therefore hand a standard dataset to a specialized loader, or
   choose between competing adapters. Declaring one resource twice in a
   manifest is an error, even if the records agree.
2. A resource in the standard package data inventory (`.rda` and script
   datasets in `data/`) belongs to the built-in reader. An adapter claim on one
   is ignored with a `projectData_ignored_claim` warning naming the claimant,
   unless a manifest says otherwise. Adapters that routinely scan `extdata/`
   can make this a message or silent with
   `data_config(ignored_claims = "message")` or `"silent"`. The rule is about
   resource identity rather than file type: an adapter can still claim a
   directory resource that contains `.rda` files internally.
3. Otherwise, several adapters claiming one resource is ambiguous and fails
   with `projectData_ambiguous_discovery`, naming the claimants. The outcome
   does not depend on the order in which the loaders were registered.

Two distinct resources that expose the same object name are a separate error
(`projectData_duplicate_object`), and a manifest does not merge them. The
check is repeated on the full proposed mapping in `data_inspect()`. Identical
names in separate collections are supported.

## Registration-time and first-access checks

At registration, validation runs only for loaders that are already registered.
Registration therefore succeeds when an optional adapter is missing. At first
access, validation runs again, against whichever loader is selected to open.
A replacement loader is never covered by an earlier loader's validation.
Validators should inspect paths and small metadata only.

## Roots, layouts, and relative paths

`Source` paths in a manifest, and the paths your `discover()` and `validate()`
callbacks work with, are relative to the **installed** package root, where
`data/` and the contents of `inst/` sit side by side (`data/example.rda`,
`extdata/counts.tinymat`). A source tree is laid out differently
(`inst/extdata/...`), so authoring accepts three kinds of `root`:

* a **package source tree** (a directory with a `DESCRIPTION`), the
  recommended entry point. projectData builds a temporary view of the installed
  shape from links to `data/` and each top-level entry of `inst/`, passes it to
  your callbacks, and removes it afterwards. Your files are not modified, the
  package is not installed, and no dataset is loaded, copied, or run. Standard
  datasets are recognized by file name, so their object names stay unknown.
* an **installed package** directory, used as is.
* a **standalone resource directory**, used as is. This is the earlier
  convention of passing `inst/`. It works for `extdata/` resources but cannot
  see `data/`.

The `context` your callbacks receive describes which one applies:

| root | `package`, `version` | `library` | `root` |
|---|---|---|---|
| installed package | installed metadata | the library holding it | the package directory |
| package source tree | from `DESCRIPTION` | `NA` (not installed) | temporary installed-shaped view |
| standalone directory | `NA` (never guessed from the name) | `NA` | the directory |

`context$collection` is `NA` while authoring and in a standalone
`data_manifest_validate()`, and `context$resource` is `NA` during discovery. A
callback that stores the `root` it received during authoring must not rely on
it afterwards, because the source-tree view is temporary.

## Symlinks and filesystem limits

Authoring against a source tree needs a file system and R session that can
create symbolic links. If a link cannot be created, authoring fails with an
error that names the entry. It does not fall back to copying, because that
could duplicate a very large collection, and existing manifests are left
unchanged. The temporary view is removed as it was made (links as links)
without following or deleting their targets. If something cannot be removed a
`projectData_cleanup_warning` names it.

In a verified source tree, a top-level entry of `inst/` may itself be a
symbolic link, for example `inst/extdata -> /shared/data/project`. A source
such as `extdata/example_resource` is accepted if the final path exists, and is
always written as the package-relative path, never the physical target. This
does not apply to `data/`, installed packages, or standalone directories,
which keep strict containment, and it does not make the package portable.
Check that `R CMD build` places regular files, not links, in the tarball.

## Optional dependencies and failures

* Heavy dependencies belong in the adapter, loaded inside `open` with their
  namespace (`requireNamespace()` or `pkg::`), not attached. A missing
  dependency should produce a clear error from `open`, and then only that
  resource fails.
* `validate` should not need those dependencies. If a check needs them, return
  `TRUE` and let `open` report the problem.
* An unavailable adapter package gives a `projectData_loader_unavailable` error
  for the resources that need it; unrelated resources are unaffected.
* `data_retry()` clears a failed state for another attempt. `data_inspect()`
  inspects a resource on request to learn its true object names.

## Safety checklist

* Do not read data in `discover` or `validate`.
* Resolve locations from `paths`; never hard-code a library location.
* Declare `storage = "disk-backed"` when objects read lazily from files.
* Know whether the object references installed files or a working copy, and
  document which.
* Discover by an explicit format convention, or let manifests list resources.

# 8. A real-world example: projectData.monocle3

`projectData.monocle3` is a separately distributed adapter built on the
architecture described here. It restores Monocle 3 `cell_data_set` objects,
including those whose expression matrices are stored on disk with BPCells.
Monocle 3 and BPCells are needed only by that adapter package, and
projectData itself does not depend on either.

This is the same pattern as tinydisk: a data package carries the files, an
adapter package carries the format-specific discovery and restoration code, and
projectData handles registration, naming, caching, and provenance. Refer to the
`projectData.monocle3` repository and its user vignette for instructions on
creating and consuming CDS data packages. They are not repeated here.

* Repository: <https://github.com/blaserlab/projectData.monocle3>
* User vignette: `using-monocle3-data`, which you can open with
  `vignette("using-monocle3-data", package = "projectData.monocle3")` once the
  adapter is installed.

# 9. Further reading

* The main vignette, `vignette("projectData")`, covers collections, primary
  collections, preferences, suffix aliases, provenance, and the user-facing
  workflow.
* `?data_loader` documents the loader contract, including the callback
  signatures and the `paths` and `context` fields.
* `?data_loader_add` and `?data_loader_adapter` document registration, and
  `?data_manifest_draft` covers manifest authoring.
* `inst/examples/tinydisk.R` and `inst/examples/fixtures.R` hold the code used
  in this tutorial. The tests in `tests/testthat/` exercise the contract with
  an installed adapter package.
