Create, inspect and maintain a research scaffold

cttiR creates a scaffold from a name, research type and goal. The current implementation records unresolved scientific decisions and provides a reviewed layout adaptation, catalog evidence and transactional maintenance. It does not yet implement the complete research workflow or confer scientific approval.

Start with three inputs

path is the existing parent directory. Creation uses a portable child name. A preview does not create the child or install dependencies.

plan <- cttiR::project(
  "Example comparison", "methods", "Plan a reproducible tabular comparison",
  path = example_root, dry_run = TRUE
)
plan$spec$project[c("name", "slug", "type")]
#> $name
#> [1] "Example comparison"
#> 
#> $slug
#> [1] "example_comparison"
#> 
#> $type
#> [1] "methods"
plan$readiness
#> $level
#> [1] "scaffold_ready"
#> 
#> $materialized
#> [1] FALSE
#> 
#> $blockers
#> [1] "environment_pending"      "capability_gaps_recorded"
#> 
#> $analysis
#> $analysis$state
#> [1] "incomplete"
#> 
#> $analysis$candidate_engine
#> NULL
#> 
#> $analysis$requested_engine
#> NULL
#> 
#> $analysis$missing_fields
#> $analysis$missing_fields[[1]]
#> [1] "/analysis/outcome_family"
#> 
#> $analysis$missing_fields[[2]]
#> [1] "/analysis/unit_structure"
#> 
#> $analysis$missing_fields[[3]]
#> [1] "/analysis/mapping/data_source_id"
#> 
#> $analysis$missing_fields[[4]]
#> [1] "/analysis/mapping/estimand"
#> 
#> $analysis$missing_fields[[5]]
#> [1] "/analysis/mapping/missing_data"
#> 
#> $analysis$missing_fields[[6]]
#> [1] "/analysis/approved"
#> 
#> 
#> $analysis$capability_gaps
#> list()
#> 
#> $analysis$model
#> $analysis$model$state
#> [1] "incomplete"
#> 
#> $analysis$model$settings
#> NULL
#> 
#> $analysis$model$missing_fields
#> $analysis$model$missing_fields[[1]]
#> [1] "/analysis/model/reviewed"
#> 
#> 
#> $analysis$model$inapplicable_fields
#> list()
#> 
#> $analysis$model$capability_gaps
#> $analysis$model$capability_gaps[[1]]
#> [1] "supported_engine_not_selected"
#> 
#> 
#> $analysis$model$executable
#> [1] FALSE
#> 
#> $analysis$model$limitation
#> [1] "Recorded choices do not approve a package revision, model fit or scientific interpretation."
#> 
#> 
#> $analysis$executable
#> [1] FALSE
#> 
#> $analysis$blockers
#> $analysis$blockers[[1]]
#> [1] "adapter_and_revision_approval_pending"
#> 
#> $analysis$blockers[[2]]
#> [1] "data_checks_not_run"
#> 
#> $analysis$blockers[[3]]
#> [1] "model_settings_incomplete"
#> 
#> 
#> $analysis$limitations
#> $analysis$limitations[[1]]
#> [1] "Candidate routing is not methodological or workflow approval."
#> 
#> $analysis$limitations[[2]]
#> [1] "No dataset is opened and no formula, expression or model is evaluated."
#> 
#> $analysis$limitations[[3]]
#> [1] "Column types, levels, missingness, model assumptions and diagnostics require runtime checks."
#> 
#> 
#> 
#> $planning
#> $planning$mode
#> [1] "deterministic"
#> 
#> $planning$fallback_reason
#> NULL
#> 
#> $planning$local_runtime
#> [1] "not_checked"
#> 
#> $planning$guidance
#> [1] "Use setup(dry_run = TRUE) to inspect optional local planning prerequisites."
#> 
#> 
#> $workflow
#> $workflow$profile
#> [1] "standard_reflowR"
#> 
#> $workflow$bundle
#> [1] "standard-0.3.0"
#> 
#> $workflow$engine
#> NULL
#> 
#> $workflow$modality
#> [1] "unknown"
#> 
#> $workflow$stages
#> $workflow$stages[[1]]
#> $workflow$stages[[1]]$stage
#> [1] "project"
#> 
#> $workflow$stages[[1]]$capability
#> [1] "std.project.reflowr_layout"
#> 
#> $workflow$stages[[1]]$adapter
#> [1] "[email protected]"
#> 
#> $workflow$stages[[1]]$enabled
#> [1] TRUE
#> 
#> $workflow$stages[[1]]$status
#> [1] "approved"
#> 
#> 
#> $workflow$stages[[2]]
#> $workflow$stages[[2]]$stage
#> [1] "import"
#> 
#> $workflow$stages[[2]]$capability
#> [1] "std.import.delimited"
#> 
#> $workflow$stages[[2]]$adapter
#> [1] "[email protected]"
#> 
#> $workflow$stages[[2]]$enabled
#> [1] TRUE
#> 
#> $workflow$stages[[2]]$status
#> [1] "approved"
#> 
#> 
#> $workflow$stages[[3]]
#> $workflow$stages[[3]]$stage
#> [1] "check"
#> 
#> $workflow$stages[[3]]$capability
#> [1] "std.check.mapped"
#> 
#> $workflow$stages[[3]]$adapter
#> [1] "[email protected]"
#> 
#> $workflow$stages[[3]]$enabled
#> [1] TRUE
#> 
#> $workflow$stages[[3]]$status
#> [1] "approved"
#> 
#> 
#> $workflow$stages[[4]]
#> $workflow$stages[[4]]$stage
#> [1] "tidy"
#> 
#> $workflow$stages[[4]]$capability
#> [1] "std.tidy.dplyr"
#> 
#> $workflow$stages[[4]]$adapter
#> [1] "[email protected]"
#> 
#> $workflow$stages[[4]]$enabled
#> [1] TRUE
#> 
#> $workflow$stages[[4]]$status
#> [1] "approved"
#> 
#> 
#> $workflow$stages[[5]]
#> $workflow$stages[[5]]$stage
#> [1] "describe"
#> 
#> $workflow$stages[[5]]$capability
#> [1] "std.describe.descrtab2"
#> 
#> $workflow$stages[[5]]$adapter
#> [1] "[email protected]"
#> 
#> $workflow$stages[[5]]$enabled
#> [1] TRUE
#> 
#> $workflow$stages[[5]]$status
#> [1] "approved"
#> 
#> 
#> $workflow$stages[[6]]
#> $workflow$stages[[6]]$stage
#> [1] "figures"
#> 
#> $workflow$stages[[6]]$capability
#> [1] "std.figures.accessible"
#> 
#> $workflow$stages[[6]]$adapter
#> [1] "[email protected]"
#> 
#> $workflow$stages[[6]]$enabled
#> [1] TRUE
#> 
#> $workflow$stages[[6]]$status
#> [1] "approved"
#> 
#> 
#> $workflow$stages[[7]]
#> $workflow$stages[[7]]$stage
#> [1] "demo"
#> 
#> $workflow$stages[[7]]$capability
#> [1] "std.demo.synthetic"
#> 
#> $workflow$stages[[7]]$adapter
#> [1] "[email protected]"
#> 
#> $workflow$stages[[7]]$enabled
#> [1] TRUE
#> 
#> $workflow$stages[[7]]$status
#> [1] "approved"
#> 
#> 
#> $workflow$stages[[8]]
#> $workflow$stages[[8]]$stage
#> [1] "report"
#> 
#> $workflow$stages[[8]]$capability
#> [1] "std.report.render"
#> 
#> $workflow$stages[[8]]$adapter
#> [1] "[email protected]"
#> 
#> $workflow$stages[[8]]$enabled
#> [1] TRUE
#> 
#> $workflow$stages[[8]]$status
#> [1] "approved"
#> 
#> 
#> 
#> $workflow$gaps
#> $workflow$gaps[[1]]
#> [1] "model_requires_outcome_family_and_unit_structure"
#> 
#> 
#> 
#> $environment
#> $environment$state
#> [1] "dependencies_missing"
#> 
#> $environment$missing
#> $environment$missing[[1]]
#> [1] "DescrTab2"
#> 
#> 
#> $environment$mismatched
#> $environment$mismatched[[1]]
#> [1] "colorspace 2.1-4 != 2.1-2"
#> 
#> $environment$mismatched[[2]]
#> [1] "grDevices 4.6.2 != 4.6.1"
#> 
#> $environment$mismatched[[3]]
#> [1] "knitr 1.52 != 1.51"
#> 
#> $environment$mismatched[[4]]
#> [1] "nlme 3.1-172 != 3.1-170"
#> 
#> $environment$mismatched[[5]]
#> [1] "rmarkdown 2.32 != 2.31"
#> 
#> $environment$mismatched[[6]]
#> [1] "stats 4.6.2 != 4.6.1"
#> 
#> $environment$mismatched[[7]]
#> [1] "survival 3.8-12 != 3.8-9"
#> 
#> $environment$mismatched[[8]]
#> [1] "tools 4.6.2 != 4.6.1"
#> 
#> $environment$mismatched[[9]]
#> [1] "utils 4.6.2 != 4.6.1"
#> 
#> 
#> $environment$unpinned
#> list()
#> 
#> $environment$limitation
#> [1] "Installed versions are read from package metadata; no environment was prepared or restored."
#> 
#> 
#> $git
#> $git$state
#> [1] "not_requested"
stopifnot(!dir.exists(plan$path))

