Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
3d2549c
Add #39 formula-evaluator implementation plan
DougManuel Jul 16, 2026
9dba1c0
Add formula variable type to the mock_spec layer (#39)
DougManuel Jul 16, 2026
0d91853
Defer formula parse errors to validate_mock_spec (#39)
DougManuel Jul 16, 2026
7e5a8f4
Add evaluate_mock_formulas() with sandboxed post-baseline evaluation …
DougManuel Jul 16, 2026
612810c
Strict-validate specs at evaluator entry and mirror the formula skip …
DougManuel Jul 16, 2026
b2ff0e0
Wire mockFormula through the recodeflow adapter and orchestrator (#39)
DougManuel Jul 16, 2026
af1fb0f
Scope the D6 mockFormula carve-out to the requested databaseStart (#39)
DougManuel Jul 16, 2026
a969417
Document formula-derived variables and refresh deferred-work claims (…
DougManuel Jul 17, 2026
779463d
Add defensive warnings for factor-to-numeric coercion and conflicting…
DougManuel Jul 17, 2026
58bdfc7
Reconcile plan and ADR bookkeeping with the shipped #38/#39 work
DougManuel Sep 18, 2026
43db46b
Add R CMD check workflow and capture simstudy in the lockfile
DougManuel Sep 18, 2026
3d63ba9
Merge branch 'v05-ci-lockfile' into v05-formula-evaluator
DougManuel Sep 18, 2026
99d1fd2
Add the formula-variable exports to the pkgdown reference index (#39)
DougManuel Sep 18, 2026
ffff4cc
Return the native zero-row schema from the simstudy backend for n = 0…
DougManuel Sep 18, 2026
f1164f2
Merge branch 'v05-ci-lockfile' into v05-formula-evaluator
DougManuel Sep 18, 2026
01e5607
Expand range-notation missing codes on the v0.4 path (#58)
DougManuel Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@
^vignettes/\.quarto$
^vignettes/.*_files$
^vignettes/.*\.html$
^\.superpowers$
53 changes: 53 additions & 0 deletions .github/workflows/R-CMD-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# R CMD check on every push to main/dev and every PR into them.
# Complements pkgdown.yaml, which only builds the site. Dependencies come
# from DESCRIPTION via setup-r-dependencies (Suggests included, so the
# optional simstudy backend is exercised here even when it is absent from
# a developer's local library). See .claude/AI.md for the CI lessons that
# shaped this file.
on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
workflow_dispatch:

name: R-CMD-check

permissions:
contents: read

jobs:
R-CMD-check:
runs-on: ubuntu-latest
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8

steps:
- uses: actions/checkout@v4

- uses: r-lib/actions/setup-pandoc@v2

- name: Install Quarto
uses: quarto-dev/quarto-actions/setup@v2

- uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true

- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::rcmdcheck, any::roxygen2
needs: check

- name: Generate documentation
run: Rscript -e "roxygen2::roxygenize()"

- uses: r-lib/actions/check-r-package@v2
with:
upload-snapshots: true
error-on: '"warning"'
build_args: 'c("--no-manual")'
args: 'c("--no-manual", "--as-cran")'
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ Generate_mock_data_files/
vignettes/.quarto/
vignettes/*_files/
vignettes/*.html

# Superpowers plugin scratch (per-developer)
.superpowers/
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export(create_date_var)
export(create_mock_data)
export(create_survival_dates)
export(create_wide_survival_data)
export(evaluate_mock_formulas)
export(extract_distribution_params)
export(extract_proportions)
export(generate_garbage_values)
Expand All @@ -31,10 +32,12 @@ export(make_garbage)
export(mock_categorical)
export(mock_continuous)
export(mock_date)
export(mock_formula)
export(mock_spec)
export(mock_spec_categorical)
export(mock_spec_continuous)
export(mock_spec_date)
export(mock_spec_formula)
export(mock_spec_from_recodeflow)
export(parse_range_notation)
export(parse_variable_start)
Expand Down
28 changes: 28 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# MockData (development version)

## Formula-derived variables (#39, Phase A)

- Variables can now be generated from algebraic expressions over other
generated variables via a new `mockFormula` column in `variable_details`
(e.g. `weight / (height^2)`), or the new `mock_formula()` direct API.
Formulas are evaluated in dependency order by `evaluate_mock_formulas()`,
in a restricted environment exposing only the generated columns and a
fixed allow-list of base functions. `DerivedVar::`/`Func::` semantics are
unchanged: derived variables without a `mockFormula` remain excluded from
generation, and `Func::` dispatch is not yet supported.

## Reproducibility (breaking change)

- Seeded output changed once for the RNG-stream mechanism in this release.
Expand Down Expand Up @@ -33,6 +44,23 @@
their generation. Native exponential values are truncated to the declared
`range` by inverse-CDF sampling, rather than clipped at the range maximum
(with a point mass at the boundary) as the legacy `rexp()` path does.
- The simstudy backend now returns a typed zero-row data frame for `n = 0`,
identical to the native backend's output, instead of failing inside
`simstudy::genData()` (whose `1:n` id table has two rows when `n = 0`).
(#50)
- The v0.4 pipeline now expands range-notation missing codes such as
`[997,999]` (the usual CCHS and CHMS pattern for don't know, refusal and
not stated) into their individual codes, splitting the row's proportion
equally. Previously a continuous variable with such a code failed in
post-processing, and a categorical variable silently wrote the literal
string `"[997,999]"` into the data. A bracketed missing code that is not
an integer range now fails with a message naming the variable. (#58)
- The packaged minimal example corrects two metadata errors: BMI's low
garbage range had a stray parenthesis (`[-10;15])`), and height's
low-garbage proportion was 1.00 (every row) instead of 0.01. With these
and #58, every variable in the example except its survival dates now
generates through the v0.4 pipeline. Seeded output for the example
changes.
- With `validate = TRUE` (the default), invalid distribution parameters in
metadata — e.g. `distribution = "exponential"` without a positive `rate` —
now stop generation with a message naming the variable and how to fix it,
Expand Down
19 changes: 15 additions & 4 deletions R/create_mock_data.R
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
#' @noRd
.create_mock_data_v04_unsupported_variables <- function(spec) {
unsupported <- vapply(spec$variables, function(variable) {
# type = "formula" is supported end-to-end (#39, Phase A): baseline
# generation skips it, evaluate_mock_formulas() computes it, and
# postprocess_mock_data() applies its missing codes/garbage like any
# other variable. A stray non-empty `formula` field on a NON-formula
# variable is defensive fallback bait, not a supported feature.
if (variable$type == "formula") {
return(FALSE)
}

formula <- variable$formula
has_formula <- !is.null(formula) &&
!(is.character(formula) && length(formula) == 1 && (is.na(formula) || trimws(formula) == ""))
Expand Down Expand Up @@ -96,10 +105,12 @@
}

baseline <- generate_mock_data_native(spec, n = n, seed = seed)
# Baseline and post-processing use distinct L'Ecuyer-CMRG sub-streams derived
# from the single public seed (see .with_mock_seed / ADR v05-seed-contract),
# so both stages pass the same seed and select their own stage internally.
postprocess_mock_data(baseline, spec, seed = seed)
staged <- evaluate_mock_formulas(baseline, spec, seed = seed)
# Baseline generation, formula evaluation, and post-processing use distinct
# L'Ecuyer-CMRG sub-streams derived from the single public seed (see
# .with_mock_seed / ADR v05-seed-contract), so all three stages pass the
# same seed and select their own stage internally.
postprocess_mock_data(staged, spec, seed = seed)
}

#' Create mock data from configuration files
Expand Down
146 changes: 146 additions & 0 deletions R/mock_spec.R
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,73 @@ mock_date <- function(name,
)
}

#' Create a direct formula-derived mock-data specification
#'
#' `mock_formula()` is the simple direct API for formula-derived variables. It
#' returns a validated `mock_spec`; it does not generate data. Formula
#' variables have no `range`/`levels`/`distribution` of their own — their
#' values come from evaluating `formula` over other variables in the same
#' specification once those have been generated; see
#' [evaluate_mock_formulas()].
#'
#' @details
#' Use `mock_formula()` when specifying one formula variable directly in R
#' code. Use [mock_spec_formula()] with [mock_spec()] when composing several
#' variables or when writing an adapter from another metadata source.
#'
#' @param name Variable name.
#' @param formula Character scalar. An algebraic expression over other
#' variable names, e.g. `"weight / (height^2)"`.
#' @param rtype R output type for the computed column. Defaults to `"double"`.
#' @param missing_codes Explicit missing-code values.
#' @param missing_proportions Missing-code probabilities aligned to
#' `missing_codes`.
#' @param garbage_rules List of intentional invalid-value rules.
#' @param provenance Optional provenance metadata. Defaults to the direct API.
#' @param model_hint Backend hint.
#' @param spec_version Character version of the specification shape.
#'
#' @return A validated `mock_spec` object containing one formula variable.
#' @family direct specification APIs
#' @seealso [mock_spec()], [mock_spec_formula()], [evaluate_mock_formulas()]
#'
#' @examples
#' bmi_spec <- mock_spec(
#' mock_spec_continuous("height", range = c(1.4, 2.1)),
#' mock_spec_continuous("weight", range = c(45, 150)),
#' mock_spec_formula("bmi", formula = "weight / (height^2)")
#' )
#' validate_mock_spec(bmi_spec)
#'
#' @export
mock_formula <- function(name,
formula,
rtype = "double",
missing_codes = numeric(0),
missing_proportions = numeric(0),
garbage_rules = list(),
provenance = NULL,
model_hint = "auto",
spec_version = .mock_spec_version) {
provenance <- .direct_api_provenance("mock_formula", provenance)

mock_spec(
mock_spec_formula(
name = name,
formula = formula,
rtype = rtype,
missing_codes = missing_codes,
missing_proportions = missing_proportions,
garbage_rules = garbage_rules,
provenance = provenance,
model_hint = model_hint
),
spec_version = spec_version,
provenance = provenance,
model_hint = model_hint
)
}

#' Create a continuous variable specification
#'
#' @param name Variable name.
Expand Down Expand Up @@ -627,6 +694,55 @@ mock_spec_date <- function(name,
)
}

#' Create a formula-derived variable specification
#'
#' `mock_spec_formula()` describes a variable computed from other generated
#' variables by evaluating an algebraic expression (a `mockFormula`), rather
#' than sampled from a distribution. Evaluation happens in a restricted
#' environment exposing only the generated columns and a fixed allow-list of
#' base functions; see [evaluate_mock_formulas()].
#'
#' @param name Variable name.
#' @param formula Character scalar. An algebraic expression over other
#' variable names, e.g. `"weight / (height^2)"`.
#' @param rtype R output type for the computed column. Defaults to `"double"`.
#' @param missing_codes,missing_proportions,garbage_rules,provenance,model_hint
#' As for other variable specifications; applied by post-processing.
#'
#' @return A `mock_spec_variable` object of type `"formula"`.
#' @family mock specification APIs
#' @seealso [mock_formula()], [evaluate_mock_formulas()]
#' @export
mock_spec_formula <- function(name,
formula,
rtype = "double",
missing_codes = numeric(0),
missing_proportions = numeric(0),
garbage_rules = list(),
provenance = "direct",
model_hint = "auto") {
variable <- .new_mock_spec_variable(
name = name,
type = "formula",
rtype = rtype,
formula = formula,
missing_codes = missing_codes,
missing_proportions = missing_proportions,
garbage_rules = garbage_rules,
provenance = provenance,
model_hint = model_hint
)
# Parse failures are reported by validate_mock_spec(), not the constructor,
# matching the sibling constructors' defer-all-validation contract.
# depends_on is computed once here at construction time; it is not
# re-synced if `formula` is mutated afterwards.
variable$depends_on <- tryCatch(
.formula_dependencies(variable),
error = function(e) character(0)
)
variable
}

#' Check whether an object is a MockData specification
#'
#' @param x Object to check.
Expand Down Expand Up @@ -851,6 +967,27 @@ print.mock_spec_validation_result <- function(x, ...) {
}
} else if (variable$type == "date") {
errors <- c(errors, .validate_range(variable$range, variable$name, "Date"))
} else if (variable$type == "formula") {
f <- variable$formula
if (is.null(f) || !is.character(f) || length(f) != 1 || is.na(f) ||
trimws(f) == "") {
errors <- c(errors, paste0(
"Variable '", variable$name,
"' formula type requires a non-empty character formula."
))
} else {
parse_error <- tryCatch({ str2lang(f); NULL }, error = function(e) conditionMessage(e))
if (!is.null(parse_error)) {
errors <- c(errors, paste0(
"Variable '", variable$name, "' formula could not be parsed: ", parse_error
))
}
}
if (!variable$rtype %in% c("double", "numeric", "integer", "factor", "character", "logical")) {
errors <- c(errors, paste0(
"Variable '", variable$name, "' has unsupported formula rType '", variable$rtype, "'."
))
}
} else {
errors <- c(errors, paste0("Variable '", variable$name, "' has unsupported type '", variable$type, "'."))
}
Expand Down Expand Up @@ -908,6 +1045,15 @@ validate_mock_spec <- function(spec, n = NULL, strict = TRUE) {
for (variable in spec$variables) {
errors <- c(errors, .validate_mock_spec_variable(variable))
}

formula_error <- tryCatch({
.validate_formula_referents(spec)
.order_formula_variables(spec)
NULL
}, error = function(e) conditionMessage(e))
if (!is.null(formula_error)) {
errors <- c(errors, formula_error)
}
}
}

Expand Down
Loading
Loading