This vignette assumes you’ve read the Quick Start guide and covers:
SLC) for cross-project
queriesEach section is self-contained — run them independently or in order.
SLC)When a production deliverable is assembled from several independent
modeling pipelines — say, separate submodels for different geographic
regions — each pipeline has its own SLT. The Symlink
Composer (SLC) lets you query across all of them
at once: “what is the current best set?”
Think of each pipeline as an instrument in an orchestra. Each instrument practises independently and keeps its own sheet music (SLT logs). The composer (SLC) runs in ‘read only’ mode — it looks across all its instruments to discover “is every instrument ready for the final concert?”
Each region has been worked on at different paces, so their
best versions are on different dates.
base_slc <- file.path(tempdir(), "slt_tech", "composer")
# Each region gets its own output root and SLT
regions <- c("north", "central", "south")
tools <- lapply(setNames(regions, regions), function(r) {
root <- file.path(base_slc, r)
dir.create(root, recursive = TRUE, showWarnings = FALSE)
SLT$new(
root_list = setNames(list(root), r),
central_log_root = root
)
})Run each pipeline independently — different dates reflect real-world asynchronous development.
run_dates <- c(north = "2024_09_01", central = "2024_09_08", south = "2024_09_15")
for (r in regions) {
root <- file.path(base_slc, r)
ver <- next_version(root, run_dates[[r]])
tools[[r]]$make_new_version_folder(version_name = ver)
tools[[r]]$mark_best(version_name = ver, user_entry = list(comment = paste(r, "initial run")))
}best setcomposer$roundup_all("best")
#> tool_name root_name version_name dir_name
#> <char> <char> <char> <char>
#> 1: north north 2024_09_01.01 /tmp/RtmpGQGgAP/slt_tech/composer/north/best
#> 2: central central 2024_09_08.01 /tmp/RtmpGQGgAP/slt_tech/composer/central/best
#> 3: south south 2024_09_15.01 /tmp/RtmpGQGgAP/slt_tech/composer/south/best
#> dir_name_resolved
#> <char>
#> 1: /tmp/RtmpGQGgAP/slt_tech/composer/north/2024_09_01.01
#> 2: /tmp/RtmpGQGgAP/slt_tech/composer/central/2024_09_08.01
#> 3: /tmp/RtmpGQGgAP/slt_tech/composer/south/2024_09_15.01One table, every sub-model’s current best — a single
view of the full production-ready set, spanning three different run
dates.
keep or any other typeQuerying a full best or latest set is
probably most common, but any symlink type set can be
‘composed’ across all its instruments.
# Archive the north run — it's being cited in a journal submission
north_best <- composer$roundup_all("best")[tool_name == "north", version_name]
tools$north$mark_keep(
version_name = north_best,
user_entry = list(comment = "Journal submission 2024")
)
composer$roundup_all("keep")
#> tool_name root_name version_name dir_name
#> <char> <char> <char> <char>
#> 1: north north 2024_09_01.01 /tmp/RtmpGQGgAP/slt_tech/composer/north/keep_2024_09_01.01
#> dir_name_resolved
#> <char>
#> 1: /tmp/RtmpGQGgAP/slt_tech/composer/north/2024_09_01.01A single SLT instance can manage several root directories at once.
This is useful when one pipeline run produces outputs that land in
different locations — e.g. pre-model inputs and post-model results — but
you want them versioned in lockstep under the same
version_name.
base <- file.path(tempdir(), "slt_tech", "multi_root")
root_input <- file.path(base, "to_model")
root_output <- file.path(base, "modeled")
dir.create(root_input, recursive = TRUE, showWarnings = FALSE)
dir.create(root_output, recursive = TRUE, showWarnings = FALSE)
slt <- SLT$new(
root_list = list(
input = root_input,
output = root_output
),
central_log_root = base
)Two roots, one tool. Every mark or
make_new_version_folder call operates on both roots in
parallel.
v1 <- next_version(root_input, "2024_06_01")
slt$make_new_version_folder(version_name = v1)
# Populate with dummy files
invisible(file.create(file.path(root_input, v1, "prep.csv")))
invisible(file.create(file.path(root_output, v1, "results.csv")))
v2 <- next_version(root_input, "2024_06_01")
slt$make_new_version_folder(version_name = v2)
invisible(file.create(file.path(root_input, v2, "prep.csv")))
invisible(file.create(file.path(root_output, v2, "results.csv")))
slt$mark_best(version_name = v1, user_entry = list(comment = "Initial run"))print_tree(base, level = 2)
#> |-- log_symlinks_central.csv
#> |-- modeled
#> | |-- 2024_06_01.01
#> | |-- 2024_06_01.02
#> | |-- best -> /tmp/RtmpGQGgAP/slt_tech/multi_root/modeled/2024_06_01.01
#> | `-- report_key_versions.csv
#> `-- to_model
#> |-- 2024_06_01.01
#> |-- 2024_06_01.02
#> |-- best -> /tmp/RtmpGQGgAP/slt_tech/multi_root/to_model/2024_06_01.01
#> `-- report_key_versions.csvThe best symlink appears in both roots,
pointing to v1 in each. Promoting v2 demotes v1 everywhere at once.
suppressMessages(
slt$mark_best(version_name = v2, user_entry = list(comment = "Revised covariates"))
)
print_tree(base, level = 1)
#> |-- log_symlinks_central.csv
#> |-- modeled
#> `-- to_modelThe four built-in types — best, latest,
keep, remove — cover most workflows. But the
registry is extensible: call new_symlink_type() to define
your own, then use the generic $mark() and
$roundup() methods.
hold for clinical trialsIn clinical-trial data pipelines, a regulatory agency can issue a
clinical hold — an order to freeze a dataset in place
pending review. No one may delete, overwrite, or re-process that data
until the hold is lifted. This is different from keep (an
internal convenience) because a hold has legal weight and
should be immediately visible to anyone browsing the output
directory.
new_symlink_type("hold", is_singleton = FALSE)
#> Registered new symlink type: 'hold' (is_singleton = FALSE)
get_symlink_registry()[["hold"]]
#> $is_singleton
#> [1] FALSE
#>
#> $symlink_suffix_fn
#> function (version_name)
#> {
#> sprintf("_%s", version_name)
#> }
#> <bytecode: 0x58445dc55e18>
#> <environment: 0x58445b7d8a58>
#>
#> $ls_regex_fn
#> function (version_name)
#> {
#> sprintf("%s_%s %s .*%s$", symlink_type, version_name, arrow_regex(),
#> version_name)
#> }
#> <bytecode: 0x58445df61430>
#> <environment: 0x58445b7d9628>
#>
#> $extract_regex
#> [1] "^hold_"is_singleton = FALSE because multiple datasets can be
under hold simultaneously (each for a different regulatory inquiry).
best is a singleton type — only one folder
can hold it at a time.base_hold <- file.path(tempdir(), "slt_tech", "trial_outputs")
dir.create(base_hold, recursive = TRUE, showWarnings = FALSE)
slt_trial <- SLT$new(
root_list = list(trial = base_hold),
central_log_root = base_hold
)# Simulate three analysis runs
for (i in 1:3) {
ver <- next_version(base_hold, "2025_01_10")
slt_trial$make_new_version_folder(version_name = ver)
}
# Mark run .02 with a regulatory hold
slt_trial$mark("hold", version_name = "2025_01_10.02",
user_entry = list(comment = "FDA clinical hold — trial xxxx"))print_tree(base_hold, level = 1)
#> |-- 2025_01_10.01
#> |-- 2025_01_10.02
#> |-- 2025_01_10.03
#> |-- hold_2025_01_10.02 -> /tmp/RtmpGQGgAP/slt_tech/trial_outputs/2025_01_10.02
#> |-- log_symlinks_central.csv
#> `-- report_key_versions.csvThe hold_2025_01_10.02 symlink is now visible on the
filesystem — any team member who lses the directory sees it
immediately.
slt_trial$roundup("hold")
#> root_name version_name dir_name
#> <char> <char> <char>
#> 1: trial 2025_01_10.02 /tmp/RtmpGQGgAP/slt_tech/trial_outputs/hold_2025_01_10.02
#> dir_name_resolved
#> <char>
#> 1: /tmp/RtmpGQGgAP/slt_tech/trial_outputs/2025_01_10.02When the hold is lifted, demote with unmark():
slt_trial$unmark(version_name = "2025_01_10.02",
user_entry = list(comment = "Hold lifted — FDA clearance 2025-03-01"))
#> No symlink found for: 2025_01_10.02 - keep
#> No symlink found for: 2025_01_10.02 - remove
#> No symlink found for: 2025_01_10.02 - latest
#> No symlink found for: 2025_01_10.02 - bestprint_tree(base_hold, level = 1)
#> |-- 2025_01_10.01
#> |-- 2025_01_10.02
#> |-- 2025_01_10.03
#> |-- log_symlinks_central.csv
#> `-- report_key_versions.csvThe audit trail lives in the versioned folder’s log — when the hold was placed, by whom, and when it was released.
data.table::fread(
file.path(base_hold, "2025_01_10.02", "logs", "log_version_history.csv")
)[, ..show_vars]
#> log_id timestamp version_name action comment
#> <int> <char> <char> <char> <char>
#> 1: 0 2026_11_13_134942 2025_01_10.02 create log created
#> 2: 1 2026_11_13_134942 2025_01_10.02 promote_hold FDA clinical hold — trial xxxx
#> 3: 2 2026_11_13_134942 2025_01_10.02 demote_hold Hold lifted — FDA clearance 2025-03-01SLT auto-generates a report_key_versions.csv on every
mark call. Additional reports are available via
$make_reports().
base_rpt <- file.path(tempdir(), "slt_tech", "reports")
dir.create(base_rpt, recursive = TRUE, showWarnings = FALSE)
slt_rpt <- SLT$new(
root_list = list(output = base_rpt),
central_log_root = base_rpt
)for (i in 1:4) {
ver <- next_version(base_rpt, "2024_11_01")
slt_rpt$make_new_version_folder(version_name = ver)
}
suppressMessages({
slt_rpt$mark_best(version_name = "2024_11_01.03",
user_entry = list(comment = "Production run"))
slt_rpt$mark_keep(version_name = "2024_11_01.01",
user_entry = list(comment = "Baseline comparison"))
})suppressMessages(slt_rpt$make_reports())
print_tree(base_rpt, level = 1)
#> |-- 2024_11_01.01
#> |-- 2024_11_01.02
#> |-- 2024_11_01.03
#> |-- 2024_11_01.04
#> |-- best -> /tmp/RtmpGQGgAP/slt_tech/reports/2024_11_01.03
#> |-- keep_2024_11_01.01 -> /tmp/RtmpGQGgAP/slt_tech/reports/2024_11_01.01
#> |-- log_symlinks_central.csv
#> |-- report_all_logs_non_symlink.csv
#> |-- report_all_logs_symlink.csv
#> |-- report_all_logs.csv
#> `-- report_key_versions.csvCurrent state of every symlink-marked folder.
data.table::fread(file.path(base_rpt, "report_key_versions.csv"))[, ..show_vars]
#> log_id timestamp version_name action comment
#> <int> <char> <char> <char> <char>
#> 1: 1 2026_11_13_134943 2024_11_01.03 promote_best Production run
#> 2: 1 2026_11_13_134943 2024_11_01.01 promote_keep Baseline comparisonFolders that exist but have no active symlink — useful for identifying cleanup candidates.
If any versioned logs have unexpected structure (missing fields,
extra rows, schema drift), a REPORT_DISCREPANCIES.csv file
is written. A clean project produces no discrepancy file.
try(data.table::fread(file.path(base_rpt, "REPORT_DISCREPANCIES.csv")))
#> Error in data.table::fread(file.path(base_rpt, "REPORT_DISCREPANCIES.csv")) :
#> File '/tmp/RtmpGQGgAP/slt_tech/reports/REPORT_DISCREPANCIES.csv' does not exist or is non-readable. getwd()=='/tmp/RtmpOvpDLq/Rbuildfac415a535a/vmTools/vignettes'