Apply the same request explicitly:

p <- cttiR::project(
  "Example comparison", "methods", "Plan a reproducible tabular comparison",
  path = example_root
)
stopifnot(dir.exists(p$path))
p$spec$analysis
#> $aim
#> [1] "explanatory"
#> 
#> $outcome_family
#> [1] "unknown"
#> 
#> $unit_structure
#> [1] "unknown"
#> 
#> $engine
#> NULL
#> 
#> $approved
#> [1] FALSE

Creation does not read study data, infer endpoints or ethics approval, call a model, initialize Git, install packages or render the report. The configuration and lock record accepted identity and provenance. The lock is not an renv.lock.

The project contains:

  • cttir-project.yml: project, research and publication configuration.
  • cttir-lock.json: accepted catalog, resource and template pins.
  • metadata/: dataset registry, variable dictionary and template provenance.
  • protocol/analysis-plan.md: decisions to review before analysis.
  • analysis/: adapted reflowR pages and a labelled synthetic rendering fixture.
  • publications/: separate analysis, manuscript, figure, table and submission trees.
  • .cttir/: ownership, state and machine-local data bindings.

Raw data are not copied. Keep sensitive local paths in the ignored .cttir/local.yml; registry descriptions should be suitable for the intended repository audience.

Repeat safely and inspect explicit changes

