# Use development build when interactive *and* explicitly enabled via env var.
dev_mode <- (Sys.getenv("DEV_VIGNETTES", "false") == "true")
if (dev_mode && requireNamespace("pkgload", quietly = TRUE)) {
cli::cli_inform("loading with {.pkg pkgload}")
pkgload::load_all()
} else {
# fall back to the installed package (the path CRAN, CI, and pkgdown take)
cli::cli_inform("loading with {.pkg library}")
library(stamp)
}## loading with library
This vignette shows how stamp captures lineage (parents → children), how to detect staleness, and how to plan & rebuild downstream artifacts in level order.
You’ll use:
- Lineage:
st_children(),st_lineage() - Staleness:
st_is_stale() - Planning:
st_plan_rebuild()(returns a plan data.frame) - Rebuilding:
st_register_builder(),st_clear_builders(),st_rebuild()
Strict vs. Propagate Strict checks actual mismatch vs the parents’ current latest versions. Propagate simulates change pushing downstream (A changes ⇒ schedule B ⇒ schedule C, etc.).
A few implementation notes that clarify behavior used by the examples below:
- Committed parents (the
parents.jsonfile inside a version snapshot) are the authoritative record for lineage when present. They are captured at commit time and are used for reproducible, recursive lineage walking. - Sidecar parents (the
parentsfield in the sidecar metadata written next to the artifact file) are a lightweight convenience that let you inspect first-level lineage before a snapshot is committed.st_lineage()falls back to sidecar parents only for the immediate parent level when no snapshot is available; recursive walking beyond level 1 requires snapshot-backed parents.
This design keeps interactive workflows fast (sidecars are quick) while preserving reproducible history once snapshots are written.
Setup a tiny graph A → B(A) → C(B)
st_opts_reset()
st_opts(
versioning = "content",
code_hash = TRUE,
store_file_hash = TRUE,
verify_on_load = TRUE,
meta_format = "both"
)## ✔ stamp options updated
## versioning = "content", code_hash = "TRUE", store_file_hash = "TRUE",
## verify_on_load = "TRUE", meta_format = "both"
## ✔ stamp initialized
## alias: default
## root: /tmp/RtmpJfWT1Z
## state: /tmp/RtmpJfWT1Z/.stamp
# A
pA <- fs::path(root, "A.qs")
xA <- data.frame(a = 1:3)
st_save(xA, pA, code = function(z) z)## ✔ Saved [qs2] → /tmp/RtmpJfWT1Z/A.qs @ version
## 81a30bf8d52a5a63
# B depends on A
pB <- fs::path(root, "B.qs")
xB <- transform(xA, b = a * 2)
st_save(
xB,
pB,
code = function(z) z,
parents = list(list(path = pA, version_id = st_latest(pA)))
)## ✔ Saved [qs2] → /tmp/RtmpJfWT1Z/B.qs @ version
## 4c3715eb35e53f20
# C depends on B
pC <- fs::path(root, "C.qs")
xC <- transform(xB, c = b + 1L)
st_save(
xC,
pC,
code = function(z) z,
parents = list(list(path = pB, version_id = st_latest(pB)))
)## ✔ Saved [qs2] → /tmp/RtmpJfWT1Z/C.qs @ version
## 0588277e72afc54e
Note: after these saves each artifact has a sidecar (in
stmeta/) and snapshots under
.stamp/versions/.
Inspect lineage
# Immediate children of A (depth 1)
st_children(pA, depth = 1)## child_path child_version parent_path parent_version
## 1 /tmp/RtmpJfWT1Z/B.qs 4c3715eb35e53f20 /tmp/RtmpJfWT1Z/A.qs 81a30bf8d52a5a63
## level
## 1 1
# Full lineage (parents of an artifact)
st_lineage(pC, depth = Inf)## level child_path child_version parent_path
## 1 1 /tmp/RtmpJfWT1Z/C.qs 0588277e72afc54e /tmp/RtmpJfWT1Z/B.qs
## 2 2 /tmp/RtmpJfWT1Z/B.qs 4c3715eb35e53f20 /tmp/RtmpJfWT1Z/A.qs
## parent_version
## 1 4c3715eb35e53f20
## 2 81a30bf8d52a5a63
Make a change upstream & detect staleness
## ✔ Saved [qs2] → /tmp/RtmpJfWT1Z/A.qs @ version
## 448aa4ba4ba5645f
# Strict staleness
st_is_stale(pB) # TRUE (B's recorded A version is now old)## [1] TRUE
st_is_stale(pC) # FALSE (C points to B, which hasn't changed yet)## [1] FALSE
Plan rebuilds
Two strategies:
- strict: only items whose recorded parent IDs differ from parents’ current latest.
- propagate: assume targets will change and plan descendants in BFS layers.
# Strict: only B right now
plan_strict <- st_plan_rebuild(pA, depth = Inf, mode = "strict")
plan_strict## level path reason latest_version_before
## 1 1 /tmp/RtmpJfWT1Z/B.qs parent_changed 4c3715eb35e53f20
# Propagate: includes B (level 1) and C (level 2)
plan <- st_plan_rebuild(pA, depth = Inf, mode = "propagate")
plan## level path reason latest_version_before
## 1 1 /tmp/RtmpJfWT1Z/B.qs upstream_changed 4c3715eb35e53f20
Register builders and rebuild in level order
Builders are tiny functions that produce an artifact
from its parents. They receive (path, parents) and return a
list with at least x = <object>.
# Clear any previous registry
st_clear_builders()## ✔ Cleared all registered builders
# Register a builder for B: rebuild from A's committed version
st_register_builder(pB, function(path, parents) {
# parents is list(list(path=..., version_id=...))
A <- st_load_version(parents[[1]]$path, parents[[1]]$version_id)
list(
x = transform(A, b = a * 2),
code = function(z) z,
code_label = "B <- A * 2"
)
})## ✔ Registered builder for /tmp/RtmpJfWT1Z/B.qs
## (default)
# Register a builder for C: rebuild from B's committed version
st_register_builder(pC, function(path, parents) {
B <- st_load_version(parents[[1]]$path, parents[[1]]$version_id)
list(
x = transform(B, c = b + 1L),
code = function(z) z,
code_label = "C <- B + 1"
)
})## ✔ Registered builder for /tmp/RtmpJfWT1Z/C.qs
## (default)
# Dry run first (uses registered builders found by st_rebuild when rebuild_fun is NULL)
st_rebuild(plan, dry_run = TRUE)## ✔ Rebuild level 1: 1 artifact
## • /tmp/RtmpJfWT1Z/B.qs (upstream_changed)
## DRY RUN
## ✔ Rebuild summary
## dry_run 1
# Now actually rebuild (will use registered builders)
res <- st_rebuild(plan, dry_run = FALSE)## ✔ Rebuild level 1: 1 artifact
## • /tmp/RtmpJfWT1Z/B.qs (upstream_changed)
## ✔ Loaded ← /tmp/RtmpJfWT1Z/A.qs @ 448aa4ba4ba5645f [qs2]
## ✔ Saved [qs2] → /tmp/RtmpJfWT1Z/B.qs @ version b97ee110482e1933
## OK @ version b97ee110482e1933
## ✔ Rebuild summary
## built 1
res## level path reason status version_id msg
## 1 1 /tmp/RtmpJfWT1Z/B.qs upstream_changed built b97ee110482e1933
After rebuilding B, C becomes strictly stale if B changes again later. You can re-plan from B to keep propagating:
st_is_stale(pB)## [1] FALSE
st_is_stale(pC)## [1] TRUE
st_plan_rebuild(pB, depth = Inf, mode = "propagate")## level path reason latest_version_before
## 1 1 /tmp/RtmpJfWT1Z/C.qs upstream_changed 0588277e72afc54e
Inspect snapshots on disk
## /tmp/RtmpJfWT1Z/A.qs/versions
## ├── 448aa4ba4ba5645f
## │ ├── artifact
## │ ├── sidecar.json
## │ └── sidecar.qs2
## └── 81a30bf8d52a5a63
## ├── artifact
## ├── sidecar.json
## └── sidecar.qs2
Tip: st_info(path) summarizes sidecar,
catalog status, and the latest snapshot dir, plus parsed
parents.json from the committed snapshot (if present).
st_info(pC)## $sidecar
## $sidecar$path
## [1] "/tmp/RtmpJfWT1Z/C.qs"
##
## $sidecar$format
## [1] "qs2"
##
## $sidecar$created_at
## [1] "2026-01-28T15:50:07.745564Z"
##
## $sidecar$size_bytes
## [1] 266
##
## $sidecar$content_hash
## [1] "28e3e19ddcccd8c6"
##
## $sidecar$code_hash
## [1] "488e8fa49c740261"
##
## $sidecar$file_hash
## [1] "fefe26c9df86a402"
##
## $sidecar$code_label
## NULL
##
## $sidecar$parents
## path version_id
## 1 /tmp/RtmpJfWT1Z/B.qs 4c3715eb35e53f20
##
## $sidecar$attrs
## list()
##
##
## $catalog
## $catalog$latest_version_id
## [1] "0588277e72afc54e"
##
## $catalog$n_versions
## [1] 1
##
##
## $snapshot_dir
## /tmp/RtmpJfWT1Z/C.qs/versions/0588277e72afc54e
##
## $parents
## $parents[[1]]
## $parents[[1]]$path
## [1] "/tmp/RtmpJfWT1Z/B.qs"
##
## $parents[[1]]$version_id
## [1] "4c3715eb35e53f20"