Standard workflow: routing, mappings and reviewed adapters

Every project receives a useful workflow from the same three inputs, whether or not a CTTIR specialist package fits. This vignette shows how cttiR chooses the profile, what the generated standard workflow contains, and which explicit steps stand between a scaffold and a study-data analysis.

The preferred DescrTab2 2.1.16 backend is archived on CRAN. Choose options = list(workflow = list(table_backend = "none")) when creating a project to use the supported base descriptive table instead. To use the reviewed DescrTab2 adapter, provision the exact archive explicitly with remotes::install_version("DescrTab2", version = "2.1.16", upgrade = "never"). The generated dependency lock remains authoritative for the other package pins; an available archive does not establish ongoing upstream maintenance.

Profiles

Evidence Profile
Approved CTTIR specialist adapters cover the analysis cttir_specialist
Approved specialist stages cover part of it hybrid
No approved specialist capability fits standard_reflowR

Specialist evidence means an adapter-tested capability whose package revisions carry approvals in the pinned catalog. Infrastructure such as reflowR, renv or targets never counts as a specialist match. When a goal mentions a specialist area without an approved adapter, the standard profile is used and the gap is recorded.

p <- cttiR::project(
  "Cohort outcomes", "primary_research",
  "Describe outcomes and plan an adjusted regression for a tabular cohort",
  path = example_root
)
p$readiness$workflow$profile
#> [1] "standard_reflowR"
vapply(p$readiness$workflow$stages, function(x) paste(x$stage, x$capability, x$status), character(1))
#> [1] "project std.project.reflowr_layout approved"
#> [2] "import std.import.delimited approved"       
#> [3] "check std.check.mapped approved"            
#> [4] "tidy std.tidy.dplyr approved"               
#> [5] "describe std.describe.descrtab2 approved"   
#> [6] "figures std.figures.accessible approved"    
#> [7] "demo std.demo.synthetic approved"           
#> [8] "report std.report.render approved"
p$readiness$blockers
#> [1] "environment_pending"      "capability_gaps_recorded"

Goal keywords only suggest unset analysis fields (here the explanatory aim); the suggestion is stored as an inferred decision and never approves anything. Outcome family, unit structure, variables and estimands remain unknown until you record them.

Filter(function(d) d$origin == "inferred", p$spec$decisions)[[1]][c("field", "reason")]
#> $field
#> [1] "/analysis/aim"
#> 
#> $reason
#> [1] "Goal keywords suggest 'explanatory'. Review before analysis; this is not an approval."

What the standard bundle contains

The layout adapts the pinned MIT-licensed reflowR minimal template. Reviewed, static stage code lives in code/R/; user text and column names are read from configuration files as data and never reach formulas or code.

sort(grep("^(code|config|analysis|demo)/", p$plan$path, value = TRUE))
#>  [1] "analysis/_site.yml"          "analysis/01_read_data.Rmd"  
#>  [3] "analysis/02_eda.Rmd"         "analysis/03_report.Rmd"     
#>  [5] "analysis/index.Rmd"          "analysis/README.md"         
#>  [7] "analysis/synthetic_demo.Rmd" "code/R/cttir_figures.R"     
#>  [9] "code/R/cttir_workflow.R"     "code/render_report.R"       
#> [11] "code/run_demo.R"             "code/run_workflow.R"        
#> [13] "code/validate_project.R"     "config/analysis.yml"        
#> [15] "config/workflow.yml"         "demo/README.md"

config/workflow.yml lists the enabled stages, their capability IDs, approval status and the package versions pinned from the project’s catalog snapshot:

config <- yaml::read_yaml(file.path(p$path, "config", "workflow.yml"))
config[c("profile", "table_backend", "engine")]
#> $profile
#> [1] "standard_reflowR"
#> 
#> $table_backend
#> [1] "DescrTab2"
#> 
#> $engine
#> NULL
vapply(config$dependencies, function(x) x$package, character(1))
#>  [1] "DescrTab2"    "RColorBrewer" "broom"        "broom.mixed"  "colorspace"  
#>  [6] "dplyr"        "generics"     "ggplot2"      "grDevices"    "jsonlite"    
#> [11] "knitr"        "nlme"         "patchwork"    "readr"        "rmarkdown"   
#> [16] "stats"        "survival"     "tools"        "utils"        "viridisLite" 
#> [21] "yaml"

