DataVault.jl
Models
DataVault.DataVault — Module
DataVault — config 駆動のパス解決・データ I/O・ledger 管理
ファイル構成:
src/
├── DataVault.jl このファイル(モジュールエントリ)
├── core/
│ ├── vault.jl Vault struct と constructor (path_formatter 含む)
│ └── paths.jl data/bin/status のディレクトリ・ファイルパス解決
├── io/
│ ├── atomic.jl NFS-safe 原子的書き込み + git_hash
│ ├── data.jl load / save! / load_bin / save_bin!
│ └── status.jl is_done / mark_done! / mark_running! / owner-stamped locks
├── reporting/
│ ├── ledger.jl build_ledger
│ └── figure.jl record_figure (meta.toml)
└── util/
├── enumerate.jl keys() — DataKey の列挙
├── snapshot.jl config_snapshot.toml の保存と差分検知
└── cleanup.jl cleanup_staleDataVault.DATAVAULT_DIR_NAME — Constant
凍結: discovery anchor のディレクトリ名。永遠に変えない
DataVault.LOG_TOML_READERS — Constant
Reader registry: logtomlversion → reader function.
新しいスキーマを追加するときは:
- 新しい struct LogTomlVN と readlogtomlvN を定義
- このレジストリに
N => _read_log_toml_vNを追加 - 古いエントリは絶対に消さない
これにより、過去どのバージョンで書かれた log.toml でも、現役の DataVault が 読み戻せることを保証する。
DataVault.LOG_TOML_VERSION — Constant
現在の writer が出すスキーマバージョン。新フォーマットを追加するたびに +1
DataVault.ArtifactBusy — Type
ArtifactBusy(name, identity)Thrown by artifact! with wait=false when another process is building the artifact. A scheduler catches it and runs a different cell instead of blocking a worker.
DataVault.AttachedStudy — Type
AttachedStudyA bundle of (vault, log.toml info, log_path) produced by open_all. Iterate over these to process every study under an outdir.
DataVault.AutoPathFormatter — Type
AutoPathFormatter(axis_formats)The path_formatter behind [datavault] float_format = "auto". Holds the per-axis float precision ParamIO.build_axis_formats derives from the sweep's whole value set, which is what makes distinct values of a swept float axis land in distinct directories.
Built by Vault; callers do not normally construct one.
DataVault.LogTomlV1 — Type
log.toml v1 が保持する全フィールド
DataVault.Vault — Type
VaultHandle for a single (study, run) pair. Wraps a ConfigSpec and resolves file paths under outdir.
Fields
config_path: absolute path to the source config TOMLspec: parsedParamIO.ConfigSpec(study, path_keys, paramsets)outdir: absolute root path under whichdata/,status/,bin/,figure/, and.datavault/liverun: named campaign / phase within the study. Defaults to"default". Use distinct names to keep multi-phase explorations separate (e.g."phase1","phase2_refined")path_formatter: function(DataKey, path_keys) -> Stringreadonly: whentrue, every write verb refuses (see below)
outdir resolution
Priority: constructor argument > ENV["DATAVAULT_OUTDIR"] > config value.
Multi-run usage
A study (= one project_name) may contain multiple runs. Each run gets its own data / status / bin / figure subtree under {outdir}/{layer}/{project}/{run}/ and its own discovery anchor at {outdir}/.datavault/{project}/{run}.log.toml.
v1 = Vault("configs/linear_response.toml"; run="phase1", outdir="out/")
v2 = Vault("configs/linear_response.toml"; run="phase2_refined", outdir="out/")Multiple parallel jobs can share the same (project, run): each job touches different DataKeys, and the log.toml upsert is idempotent and atomic.
Path naming
The config decides. [datavault] float_format = "auto" gives each swept float axis the precision its own value set needs, so distinct values get distinct directories; the default "fixed2" renders every float with %.2f, under which 0.006 and 0.008 share a directory and one result overwrites the other.
An existing run's scheme WINS over the config: log.toml records what a run was actually written with, and changing the config afterwards must not move data that is already on disk. The mismatch is reported; use a different run name to start a tree under the new scheme.
Pass path_formatter to override both:
formatter(key::DataKey, path_keys::Vector{String}) -> StringA formatter given this way cannot be reproduced from log.toml alone, so DataVault warns.
Reading a run without committing it
Construction is the only way to get a Vault, and it normally upserts the log.toml. That upsert is the discovery anchor: it also FREEZES the run's path_keys, so a tool whose contract is "count how many keys are left" has the effect "commit the schema" on a run that has not executed a key yet.
readonly=true takes the validation without the write. An existing log.toml is still checked against the spec, a run that has none is not refused for it, and save!, save_bin!, mark_done!, mark_running!, acquire_running!, touch_running!, refresh_running!, clear_running!, build_ledger, record_figure and cleanup_stale all throw rather than write, so the flag is a check and not a label.
v = Vault("config.toml"; run="phase1", outdir="out/", readonly=true)
count(k -> !is_done(v, k), ParamIO.expand(ParamIO.load("config.toml")))Path collision check
Construction warns when two DISTINCT parameter points format to ONE directory — they would overwrite each other while the ledger reports both as done. One deduplicated pass over the expanded grid: on the largest sweep in this fleet (90k keys, 900 distinct points) it adds ~150 ms warm, and ~1 s on the first call in a session, which is compilation. check_paths=false skips it; attach and open_all already pass it, being reads rather than writes.
DataVault._atomic_jld2_write — Method
_atomic_jld2_write(path, data) -> StringWrite data (a Dict) to path atomically by writing to a per-task, per-pid temporary file first and then mving it into place. Safe against concurrent writers across processes (NFS) and across tasks within one process.
Returns the SHA-256 (hex) of the bytes written, read from the temporary file after it is closed and before it is moved into place: the digest names exactly what this call published under path, whatever touches path afterwards.
DataVault._default_experiments_root — Method
_default_experiments_root(vault::Vault) -> StringHeuristic: for a vault attached to <pkg_root>/projects/<P>/configs/<run>.toml, return <pkg_root>/projects/<P>/experiments. Falls back to dirname(vault.config_path)/../experiments when the layout does not match.
DataVault._experiment_provenance_md — Method
_experiment_provenance_md(vault::Vault, run_readme::AbstractString) -> StringRender the markdown block that build_experiment_report injects into each matched EXP-NNN README's ## Generated provenance section. Relative paths are computed so the link works from the EXP directory.
DataVault._git_hash — Method
_git_hash(ref_path) -> StringReturn the short HEAD hash of the git repo containing ref_path, or "unknown" if not in a repo.
DataVault._git_observe — Method
_git_observe(ref_path) -> (; commit, object_format)The full HEAD of the git repo containing ref_path and the repo's object format ("sha1" or "sha256"), each "unknown" when git cannot say. This is an observation of the working tree at the moment it is called; it does not say which code a process had loaded.
DataVault._infer_outdir — Method
Infer outdir from a log.toml path. The frozen discovery contract puts log.toml at {outdir}/.datavault/{project}/{run}.log.toml, so three dirname pops recover the outdir. Also validates the .datavault/ parent to catch paths that don't match the contract.
DataVault._inject_provenance_block! — Method
_inject_provenance_block!(readme_path, provenance_md)Rewrite the ## Generated provenance section of an EXP-NNN README with provenance_md (pure markdown). Idempotent: the block is detected by its heading and replaced in place; surrounding narrative is untouched. Noop if the file lacks a ## Generated provenance heading.
DataVault._introspect_schema_keys — Method
_introspect_schema_keys(vault) -> (top_keys::Vector{String},
bench_keys::Vector{String})Open the first completed sample JLD2 under the run directory and collect its top-level keys plus bench subkeys (empty if absent). Returns two empty vectors when no JLD2 is present yet.
DataVault._parse_front_matter — Method
_parse_front_matter(text::AbstractString) -> (Dict{String,Any}, String)Parse the ----delimited YAML-ish front-matter block at the top of a markdown file. Returns the parsed header dict and the body (everything after the closing ---). When no front-matter is present returns (Dict(), text).
Supports:
- scalar values (
status: planning) - quoted strings (
slug: "foo-bar") - inline list values (
data_runs: [smoke, phase1])
Not a full YAML parser — deliberately minimal so DataVault does not grow a YAML.jl dep.
DataVault._read_ledger_csv — Method
Minimal CSV reader for DataVault's ledger.csv output. Handles RFC4180-style quoted fields (a field wrapped in "..." may contain commas; an inner " is written ""), matching what build_ledger/_csv_escape emit. Embedded newlines are not supported (the writer flattens them to spaces), so parsing stays line-based.
DataVault._save_config_snapshot — Method
_save_config_snapshot(vault)On first call, copy the config TOML to {outdir}/data/{project}/{run}/config_snapshot.toml. On subsequent calls, warn (without overwriting) if the live config differs.
The snapshot lives next to the run's data so each run preserves the exact config it was launched with.
DataVault._sync_experiment_narratives! — Method
_sync_experiment_narratives!(vault, experiments_root, run_readme; match_all=false)Walk every EXP-NNN-*/README.md under experiments_root. For each match (see below), rewrite its ## Generated provenance section to point at run_readme and append vault.run to the front-matter data_runs list.
"Match" policy:
match_all=false(default): only READMEs whose front-matterdata_runslist already containsvault.runare updated. This lets authors opt in explicitly — they wrotedata_runs: [smoke]in the front-matter, and DataVault fills in the provenance link after the run finishes.match_all=true: every EXP is updated. Useful for bulk initial cross-linking after a refactor.
Quiet — fails silently when experiments_root does not exist.
DataVault._update_data_runs! — Method
_update_data_runs!(readme_path, run_entry::AbstractString)Append run_entry to the data_runs: list in the README's front-matter (idempotent — no duplicate inserts). Noop when there is no front-matter or no data_runs field.
DataVault._validate_log_toml — Method
_validate_log_toml(vault) -> Union{String,Nothing}Refuse a run whose recorded path_keys differ from the spec's, and return its created_at so the writer can preserve it. nothing when the run has no log.toml yet, which is not a refusal: a run is allowed to be new.
DataVault._vcompare — Method
_vcompare(a, b) -> IntCompare two version strings loosely. Returns -1, 0, or 1. Falls back to lexicographic comparison when either string is not a valid VersionNumber.
DataVault._write_schema_toml — Method
_write_schema_toml(vault, writer, code_versions, top_keys, bench_keys) -> StringWrite schema.toml the first time it is called for this run. Subsequent calls compare writer identity (package + version + dataschemaversion) to the existing record:
- identity unchanged → no-op, return existing path
- identity changed → emit a warning and write
schema.toml.vN(N ≥ 2) next to the original, preserving the original
Schema.toml is thus append-only: the initial one is never overwritten, but evolving writers can still leave a trail.
DataVault.acquire_running! — Method
acquire_running!(vault, key, owner; stale_after=600.0) -> Symbolacquire_running! stamping owner= into the .running file, so that refresh_running! and clear_running! can tell this acquisition's lock from the one a sibling took after reclaiming it. owner is normally new_owner_token.
Returns the same :ok / :reclaimed / :busy as the two-argument form.
DataVault.acquire_running! — Method
acquire_running!(vault, key; stale_after=600.0) -> SymbolAtomically acquire the .running sentinel as the exclusive in-flight marker for (vault, key). This is the intended multi-master coordination primitive — downstream packages call it in place of maintaining a separate lock-file tree.
Returns
| Symbol | Meaning |
|---|---|
:ok | No prior .running existed; fresh file created. |
:reclaimed | Prior .running was stale (heartbeat age > stale_after); |
| replaced with a fresh file owned by this caller. | |
:busy | Another master holds a fresh .running; caller must not run |
| work for this key. |
Atomicity
Implemented via POSIX link() ("create iff not exists"). A uniquely named temp file is written, then link(tmp, path) publishes it under the canonical .running name — link returns -1 with errno=EEXIST if the target already exists. link is atomic on local filesystems and on NFSv3/v4 per man 2 link, so two concurrent acquire_running! calls on different hosts cannot both return :ok.
Stale-reclaim is best-effort (the rm() before link() is racy with other reclaimers), but the final link() call still serialises: at most one caller sees :ok / :reclaimed; the rest see :busy.
Companion API
refresh_running!— refresh heartbeat while holding the lock.mark_done!— remove.runningand write.doneon success.clear_running!— release without marking done (failure paths).cleanup_stale— background reaper for crashed masters.
Ownership
This form leaves the lock UNOWNED, and the companions above are owner-blind: after a sibling reclaims, the previous holder's refresh_running! still returns true and its clear_running! still deletes, now against the reclaimer's file. Pass a new_owner_token to the three-argument methods to close both.
DataVault.archive_figure! — Method
archive_figure!(vault::Vault, live_path::AbstractString;
generator_script::Union{Nothing,AbstractString}=nothing,
metadata::AbstractDict=Dict{String,Any}(),
subdir::AbstractString="") -> StringSnapshot live_path into the run's figure archive and append a version entry to figures.toml. live_path itself is not modified.
subdiris the relative directory under the run's figure dir (e.g."phase1"); use""for files sitting directly under<run>/.generator_scriptis recorded verbatim (typically@__FILE__).metadatais merged into the version entry (free-form).- Content dedup: if a prior archive has the same SHA-1, the new version entry points at that existing archive file instead of copying again.
Returns the absolute path of the archived file (old or new). Marks the new entry is_current = true and flips every older sibling to false.
DataVault.artifact! — Method
artifact!(build, vault, name, key; wait=true, poll=5.0, stale_after=600.0,
heartbeat_interval=60.0, timeout=Inf) -> valueArtifact name for sweep key key: loaded if some process already built it, built by calling build(akey) otherwise, and stored for every later caller — this job, the next one, another run under the same outdir.
gs = DataVault.artifact!(vault, :ground_state, key) do akey
prepare_ground_state(param(akey, "run.U"), param(akey, "run.D"))
endakey is key projected onto the artifact's depends_on (see ParamIO.artifact_key), not key: reading a parameter the artifact did not declare fails there, instead of silently sharing one artifact across that parameter's values.
Concurrency. Callers that need the same artifact at once serialise on a link() lock in its directory — the same lock and reclaim rule as acquire_running!. One builds; with wait=true the rest poll every poll seconds and then load it; with wait=false they throw ArtifactBusy at once. A builder that throws leaves nothing behind and releases the lock, and the error propagates.
Heartbeat. While build runs, a small sh child process rewrites the lock's heartbeat= every heartbeat_interval seconds for as long as this process is alive (kill -0), and a sibling reclaims the lock after stale_after seconds without one. It is a separate PROCESS on purpose: a task inside Julia does not run while a build computes without yielding — measured, a sleep-driven task on an interactive thread (julia -t 1,1) ticked 0 times in 3 s of a busy main thread. So stale_after bounds how long a crashed or walltime-killed builder blocks the next job, independently of how long a build takes.
A readonly vault loads but never builds: a miss throws.
DataVault.artifact_dir — Method
artifact_dir(vault, name, key) -> StringThe directory artifact name of key lives in: {outdir}/artifacts/{project}/{name}/{16 hex of sha256(identity)}/, holding artifact.jld2 and a human-readable inputs.toml. Shared by every run under outdir, not owned by one.
DataVault.attach — Method
attach(log_path) -> Vault
attach(outdir; project, run="default") -> VaultAttach to a previously written (study, run) and return a Vault.
The first form takes the absolute path to a log.toml file directly. The second form uses the frozen discovery contract — given an outdir, a project name, and a run name, it resolves the log.toml at {outdir}/.datavault/{project}/{run}.log.toml and attaches to it. The two forms are distinguished by whether project is supplied.
Config resolution order:
{outdir}/{layout.config_snapshot}— the frozen snapshot taken at initial construction; preferred because it is stable under moves and is colocated with the data.log.toml[study].config— the original absolute path recorded at construction; used as a fallback with a warning.- Error if neither is available.
The returned Vault is fully writable. Its constructor will idempotently upsert the log.toml, refreshing [meta].datavault_version and datavault_git_hash. created_at is preserved.
readonly=true passes through to the constructor: the log.toml is validated but not rewritten, and the returned vault refuses the write verbs. Use it for aggregates, progress counters and plotting scripts, which otherwise leave a mark on every run they read.
DataVault.bin_dir — Method
bin_dir(vault, key) -> StringDirectory holding key's checkpoints, the counterpart of data_dir under bin/.
DataVault.binding_of — Method
binding_of(status, revise_loaded, main_files_in_roots) -> (binding, reasons)The binding an observation can claim, from each root's loaded status: loaded-differs-from-disk when a loaded package's sources differ from the snapshot (a fact: the bytes it was built from are not on disk), and otherwise unverified, with the reasons. It never returns loaded-matches-disk: that every loaded package matched does not show that the code which ran was theirs, since code defined outside any package (a script, a closure, a method added to Base from Main) leaves no trace to check. A reader treats a loaded-matches-disk written by an earlier version as unverified.
DataVault.build_experiment_report — Method
build_experiment_report(vault::Vault, writer::Module;
project_root::Union{Nothing,AbstractString}=nothing,
data_schema_version::Union{Nothing,Integer}=nothing,
output_name::AbstractString="README.md") -> StringGenerate a machine-readable README.md + schema.toml pair for the run at outdir/data/<project>/<run>/, summarising identity, code versions, config, ledger progress, event timings, figures, and data schema.
writer is the caller's top-level module (e.g. ThermalEquilibrium). Package name, version, package root, and DATA_SCHEMA_VERSION (if declared as a module constant) are extracted via Julia reflection — no explicit kwargs are required in the common case.
project_rootdefaults topkgdir(writer)(used for git introspection).data_schema_versionoverrides any module constant.experiments_root— when given, the run is cross-linked into anyEXP-NNN-*/README.mdwhose front-matterdata_runslist containsvault.run(or into every EXP whennarrative_match_all=true). The target README gets its## Generated provenancesection rewritten with a link back to this run's auto-README, and thedata_runslist is updated. Idempotent — safe to call repeatedly.
The README is overwritten on every call (idempotent). The schema.toml is append-only: unchanged writers are a no-op, but a changed (package, version, dataschemaversion) triple emits a warning and spills to schema.toml.vN so history is preserved.
Returns the absolute path of the written README.
DataVault.build_experiments_index — Method
build_experiments_index(outdir, project_name) -> StringWalk outdir/data/<project_name>/ and write INDEX.md with one row per run (linking to its README.md). Columns: run, snapshot availability, completed count, parent git hash (from schema.toml if present), latest ledger activity.
Returns the absolute path of INDEX.md.
DataVault.build_ledger — Method
build_ledger(vault) -> StringScan all .done files and write ledger.csv under the project data directory. Returns the path to the written file.
DataVault.build_master_ledger — Method
build_master_ledger(outdir::AbstractString) -> Vector{Dict{String,String}}Aggregate every ledger under outdir into a single flat sequence of row dicts. Each row has its original ledger columns plus the following meta columns prepended conceptually (the dict is unordered):
project_namerundatavault_versionlog_toml(outdir-relative path to the originating log.toml)
Studies whose ledger.csv does not yet exist contribute zero rows but are still attached (their metadata appears via open_all if you want both).
For cross-outdir aggregation (e.g. scanning a whole Vault tree), call this per outdir and concatenate — DataVault itself is intentionally unaware of any higher-level layout like Vault's apps/lib/dev layers.
DataVault.build_narrative_index — Method
build_narrative_index(experiments_root::AbstractString;
output::AbstractString = joinpath(experiments_root, "INDEX.md"))
-> StringScan experiments_root/EXP-*-*/README.md, parse each file's front-matter, and write a markdown table of experiments ordered by numeric ID. Complementary to build_experiments_index (which lists out/data/<P>/<run>/ provenance records).
Columns: ID, Slug, Status, Started, Hypothesis ref, Data runs.
Returns the absolute path of the written INDEX.
DataVault.check_schema_compat — Method
check_schema_compat(vault;
reader_package::AbstractString,
reader_min_writer_version::AbstractString="",
reader_expected_fields::Vector{String}=String[])
-> NamedTupleInspect the run's schema.toml and return:
(; ok::Bool, status::Symbol, missing_fields::Vector{String},
extra_fields::Vector{String}, notes::String)status values:
:legacy— no schema.toml (pre-0.5.0 run);ok=false(reader should use best-effort fallback).:mismatch— writer'spackage_versionis lower thanreader_min_writer_version, or the writer package name differs fromreader_package;ok=false.:partial— schema is present and the writer matches, but somereader_expected_fieldsare missing from the data;ok=false(reader may still proceed with a derive fallback).:match— all checks passed;ok=true.
DataVault.cleanup_stale — Method
cleanup_stale(vault; stale_after=600.0) -> IntRemove .running sentinel files whose heartbeat= timestamp is older than stale_after seconds. Returns the number of files removed.
The heartbeat is read from the file content (written by mark_running! and updated by touch_running!). If the heartbeat cannot be parsed, the file's mtime is used as a fallback.
Pass stale_after=0.0 to remove all .running files unconditionally (the pre-v0.4.1 behaviour).
DataVault.clear_running! — Method
clear_running!(vault, key, owner) -> Boolclear_running! that removes the file only when its owner= is owner. Returns whether it removed anything.
The two-argument form deletes regardless of owner, so a master releasing AFTER losing its lock deletes the reclaiming master's live .running and re-opens double execution. An unstamped file is not removed either: it cannot be shown to be ours, and stale_after will reclaim it.
Read-then-unlink, so the same non-atomic gap as refresh_running! applies. The difference from the owner-blind form is unbounded-to-microseconds, not to zero.
DataVault.clear_running! — Method
clear_running!(vault, key)Remove the .running sentinel for key. Idempotent — safe to call when the file has already been removed (e.g. by mark_done!).
DataVault.data_dir — Method
data_dir(vault, key) -> StringDirectory holding key's payloads, {outdir}/data/{project}/{run}/{param_path}. Reach for this when writing something alongside the payload — a snapshot, a log, a figure this key owns — so it lands where load will look.
DataVault.data_file — Method
data_file(vault, key; prefix="data") -> StringPath of key's payload file inside data_dir. prefix selects a parallel series stored beside the default one.
DataVault.experiment_template — Method
experiment_template() -> StringReturn the canonical EXP-NNN/README.md template shipped with DataVault.
The template uses {{name}} placeholders (id, slug, purpose, hypothesis, hypothesis_ref, author, today) that new_experiment interpolates. The header is a YAML front-matter block delimited by --- lines — build_narrative_index parses it back out.
DataVault.find_log_tomls — Method
find_log_tomls(outdir) -> Vector{String}Recursively scan {outdir}/.datavault/ for *.log.toml files and return their absolute paths. The discovery contract (the directory name and the .log.toml suffix) is frozen across DataVault versions, so this works for data written by any past or future version.
DataVault.gather_code_versions — Method
gather_code_versions(project_root) -> Dict{String,String}Collect git rev-parse --short HEAD for project_root and every submodules/*/ directory underneath. The parent entry is keyed "parent"; submodule entries use their directory name.
Never throws: directories that are not git checkouts (or lookups that fail) yield an empty-string hash.
DataVault.has_artifact — Method
has_artifact(vault, name, key) -> BoolWhether artifact name of key is already built. Only a COMPLETE artifact counts: the payload is moved into place atomically after it is written.
DataVault.is_done — Method
is_done(vault, key) -> BoolDataVault.is_running — Method
is_running(vault, key) -> BoolDataVault.list_figure_history — Method
list_figure_history(vault::Vault; name::Union{Nothing,AbstractString}=nothing)
-> Vector{NamedTuple}Return version entries from figures.toml. If name is given, only that figure's versions are returned. Each element:
(; name, subdir, generated_at, git_hash, generator, archive_path,
size_bytes, content_hash, is_current, tag, metadata)DataVault.load — Method
DataVault.load(vault, key; prefix="data") -> DictLoad the JLD2 data file for key. Returns the stored dict. Raises an error if the file does not exist.
Absence is the common case when walking a run that is still being acquired, and a try/catch around this is the expensive way to ask: guard with is_done, or use tryload, which returns nothing instead of raising.
DataVault.load_artifact — Method
load_artifact(vault, name, key) -> valueThe stored artifact; an error if it has not been built. See artifact! to build on a miss, and tryload_artifact for absence as nothing.
DataVault.load_bin — Method
DataVault.load_bin(vault, key; prefix="checkpoint") -> DictLoad a binary checkpoint file. Raises an explicit error if not present (checkpoints may exist only on HPC).
DataVault.load_ledger — Method
load_ledger(vault::Vault) -> Vector{Dict{String,String}}Read ledger.csv for this vault's (study, run) and return it as a vector of row dicts (column_name => string_value). Returns Dict{String,String}[] if the ledger file does not exist or is empty.
Values are returned as strings; callers are responsible for type conversion. The parser assumes DataVault's output format (simple comma-separated, no quoting, no embedded commas).
Use build_ledger(vault) first if you want a fresh ledger reflecting the latest .done files.
DataVault.load_recorded — Method
load_recorded(vault, key; prefix="data") -> (data, record)Load key's result the way a report should: copy the file once, hash the copy, load the copy. The digest then names exactly the bytes data came from — a file replaced while it is being read cannot yield the digest of one version and the data of another.
record is (; key, file, read_sha256, result_sha256, observation, completed_at, done_version): key is canonical(key), file is relative to the outdir, read_sha256 is what was read, and the rest is what the .done marker recorded when the key was computed ("unknown" when it did not say). read_sha256 != result_sha256 means the bytes read are not the bytes that computation wrote.
DataVault.mark_done! — Method
mark_done!(vault, key; jobid=nothing, tag_value=nothing, result=nothing, observation=nothing)Write a .done file for key. Removes the corresponding .running file if present.
result is what save! returned for this key. Pass it: it is the only way the marker can name the bytes that were written. observation is the token observe_sources returned in the process that computed the key; the marker records it, and the observation says how far that process's loaded code was checked against its source snapshot.
Fields written (done_version=2), every one of them on every call:
| field | value |
|---|---|
jobid | SLURM_JOB_ID, else the current PID, unless given |
completed | local time with no zone, as before (kept for existing readers) |
completed_at | the completion time in UTC, yyyy-mm-ddTHH:MM:SSZ |
git_hash | short HEAD of the config's repo, as before (kept for existing readers) |
git_commit_observed, git_object_format | full HEAD and object format, or unknown |
git_observed_at | completion: the working tree seen now, not the code the process loaded |
result_sha256, result_file | from result (file relative to the outdir), or unknown |
observation | the token of the computing process's source observation, or unknown |
tag_value | only when given |
DataVault.mark_running! — Method
mark_running!(vault, key)Write a .running sentinel with pid, started, and heartbeat fields. Non-atomic overwrite — for multi-master coordination use acquire_running! instead, which guarantees exclusive acquisition via POSIX link().
DataVault.master_ledger_report — Method
master_ledger_report(outdir) -> NamedTuplebuild_master_ledger's rows together with whether the sources were compatible:
(; rows, ok, columns, sources, collisions)columns: the union of every contributing ledger's columns, sorted.sources: one(; project_name, run, log_toml, nrows, columns, missing)per contributing run, wheremissingis the union columns that run's ledger does not have.collisions:(; project_name, run, column)for each ledger column shadowed by one of the meta columns the merge adds.ok: every source has the full column set and nothing collided.
The schema that decides whether a merge is sound here is the CSV COLUMN SET, not schema.toml: build_ledger derives its columns from the run's own params, so two runs whose key spaces differ produce rows that are not the same shape. check_schema_compat answers a different question, which is whether ONE run satisfies a reader's expectations.
DataVault.new_experiment — Method
new_experiment(vault::Vault;
slug::AbstractString,
purpose::AbstractString = "",
hypothesis::AbstractString = "",
hypothesis_ref::AbstractString = "",
author::AbstractString = _git_user_name(),
experiments_root::Union{Nothing,AbstractString} = nothing,
id::Union{Nothing,AbstractString,Integer} = nothing)
-> StringScaffold experiments_root/EXP-<id>-<slug>/README.md from the DataVault TEMPLATE. Returns the absolute path of the written README.
Behaviour:
experiments_rootdefaults to a siblingexperiments/dir of the caller's package root:pkgdir(...)is discovered fromvault.config_pathif available (see_default_experiments_root). Override when the caller's layout differs.idis:- an
AbstractString→ used verbatim (e.g."001","20260418") - an
Integer→ zero-padded to 3 digits nothing→ auto-increment from existingEXP-NNN-*siblings
- an
- Aborts with an error if
experiments_root/EXP-<id>-<slug>already exists (pre-existing experiments must not be silently overwritten). - Does not run DMRG / TDVP / anything compute-heavy — purely a doc scaffold that an editor can open immediately.
DataVault.new_owner_token — Method
new_owner_token() -> StringA token identifying one acquisition, as "<host>:<pid>:<nonce>". The nonce is what makes it identify the ACQUISITION rather than the process: a master that loses a lock and later reacquires the same key must not be mistaken for its earlier self by a heartbeat still in flight.
DataVault.observe_sources — Method
observe_sources(vault; phase = "run-start", process = Dict(), hash_limit = 64 MiB) -> tokenObserve the source roots this process can see — the config's repository and every path dependency of the active environment — store the snapshot (once per distinct content) and an observation record, and return the record's token for mark_done!'s observation.
The record says when (observed_at, phase), where (host, pid, and whatever process adds, such as a worker id), which snapshot (source), each root's git HEAD and whether it was dirty, the Julia build and a fixed list of environment variables, and the binding: how far the code this process has loaded was checked against the snapshot (see binding_of). File contents are stored only for .jl and .toml files; every other file is inventoried by size and digest.
DataVault.open_all — Method
open_all(outdir::AbstractString; readonly=false) -> Vector{AttachedStudy}Discover every *.log.toml under {outdir}/.datavault/ and attach to each. Broken log.toml files (unknown version, missing envelope, etc.) are logged as warnings and skipped so that iteration over a partially-corrupted outdir still yields the healthy studies.
readonly=true attaches without writing. Discovery over an outdir touches every run it finds, so a counter or a plotter run against three runs that had never executed a key creates three log.toml files and freezes their key spaces.
DataVault.param_path — Method
param_path(vault, key) -> StringThe directory segment key gets under this vault's path scheme — one component, no parents.
Which scheme that is comes from the vault, not from the caller: see Vault. Rebuilding this string from ParamIO.format_path instead is what silently pins a consumer to the legacy fixed2 rendering.
DataVault.read_done — Method
read_done(vault, key) -> Dict{String,String}The fields of key's .done marker, or an empty dict when there is none. Every version-2 field is present in a version-2 marker (as unknown when it could not be known); a version-1 marker has no done_version.
DataVault.read_log_toml — Method
read_log_toml(path) -> LogTomlV1 (or future LogTomlV2, …)Read a log.toml file from path. Dispatches on [meta].log_toml_version via LOG_TOML_READERS. The returned struct depends on the file's version.
Throws an explicit error if:
- the file does not exist
[meta]envelope is missing or malformedlog_toml_versionis unknown to this DataVault build
DataVault.read_schema_record — Method
read_schema_record(vault) -> Union{NamedTuple,Nothing}Parse outdir/data/<project>/<run>/schema.toml and return:
(; package, package_version, package_root, parent_git_hash,
submodule_git_hashes::Dict{String,String},
data_schema_version::Int,
top_level_keys::Vector{String},
bench_keys::Vector{String},
datavault_version::String,
created_at::String,
hostname::String)Returns nothing for legacy runs (no schema.toml on disk).
DataVault.record_figure — Method
record_figure(vault; study, scripts=Dict())Write meta.toml under out/figure/{study}/.
scripts is an optional Dict{String,String} mapping label → path, e.g. Dict("plot_energy" => "scripts/analysis/plot_energy.jl").
DataVault.refresh_running! — Method
refresh_running!(vault, key, owner) -> Boolrefresh_running! that first checks the file is still owner's. Returns false and writes NOTHING when the on-disk owner= differs, is absent, or the file is gone, so a master that stalled past stale_after learns it lost the lock instead of stamping its own clock onto the reclaiming master's file.
The owner-blind two-argument form cannot: it returns false only when the file is ABSENT, which is a window of microseconds during a reclaim.
The check is read-then-write and not atomic. A sibling reclaiming in the gap between the two is still possible; what this closes is the case where a reclaim has ALREADY happened, which is the one that lasts for the rest of the key. A file that cannot be read at all is false as well: absent and unreadable are both "not provably ours".
DataVault.refresh_running! — Method
refresh_running!(vault, key) -> BoolRefresh the .running heartbeat to now. Returns true if the file existed (our lock is still ours) or false if it was cleared underneath us — meaning another master has reclaimed via acquire_running! after stale_after elapsed, and the caller should stop work.
Thin wrapper around touch_running! that also tells the caller whether the heartbeat update actually landed.
DataVault.restore_figure! — Method
restore_figure!(vault::Vault, name::AbstractString, archive_tag::AbstractString;
subdir::AbstractString="") -> StringReplace the live figure <run>/<subdir>/<name> with the archived version identified by archive_tag. The current live file is first archived (via archive_figure!) so the restoration is reversible.
Throws if the archive_tag does not exist for this (name, subdir). Returns the absolute live path.
DataVault.results — Method
results(vault; status=:done, prefix="data") -> iterator of (key, payload)Every point and what was stored for it, as (DataKey, Dict) pairs.
The reader side of a sweep is otherwise always the same three lines — enumerate, load, push.
Not exported, like keys beside it: call it as DataVault.results(vault).
status defaults to :done rather than to keys' :all, because load raises on a key nobody has computed. A pairing that inherited :all would die on the first pending key, which is the state a sweep is in for most of its life.
It is lazy — a generator, not a Vector. A production sweep is thousands of JLD2 files and a payload can be large, so reading one point must not force the rest. collect it when the whole thing is wanted.
for (key, d) in DataVault.results(vault)
push!(rows, (; T = d["kbT"], E = d["energy"]))
endDataVault.running_age_secs — Method
running_age_secs(vault, key) -> Float64Age in seconds of the .running file's most recent heartbeat. Returns Inf if no .running file exists. The same computation is used internally by acquire_running! and cleanup_stale.
DataVault.running_heartbeat — Method
running_heartbeat(vault, key) -> Union{DateTime, Nothing}Read the heartbeat= timestamp from the .running file. Returns nothing if the file does not exist or the timestamp cannot be parsed.
DataVault.running_owner — Method
running_owner(vault, key) -> Union{String,Nothing}The owner= token in the .running file, or nothing when the file is absent or carries no token. A .running written before owner stamping, or by mark_running!, has none.
DataVault.save! — Method
DataVault.save!(vault, key, data; prefix="data") -> (; file, sha256)Atomically write data (a Dict) to the JLD2 data file for key. Uses a tmp file + rename pattern safe on NFS. Does NOT automatically mark done — call mark_done! explicitly, and pass it what this returns (mark_done!(vault, key; result=save!(…))) so the .done marker names the bytes that were written: sha256 is taken from the temporary file before the rename, not from file afterwards.
DataVault.save_bin! — Method
DataVault.save_bin!(vault, key, data; prefix="checkpoint")Atomically write a binary checkpoint.
DataVault.status_dir — Method
status_dir(vault, key) -> StringDirectory holding key's .done / .running markers. The markers themselves are is_done and is_running's business; this is for a caller that needs the directory itself.
DataVault.touch_running! — Method
touch_running!(vault, key)Update the heartbeat= line in the .running file to the current time. Called periodically (e.g. every 60 s) while computation is in progress so that cleanup_stale can distinguish live jobs from crashed ones.
No-op if the .running file does not exist (already cleared or never created).
DataVault.tryload — Method
DataVault.tryload(vault, key; prefix="data") -> Union{Dict,Nothing}The stored dict for key, or nothing when there is no file. load with absence as a value rather than an exception, for the scan-a-partial-vault case.
Only absence is a nothing: a file that exists and cannot be read still raises, so a corrupt payload is not reported as a missing one.
DataVault.tryload_artifact — Method
tryload_artifact(vault, name, key) -> Union{value,Nothing}load_artifact with absence as nothing. An artifact that exists and cannot be read, or was stored under a different identity, still raises.