Classify a functional strategy's reproducibility tier
Source:R/strategy-preflight.R
ledgr_strategy_preflight.Rdledgr_strategy_preflight() statically inspects a function(ctx, params)
strategy before execution. It classifies the strategy into ledgr's
reproducibility tiers:
Value
A ledgr_strategy_preflight object with fields tier, allowed,
reason, unresolved_symbols, package_dependencies,
qualified_package_dependencies, attached_package_dependencies,
worker_dependencies, ambient_rng_symbols, and notes.
Details
tier_1: self-contained strategy logic using only explicitparams, base/recommended R references, and ledgr's exported public namespace.tier_2: inspectable strategy logic that also uses package-qualified calls outside the active R distribution, such aspkg::fn(), or resolved immutable non-function closure objects that ledgr does not store as standalone replayable artifacts.tier_3: strategy logic with unresolved free symbols, user helpers, forbidden nondeterministic calls, or global assignment that ledgr cannot recover from stored run metadata.
The preflight is static analysis, not proof of semantic reproducibility. Dynamic dispatch, mutable captured environments, and dynamically constructed code remain user responsibilities.
Tier 3 strategies fail before execution with condition classes
ledgr_strategy_tier3 and ledgr_strategy_preflight_error; there is no
force = TRUE override on ledgr_run() or ledgr_sweep(). Covered
forbidden calls include direct wall-clock/process-environment calls such as
Sys.time(), Sys.Date(), and Sys.getenv(), visible indirection such as
do.call("Sys.time", list()), dynamic lookup/evaluation helpers such as
get(), eval(), and assign(), global assignment with <<-, and visible
context mutation such as attr(ctx, "secret") <- 1.
Strategies read the pulse context only through its public fields, its
public functions and exported ledgr helpers such as
ledgr_history_frame(). The preflight refuses as Tier 3 common access to
private context internals: extracting a private context field from any
object (ctx$.history, x[[".notes"]]); naming a field that holds
prepared data beyond the pulse anywhere, also inside quote(); naming a
non-exported ledgr object (ledgr:::name, or the bare name other than a
member or a name the strategy binds itself); calling, assigning through
(base::body(f) <-), or putting in a functional's function slot an
environment-reaching function (environment(), parent.frame(),
asNamespace(), getFromNamespace(), get0(), mget(), body<-,
formals<-, as.function(), unserialize() and their rlang
equivalents, also attached, as lapply(x, "environment") or as
baseenv()$environment(...)); and substitute() or bquote() with an
environment to evaluate in. A field, column or label called environment,
a strategy's own config_hash and a dot-prefixed name such as a model's
$.fitted are ordinary data.
The check is static and best-effort, not a sandbox: R offers no private
memory. A strategy that deliberately subverts evaluation by a route the
check does not recognise (a function reached through an alias,
parentheses or getElement(), another package's replacement function,
trace(), names built at run time, iteration over the context, objects
from outside its body) is outside ledgr's guarantee, as is one that reads
a future-data file. ledgr guarantees that its own history, feature and
fact APIs return only data known at the decision time.
Ambient strategy RNG calls such as runif(1) are Tier 2 under the execution
seed contract for ordinary sequential runs, but they are not certified for
resume or parallel equivalence. Strategies that need pulse-specific
stochastic inputs in those paths should derive them from ctx$pulse_seed
without reading ambient .Random.seed. Custom indicator generation has a
stricter deterministic feature contract; do not infer indicator RNG policy
from strategy preflight.
Articles
Reproducibility model:
vignette("reproducibility", package = "ledgr")
system.file("doc", "reproducibility.html", package = "ledgr")