Recording mappings explicitly

Variables, estimand, missing-data intent and model settings are explicit configuration. sync() previews the change first.

options <- list(
  data_sources = list(list(id = "cohort", label = "Cohort extract", logical_uri = NULL,
    format = "csv", access_class = "restricted", checksum = NULL,
    checksum_status = "unknown", schema_ref = NULL)),
  analysis = list(
    outcome_family = "continuous", unit_structure = "independent",
    mapping = list(data_source_id = "cohort", outcome = "Systolic BP (mmHg)",
      predictors = list("Dose (mg)", "Arm"), estimand = "Adjusted mean difference per mg",
      missing_data = "complete_case"),
    model = list(intercept = TRUE, reviewed = TRUE)
  )
)
preview <- cttiR::sync(p$path, options = options)
preview$state
#> [1] "planned"
preview$analysis$candidate_engine
#> [1] "stats::lm"
unlist(preview$analysis$missing_fields)
#> [1] "/analysis/approved"
applied <- cttiR::sync(p$path, options = options, dry_run = FALSE)
applied$state
#> [1] "applied"

The remaining missing field is the analysis approval, which only a person can record. Recording it does not run anything.

Structural data checks

check_analysis_data() checks an explicitly supplied data frame against the mapping without changing it or fitting a model.

synthetic <- data.frame(
  `Systolic BP (mmHg)` = c(121, 135, 128, NA, 140, 118),
  `Dose (mg)` = c(1, 5, 3, 4, 8, 0.5),
  Arm = c("low", "high", "low", "high", "high", "low"),
  check.names = FALSE
)
spec <- cttiR::validate_spec(file.path(p$path, "cttir-project.yml"))
result <- cttiR::check_analysis_data(synthetic, spec)
result[c("state", "rows_total", "rows_complete")]
#> $state
#> [1] "passed"
#> 
#> $rows_total
#> [1] 6
#> 
#> $rows_complete
#> [1] 5

Synthetic demonstration

Rscript code/run_demo.R runs every stage on constructed data and compares the linear, logistic, mixed and Cox adapters with independent reference computations (normal equations, a hand-written Newton-Raphson, a dense Gaussian likelihood and direct partial-likelihood maximisation). The same code can run in memory:

env <- new.env()
for (file in c("cttir_figures.R", "cttir_workflow.R")) {
  sys.source(file.path(p$path, "code", "R", file), envir = env)
}
receipt <- env$cw_run_demo(root = p$path, figures_policy = spec$figures, write = FALSE)
receipt$status
vapply(receipt$cases, function(x) x$reference$name, character(1))

These values are adapter checks on synthetic data, not results.

Running on study data

Rscript code/run_workflow.R reads registered data only when all of these are present: complete mapping and reviewed model settings, recorded analysis approval, a local binding in .cttir/local.yml, approved stage adapters for the pinned package revisions and matching installed package versions. Otherwise it stops and lists what is missing, without reading data:

result <- processx::run(file.path(R.home("bin"), "Rscript"),
  c("--vanilla", "code/run_workflow.R"), wd = p$path, error_on_status = FALSE)
cat(result$stderr)
#> Setting r-universe rmarkdown theme...
#> Error: The study-data workflow is not ready. Missing:
#> - analysis.approved
#> - dataset.local_binding
#> - [email protected]
#> - [email protected]
#> - [email protected]
#> - [email protected]
#> - [email protected]
#> - [email protected]
#> - [email protected]
#> - [email protected]
#> Execution halted

Diagnostics are computational evidence for review; a blocked fit reports no estimates. Prediction, causal, count and specialist analyses have no reviewed adapter in this bundle and remain explicit gaps.