An identical creation request preserves the accepted identity and existing edits. A different request for an occupied project name raises a conflict; use sync() to preview configuration changes.

again <- cttiR::project(
  "Example comparison", "methods", "Plan a reproducible tabular comparison",
  path = example_root
)
stopifnot(identical(again$spec$project$id, p$spec$project$id))
stopifnot(all(again$plan$action == "skip"))

options takes precedence over a config list or YAML/JSON file. Unknown fields are rejected. Updates to publication and dataset arrays are keyed by identity. Unsupported workflow or dependency migrations fail explicitly.

preview <- cttiR::sync(p$path, options = list(project = list(language = "de")))
preview$actions[preview$actions$action != "skip", c("path", "action")]
#>                         path action
#> 2          cttir-project.yml update
#> 37           cttir-lock.json update
#> 38 .cttir/managed-files.json update
#> 39         .cttir/state.json update
#> 40  .cttir/accepted-spec.yml update
stopifnot(preview$state == "planned")
applied <- cttiR::sync(
  p$path, options = list(project = list(language = "de")), dry_run = FALSE
)
applied$state
#> [1] "applied"

The language setting is metadata; it does not promise that every generated page or interface message is translated. User-owned analysis/manuscript files are preserved. Edited managed files cause a conflict. Applied changes have backups and a journal, but a multi-file update is not one atomic filesystem operation.

Record analysis mappings without running a model

Dataset identifiers link the analysis to its registry entry. Column names are literal names, including names containing spaces; they are never parsed as R expressions. Unknown decisions stay visible. This example records an intended continuous-outcome association and leaves approval unresolved:

analysis_config <- list(
  data_sources = list(list(
    id = "source01", label = "Example registry entry", logical_uri = NULL,
    format = "csv", access_class = "unknown", checksum = NULL,
    checksum_status = "unknown", schema_ref = NULL
  )),
  analysis = list(
    aim = "explanatory", outcome_family = "continuous",
    unit_structure = "independent", engine = "stats::lm", approved = FALSE,
    mapping = list(
      data_source_id = "source01", outcome = "response",
      predictors = list("exposure"), estimand = "A specified conditional association",
      missing_data = "fail"
    )
  )
)
mapped <- cttiR::project(
  "Mapping example", "methods", "Record a proposed analysis configuration",
  path = example_root, config = analysis_config, dry_run = TRUE
)
mapped$readiness$analysis[c("state", "candidate_engine", "missing_fields", "executable")]
#> $state
#> [1] "incomplete"
#> 
#> $candidate_engine
#> [1] "stats::lm"
#> 
#> $missing_fields
#> $missing_fields[[1]]
#> [1] "/analysis/approved"
#> 
#> 
#> $executable
#> [1] FALSE
stopifnot(!mapped$readiness$analysis$executable, !dir.exists(mapped$path))

