DataVault.jl

Models

DataVault.DataVaultModule

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_stale
source
DataVault.LOG_TOML_READERSConstant

Reader registry: logtomlversion → reader function.

新しいスキーマを追加するときは:

  1. 新しい struct LogTomlVN と readlogtomlvN を定義
  2. このレジストリに N => _read_log_toml_vN を追加
  3. 古いエントリは絶対に消さない

これにより、過去どのバージョンで書かれた log.toml でも、現役の DataVault が 読み戻せることを保証する。

source
DataVault.ArtifactBusyType
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.

source
DataVault.AttachedStudyType
AttachedStudy

A bundle of (vault, log.toml info, log_path) produced by open_all. Iterate over these to process every study under an outdir.

source
DataVault.AutoPathFormatterType
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.

source
DataVault.VaultType
Vault

Handle for a single (study, run) pair. Wraps a ConfigSpec and resolves file paths under outdir.

Fields

  • config_path: absolute path to the source config TOML
  • spec: parsed ParamIO.ConfigSpec (study, path_keys, paramsets)
  • outdir: absolute root path under which data/, status/, bin/, figure/, and .datavault/ live
  • run: 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) -> String
  • readonly: when true, 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}) -> String

A 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.

source
Base.keysMethod
keys(vault; status=:all) -> Vector{DataKey}

Enumerate DataKeys for this study.

  • status=:all — all keys (default)
  • status=:done — only keys with a .done file
  • status=:pending — only keys without a .done file
source
DataVault._atomic_jld2_writeMethod
_atomic_jld2_write(path, data) -> String

Write 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.

source
DataVault._default_experiments_rootMethod
_default_experiments_root(vault::Vault) -> String

Heuristic: 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.

source
DataVault._experiment_provenance_mdMethod
_experiment_provenance_md(vault::Vault, run_readme::AbstractString) -> String

Render 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.

source
DataVault._git_hashMethod
_git_hash(ref_path) -> String

Return the short HEAD hash of the git repo containing ref_path, or "unknown" if not in a repo.

source
DataVault._git_observeMethod
_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.

source
DataVault._infer_outdirMethod

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.

source
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.

source
DataVault._introspect_schema_keysMethod
_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.

source
DataVault._parse_front_matterMethod
_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.

source
DataVault._read_ledger_csvMethod

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.

source
DataVault._save_config_snapshotMethod
_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.

source
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-matter data_runs list already contains vault.run are updated. This lets authors opt in explicitly — they wrote data_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.

source
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.

source
DataVault._validate_log_tomlMethod
_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.

source
DataVault._vcompareMethod
_vcompare(a, b) -> Int

Compare two version strings loosely. Returns -1, 0, or 1. Falls back to lexicographic comparison when either string is not a valid VersionNumber.

source
DataVault._write_schema_tomlMethod
_write_schema_toml(vault, writer, code_versions, top_keys, bench_keys) -> String

Write 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.

source
DataVault.acquire_running!Method
acquire_running!(vault, key; stale_after=600.0) -> Symbol

Atomically 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

SymbolMeaning
:okNo prior .running existed; fresh file created.
:reclaimedPrior .running was stale (heartbeat age > stale_after);
replaced with a fresh file owned by this caller.
:busyAnother 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

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.

source
DataVault.archive_figure!Method
archive_figure!(vault::Vault, live_path::AbstractString;
                generator_script::Union{Nothing,AbstractString}=nothing,
                metadata::AbstractDict=Dict{String,Any}(),
                subdir::AbstractString="") -> String

Snapshot live_path into the run's figure archive and append a version entry to figures.toml. live_path itself is not modified.

  • subdir is the relative directory under the run's figure dir (e.g. "phase1"); use "" for files sitting directly under <run>/.
  • generator_script is recorded verbatim (typically @__FILE__).
  • metadata is 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.

source
DataVault.artifact!Method
artifact!(build, vault, name, key; wait=true, poll=5.0, stale_after=600.0,
          heartbeat_interval=60.0, timeout=Inf) -> value

Artifact 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"))
end

akey 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.

source
DataVault.artifact_dirMethod
artifact_dir(vault, name, key) -> String

The 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.

source
DataVault.attachMethod
attach(log_path) -> Vault
attach(outdir; project, run="default") -> Vault

Attach 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:

  1. {outdir}/{layout.config_snapshot} — the frozen snapshot taken at initial construction; preferred because it is stable under moves and is colocated with the data.
  2. log.toml[study].config — the original absolute path recorded at construction; used as a fallback with a warning.
  3. 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.

source
DataVault.binding_ofMethod
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.

source
DataVault.build_experiment_reportMethod
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") -> String

Generate 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_root defaults to pkgdir(writer) (used for git introspection).
  • data_schema_version overrides any module constant.
  • experiments_root — when given, the run is cross-linked into any EXP-NNN-*/README.md whose front-matter data_runs list contains vault.run (or into every EXP when narrative_match_all=true). The target README gets its ## Generated provenance section rewritten with a link back to this run's auto-README, and the data_runs list 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.

source
DataVault.build_experiments_indexMethod
build_experiments_index(outdir, project_name) -> String

Walk 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.

source
DataVault.build_ledgerMethod
build_ledger(vault) -> String

