20 Developer design notes

This section is for contributors who want to extend sclet without making the package harder to reason about.

20.1 Core object contract

SingleCellExperiment remains the only core container. New features should fit into the existing object rather than introducing a parallel class unless there is a very strong reason.

Internal state is collected under metadata(sce)$sclet. In practice, contributors should think in terms of a few stable buckets:

  • active: current assay, reduction, graph, and identity source
  • layers: logical expression layers mapped to underlying assays
  • states: typed analysis-state registry with active records and provenance
  • features: feature-level records such as HVG metadata
  • graphs: graph registry entries
  • analyses: higher-level outputs such as trajectory, milo, batch, supercell, and cellchat
  • commands: command log entries

20.2 How to add a new analysis function

When adding a new user-facing analysis function, the recommended pattern is:

  1. Use a canonical Run* name for the public entry point.
  2. Write structured outputs into the state layer with sclet_set_analysis(), or the relevant state helper.
  3. Record the call with sclet_log_command().
  4. If the function consumes expression matrices, make it resolve from layer first and only then to an underlying assay.
  5. Expose the result through get_*() and has_*() helpers if users will need to inspect it later.

The goal is not just to make a function run once. The goal is to make the result discoverable, inspectable, and reusable across later steps.

20.3 Implementation rules for contributors

Contributors should follow a few practical rules:

  • Do not access deep S4 internals when an accessor already exists.
  • Do not add a second storage path if the state layer can already represent the result.
  • Do not introduce a new user-facing expression argument unless you have decided whether it should be a logical layer or a physical assay.
  • When migration logic still needs to read old metadata, keep new results written to the unified state schema.
  • Keep changes surgical: one new analysis function should normally come with its state write-back, accessor path, and focused tests.
  • For derived-object workflows such as RunSuperCell(), record provenance on the returned object itself instead of inventing a separate container class.
  • When a workflow needs both a rich analysis object and a typed state record, prefer sclet_set_analysis() for the object payload and sclet_set_analysis_state() for the lightweight contract instead of hand-editing metadata(sce)$sclet.
  • If an upstream Bioconductor dependency emits known deprecation warnings that sclet cannot eliminate immediately, wrap the narrow call site with a targeted compatibility helper instead of using broad suppressWarnings().

20.4 What should be tested

For each new analysis function or state feature, tests should ideally cover:

  • the result is stored in the unified state layer
  • the corresponding accessor can read it back
  • has_*() returns the expected boolean
  • active state is updated if the function changes the current assay, reduction, graph, or identity
  • command log entries use the canonical public command name

This keeps sclet ergonomic for end users while still staying grounded in the clearer SingleCellExperiment and Bioconductor model.