Binary and survival configurations require explicit event/non-event codes; survival also requires time origin and units. Longitudinal configurations require subject/time mappings and a separate review of random effects and residual structure. Predictive, causal and other unsupported designs remain capability gaps. An explicitly empty predictor list records an intercept-only intention. complete_case records a reviewed missing-data intention; planning never removes rows. Even a fully recorded configuration and approved = TRUE cannot bypass pending adapter/revision approval, dataset checks or model diagnostics.

Model choices are recorded separately from data mappings. There are no inferred intercept, link, tie-handling or random-effects choices. reviewed = TRUE records the user’s review of those settings; it does not approve a package revision or scientific interpretation. Supported candidate settings include a fixed intercept for linear/binomial/mixed models, a logit link for binary models, explicit Cox tie handling, and a random intercept with independent homoscedastic residuals and ML or REML estimation for mixed models. Cox settings do not include an estimated intercept. More complex structures remain unsupported.

analysis_config$analysis$model <- list(intercept = TRUE, reviewed = TRUE)
model_draft <- cttiR::project(
  "Model settings example", "methods", "Record explicit model choices",
  path = example_root, config = analysis_config, dry_run = TRUE
)
model_draft$readiness$analysis$model[c("state", "missing_fields", "executable")]
#> $state
#> [1] "settings_recorded"
#> 
#> $missing_fields
#> list()
#> 
#> $executable
#> [1] FALSE
stopifnot(model_draft$readiness$analysis$model$state == "settings_recorded",
  !model_draft$readiness$analysis$executable)

The outer configuration_recorded state describes variable mapping and analysis intent; the nested model state reports settings separately. A mapping alone does not satisfy the model_settings_incomplete readiness blocker. Neither state makes a project executable while adapter, revision or data checks remain pending.

Check an explicitly supplied in-memory data frame separately. This does not read registered data locations, modify records or fit a model. Only mapped columns participate in missingness; a configured complete-case policy reports how many rows are complete without deleting them. Event codes are compared as literal text, including factor labels. Data frame subclasses, matrix/list columns and unreviewed date transformations are refused rather than coerced.

synthetic <- data.frame(response = c(2, 4, 6, 8), exposure = 1:4)
data_check <- cttiR::check_analysis_data(synthetic, mapped$spec)
data_check[c("state", "rows_total", "rows_complete", "issues", "executable")]
#> $state
#> [1] "passed"
#> 
#> $rows_total
#> [1] 4
#> 
#> $rows_complete
#> [1] 4
#> 
#> $issues
#> list()
#> 
#> $executable
#> [1] FALSE
stopifnot(data_check$state == "passed", !data_check$executable)

A passed result covers only the listed structural checks. It does not establish independence, unit correctness, rank, model assumptions, adequate sample size, causal identification or scientific approval. Survival checks currently target positive follow-up times with explicit right-censoring codes; more complex event processes require a different adapter.

Query evidence without approving a workflow

The API catalog and resource registry are separate evidence layers. A discovered resource is not automatically installed or approved. An extracted function signature is not proof of scientific suitability.

hit <- cttiR::search("reflowR::reflow_init", path = p$path)
hit[, c("package", "revision", "verification", "approved")]
#>   package                                 revision        verification approved
#> 1 reflowR cd1243a068ff2c8fb6796e34b58f6c6ce6af87e8 static_api_verified    FALSE
answer <- cttiR::ask("reflowR::reflow_init", path = p$path)
stopifnot(identical(answer$code, ""))

Literal documentation search can return bounded excerpts from stored, licensed source documents. The initial corpus includes the pinned reflowR revision. Documents are not rendered or executed during indexing. Coverage is reported separately from workflow approval:

known <- cttiR::packages(path = p$path)
known[known$package == "reflowR",
  c("package", "documents_discovered", "documents_stored", "approved")]
#>    package documents_discovered documents_stored approved
#> 45 reflowR                   33               31        0

ask(verified_only = TRUE) restricts evidence to statically resolved API records. It does not yet provide qualified natural-language planning. The package currently has zero workflow-approved revisions, so a complete research-ready workflow must not be inferred from these queries.

Refresh only sources you select