Scan all .done files and write ledger.csv under the project data directory. Returns the path to the written file.

source
DataVault.build_master_ledgerMethod
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_name
  • run
  • datavault_version
  • log_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.

source
DataVault.build_narrative_indexMethod
build_narrative_index(experiments_root::AbstractString;
                      output::AbstractString = joinpath(experiments_root, "INDEX.md"))
    -> String

Scan 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.

source
DataVault.check_schema_compatMethod
check_schema_compat(vault;
                    reader_package::AbstractString,
                    reader_min_writer_version::AbstractString="",
                    reader_expected_fields::Vector{String}=String[])
    -> NamedTuple

Inspect 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's package_version is lower than reader_min_writer_version, or the writer package name differs from reader_package; ok=false.
  • :partial — schema is present and the writer matches, but some reader_expected_fields are missing from the data; ok=false (reader may still proceed with a derive fallback).
  • :match — all checks passed; ok=true.
source
DataVault.cleanup_staleMethod
cleanup_stale(vault; stale_after=600.0) -> Int

Remove .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).

source
DataVault.clear_running!Method
clear_running!(vault, key, owner) -> Bool

clear_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.

source
DataVault.data_dirMethod
data_dir(vault, key) -> String

Directory 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.

source
DataVault.data_fileMethod
data_file(vault, key; prefix="data") -> String

Path of key's payload file inside data_dir. prefix selects a parallel series stored beside the default one.

source
DataVault.experiment_templateMethod
experiment_template() -> String

Return 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.

source
DataVault.find_log_tomlsMethod
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.

source
DataVault.gather_code_versionsMethod
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.

source
DataVault.has_artifactMethod
has_artifact(vault, name, key) -> Bool

Whether artifact name of key is already built. Only a COMPLETE artifact counts: the payload is moved into place atomically after it is written.

source
DataVault.list_figure_historyMethod
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)
source
DataVault.loadMethod
DataVault.load(vault, key; prefix="data") -> Dict

Load 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.

source
DataVault.load_binMethod
DataVault.load_bin(vault, key; prefix="checkpoint") -> Dict

Load a binary checkpoint file. Raises an explicit error if not present (checkpoints may exist only on HPC).

source
DataVault.load_ledgerMethod
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.

source
DataVault.load_recordedMethod
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.

source
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:

fieldvalue
jobidSLURM_JOB_ID, else the current PID, unless given
completedlocal time with no zone, as before (kept for existing readers)
completed_atthe completion time in UTC, yyyy-mm-ddTHH:MM:SSZ
git_hashshort HEAD of the config's repo, as before (kept for existing readers)
git_commit_observed, git_object_formatfull HEAD and object format, or unknown
git_observed_atcompletion: the working tree seen now, not the code the process loaded
result_sha256, result_filefrom result (file relative to the outdir), or unknown
observationthe token of the computing process's source observation, or unknown
tag_valueonly when given
source
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().

source
DataVault.master_ledger_reportMethod
master_ledger_report(outdir) -> NamedTuple

build_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, where missing is 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.

source
DataVault.new_experimentMethod
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)
    -> String

Scaffold experiments_root/EXP-<id>-<slug>/README.md from the DataVault TEMPLATE. Returns the absolute path of the written README.

Behaviour:

  • experiments_root defaults to a sibling experiments/ dir of the caller's package root: pkgdir(...) is discovered from vault.config_path if available (see _default_experiments_root). Override when the caller's layout differs.
  • id is:
    • an AbstractString → used verbatim (e.g. "001", "20260418")
    • an Integer → zero-padded to 3 digits
    • nothing → auto-increment from existing EXP-NNN-* siblings
  • 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.
source
DataVault.new_owner_tokenMethod
new_owner_token() -> String

A 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.

source
DataVault.observe_sourcesMethod
observe_sources(vault; phase = "run-start", process = Dict(), hash_limit = 64 MiB) -> token

Observe 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.

source
DataVault.open_allMethod
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.

source
DataVault.param_pathMethod
param_path(vault, key) -> String

The 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.

source
DataVault.read_doneMethod
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.

source
DataVault.read_log_tomlMethod
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 malformed
  • log_toml_version is unknown to this DataVault build
source
DataVault.read_schema_recordMethod
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).

source
DataVault.record_figureMethod
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").

source
DataVault.refresh_running!Method
refresh_running!(vault, key, owner) -> Bool

refresh_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".

source
DataVault.refresh_running!Method
refresh_running!(vault, key) -> Bool

Refresh 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.

source
DataVault.restore_figure!Method
restore_figure!(vault::Vault, name::AbstractString, archive_tag::AbstractString;
                subdir::AbstractString="") -> String

Replace 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.

source
DataVault.resultsMethod
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"]))
end
source
DataVault.running_heartbeatMethod
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.

source
DataVault.running_ownerMethod
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.

source
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.

source
DataVault.save_bin!Method
DataVault.save_bin!(vault, key, data; prefix="checkpoint")

Atomically write a binary checkpoint.

source
DataVault.status_dirMethod
status_dir(vault, key) -> String

Directory 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.

source
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).

source
DataVault.tryloadMethod
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.

source
DataVault.tryload_artifactMethod
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.

source