Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ maps a `DataKey` to file storage and tracks what's done. project-agnostic. See
- **`observe_sources(vault) -> token` keeps two claims apart.** The SNAPSHOT (`sources/src1-<hex>/`,
id = SHA-256 of its `files.tsv`) is content only — no HEAD, no host — so equal trees get equal
ids. The OBSERVATION (`observations/<token>.toml`) holds when/where/HEAD/dirty and the
**binding**: `loaded-matches-disk` only when packages loaded from the config's repo were
checked against the snapshot via their precompile cache headers (size + CRC32c, an internal
API — anything unreadable is `unknown`, never a match). Nothing under `.datavault/` may be
**binding**, which never claims a match: `loaded-differs-from-disk` when a loaded package's
precompile-cache sources (size + CRC32c, an internal API — anything unreadable is `unknown`)
differ from the snapshot, otherwise `unverified`. Loaded packages matching does not show the
code that ran was theirs (a script, a closure, a method added to Base from `Main` leave no
trace; `Base._included_files` records no run-time include), so `loaded-matches-disk` is never
written; an old record's is read as `unverified`. Nothing under `.datavault/` may be
named `*.log.toml`: discovery walks the tree for that suffix.
- **`.done` is `key=value` lines, `done_version=2`.** Readers take the keys they know and ignore
the rest; keys are only ever added. Every v2 field is written on every call (`unknown` rather
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "DataVault"
uuid = "23f5f8f6-b4da-40ee-8c72-c53b6c5de94f"
version = "0.8.5"
version = "0.8.6"
authors = ["sota shimozono <shimozono-sota631@g.ecc.u-tokyo.ac.jp>"]

[deps]
Expand Down
27 changes: 18 additions & 9 deletions src/provenance/observe.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
#
# * a SOURCE SNAPSHOT: every file of each source root as (path, type, mode, size, SHA-256),
# identified by the SHA-256 of that inventory, `src1-<hex>`, and stored once per vault;
# * its BINDING to the code the process runs: `unverified`, unless every package the process
# loaded from a root was checked against the snapshot through its precompile cache header.
# * its BINDING to the code the process runs, which never claims a match: `unverified`, or
# `loaded-differs-from-disk` when a loaded package's cached sources are known to differ from
# the snapshot. A match is not claimed because it cannot be shown from inside the process:
# code defined in a script, a closure, or a method added to Base from `Main` runs without
# leaving a trace in any cache header (`Base._included_files` records no run-time include).
#
# Nothing here says the snapshot is the code that ran. It says what was on disk, when, and how far
# the process's loaded code was checked against it. File names under `.datavault/` must never end
Expand Down Expand Up @@ -299,12 +302,13 @@ end
"""
binding_of(status, revise_loaded, main_files_in_roots) -> (binding, reasons)

The binding an observation can claim, from each root's loaded status. `loaded-matches-disk` only
when the config's repository (where the study's code lives) was loaded from and matched, every
other root that was loaded from matched, Revise is not loaded, and no file included into `Main`
lies inside a root (code defined there cannot be checked).
`loaded-differs-from-disk` when a loaded package's sources differ from the snapshot. Otherwise
`unverified`, with the 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`.
"""
function binding_of(status::AbstractDict, revise_loaded::Bool, main_files_in_roots)
differs = sort([k for (k, v) in status if v == "differs"])
Expand All @@ -327,9 +331,14 @@ function binding_of(status::AbstractDict, revise_loaded::Bool, main_files_in_roo
"$f is included into Main, where its code cannot be checked" for
f in main_files_in_roots
)
return isempty(reasons) ? "loaded-matches-disk" : "unverified", reasons
isempty(reasons) && push!(reasons, NO_MATCH_CLAIMED)
return "unverified", reasons
end

const NO_MATCH_CLAIMED =
"every loaded package matched the snapshot, but a match is not claimed: code defined " *
"outside a package (a script, a closure, a method added from Main) cannot be checked"

# ── the observation ───────────────────────────────────────────────────────────────────────────

function _root_record(root, loaded::String)::Dict{String,Any}
Expand Down
9 changes: 6 additions & 3 deletions test/vault/test_observe.jl
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,9 @@ end
mod = Base.require(Main, Symbol(t.name))
origin = Base.pkgorigins[Base.PkgId(mod)]
checkable = DataVault._cached_sources(origin.cachepath) !== nothing
@test config_root(record(t, observe_sources(t.vault)))["loaded"] ==
(checkable ? "matches" : "unknown")
r0 = record(t, observe_sources(t.vault))
@test config_root(r0)["loaded"] == (checkable ? "matches" : "unknown")
@test r0["binding"] == "unverified" # a match is recorded, never claimed
# The loaded code stays as it was; the file on disk changes under it.
write(
joinpath(t.pkg, "src", "$(t.name).jl"),
Expand All @@ -124,7 +125,9 @@ end

@testset "binding_of: what an observation may claim" begin
ok = Dict("config" => "matches", "pkg:A:1" => "matches")
@test DataVault.binding_of(ok, false, String[]) == ("loaded-matches-disk", String[])
# Everything checkable matched, and still no match is claimed.
@test DataVault.binding_of(ok, false, String[]) ==
("unverified", [DataVault.NO_MATCH_CLAIMED])
@test DataVault.binding_of(Dict("config" => "differs"), false, String[])[1] ==
"loaded-differs-from-disk"
for (status, revise, main) in (
Expand Down
Loading