The following examples are deliberately not run by the vignette. They show explicit local and public GitHub source registration:

options(cttiR.sources = list(
  list(id = "local-tool", path = "/path/to/package-source"),
  list(id = "public-reflow", github = "CTTIR/reflowR", package = "reflowR",
       ref = "cd1243a068ff2c8fb6796e34b58f6c6ce6af87e8")
))
cttiR::update(dry_run = TRUE)
cttiR::update(sources = "public-reflow", mode = "remote", dry_run = TRUE)

To store document text, a registration must also declare a reviewed documentation_rights basis covering those documents. Without it, the updater retains inventory and hashes only. Do not assume a package license covers all third-party assets.

Remote GitHub mode resolves one commit and verifies bounded downloads against Git blob hashes. It does not install the source package. Local mode never fetches remote records. Discovery, pruning, non-GitHub backends and release migration remain pending. A failed selected source prevents activation.

Updates retain complete immutable snapshots. Existing projects keep their pins; removed exports do not reappear through fallback to an older bundled package. Use rollback_knowledge(id) to preview a retained composite snapshot and dry_run = FALSE to apply it. Initial resource observation times can make preview and applied composite IDs differ even when API content is identical.

An applied update can recover a verified stopped local catalog writer after checking the active snapshot and activation journal. It retains old writer evidence. Live or unknown writers, conflicting journals and corrupt snapshots are refused. An interrupted recovery guard requires manual review.

Audit and render intentionally

Audit is local and read-only by default. It does not open referenced datasets or run an analysis. This example checks only the project:

report <- cttiR::audit(p$path, scope = "project")
report$overall_status
#> [1] "warning"

A warning or untested readiness check is not scientific clearance. strict = TRUE raises a typed condition for required failures or required unverified checks. repair = TRUE uses a restricted repair set: it does not overwrite arbitrary user edits. Consult help("audit", package = "cttiR") before repair.

For new adapted-layout projects, run this from the generated project root only after installing the rendering dependencies explicitly and reviewing any page edits:

# In a terminal at the generated project root:
# Rscript code/render_report.R

Rendering intentionally executes reviewed R Markdown chunks. The bundled fixture uses synthetic values; it does not fit the project’s study data. The adaptation records its pinned reflowR source and MIT notice, while using rmarkdown::render_site explicitly. It does not invoke reflow_init() or workflowr and does not claim all upstream reproducibility features.

Index a local R distribution source

Base package evidence can come from an explicitly acquired, reviewed R source tree. Register the distribution root and exact package name; preview remains read-only. This never downloads, builds or installs R. The package’s original DESCRIPTION.in stays unchanged, while metadata substitutes the distribution version in memory. The distribution VERSION and COPYING hashes contribute to source identity. Documentation storage requires an explicit rights basis.

options(cttiR.sources = list(list(
  id = "reviewed-r-source", r_distribution = "/path/to/R-4.6.1",
  package = "stats", documentation_rights = "Reviewed local storage rights"
)))
cttiR::update(sources = "reviewed-r-source", dry_run = TRUE)

Newly indexed sources also retain S3 generic/class registrations and the exact statically resolved implementation signature, including non-exported methods. Search labels these as method declarations, never as public exports or installed dispatch proof. Duplicate registrations and unknown implementations remain unresolved. packages() reports S3 counts; historical snapshots without this index report missing counts rather than zero methods. Update differences report removed registrations and changed implementation evidence separately.

This indexes the package subtree, not all distribution manuals or NEWS. A local registration is not proof of canonical origin, installed numerical equivalence, complete documentation coverage or workflow approval. Development versions and ambiguous source locations are refused.

Optional local interface and runtime

With shiny and callr installed, setup_app() provides Fast/Detailed creation, and configure(path) uses the same synchronization engine. It is a local interface. Use Save or resume a draft to export a JSON file or restore saved answers. Drafts contain user text/configuration but exclude the parent directory, local bindings and accepted preview. Restoring requires a fresh preview, and Configure drafts must match the project identity. Review sensitive content before sharing; drafts are not saved automatically. The full typed questionnaire and remaining workflow views are still incomplete.

cttiR::setup_app()
cttiR::configure(p$path)
cttiR::setup(dry_run = TRUE)

Runtime setup is explicit. The live-tested installer currently targets Linux x86_64 and verifies the pinned Ollama runtime and local model. Offline setup never acquires software or pulls a model. A successful inference probe is not a qualified workflow planner. No CRAN submission is performed by these functions.