tpt-repro
RustPure-Rust, MIT/Apache-2.0-dual-licensed substrate for treating computation as a content-addressable, reproducible object — canonical identity, provenance, and cache soundness for builds, data, AI, media, WASM, and container workloads.
Languages
tpt-repro
Treat computation as a content-addressable, reproducible object.
tpt-repro is a pure-Rust substrate built on one idea: a computation has an
identity, and that identity is derived rather than declared.
Inputs + computation + environment + dependencies + policy -> Computation Identity
Two independently written specifications that mean the same thing get the same
ComputationID. Specifications that differ anywhere — a compiler flag, an
environment variable, a cache policy — get different ones. An execution then
produces artifacts and provenance that says what produced them, so a result is
evidence of a derivation rather than just a file.
TPT Solutions. Dual-licensed under MIT OR Apache-2.0.
Where it is
This repository covers the Initial Milestone (spec §10) and the first two phases beyond it:
- Phases 0–9 (complete). Computation specification, canonicalization, computation identity, local CAS, DAG, execution, provenance receipts, cache validation, reproduction verification and the CLI.
- Phase 10 (complete). Execution isolation, with per-control reporting of what was actually enforced.
- Phase 11 (complete). Property-based verification of identity stability, dependency closure, cache soundness, provenance integrity and invalidation correctness.
- Phase 12 (complete). Rust build integration: a compiler invocation as an addressable computation, manifest capture, incremental builds and reproducible-build reports.
- Phase 13 (complete). WASM build integration, with the determinism hazards specific to wasm modules made explicit.
- Phase 14 (complete). OCI image integration: deterministic layer identity, and an image derived as an ordinary computation over its base and layers.
- Phase 15 (complete). Media integration: a transcode's picture identity held separate from its bytes, with the codec hazards named explicitly.
- Phase 16 (complete). Data integration: dataset identity over a declared row order, and reductions whose answer depends on how they were partitioned.
- Phase 17 (complete). AI/ML integration: model identity, experiment manifests, and why a seeded run still cannot be called deterministic.
- Phase 18 (complete, capstone). Semantic substitution: a compiler, a site or a device may stand in for another only when a checkable condition holds, wired into cache validation as an auditable finding.
All 96 items in todo.md are complete.
Requirements
Rust 1.85 or newer. That floor is set by the dependency graph rather than
by this code, and the reason is recorded next to the declaration in
Cargo.toml: thiserror 2.x needs 1.77, and a getrandom reached through the
tempfile dev-dependency uses a manifest that older cargo cannot even parse.
The msrv CI job runs the same command, so the claim is checked rather than
asserted.
Crate map
| Crate | What it is for |
|---|---|
tpt-repro-core | Domain model: specifications, the five distinct identities, policies, resource requirements, determinism contracts |
tpt-repro-canon | Canonical encoding, canonical hashing, schema identity, stable normalization |
tpt-repro-cas | Content-addressed storage: the CasBackend trait, filesystem and in-memory backends |
tpt-repro-graph | The computation DAG: closure, cycle detection, incremental invalidation |
tpt-repro-exec | Execution: runners, output capture, cancellation, resource accounting, receipts |
tpt-repro-provenance | The provenance graph, lineage, toolchain/environment capture, signed receipts |
tpt-repro-cache | Identity-and-policy keyed cache, self-explaining validation, trust policies, eviction |
tpt-repro-reproduce | Reproduction requests, permitted substitutions, verification reports |
tpt-repro-sandbox | Execution isolation: filesystem, network, environment, capabilities, limits |
tpt-repro-proof | Property-based verification of the claims above |
tpt-repro-build | Rust builds: manifest capture, incremental planning, reproducibility reports |
tpt-repro-wasm | WASM builds: mandatory targets, toolchain capture, determinism profile |
tpt-repro-oci | OCI images: deterministic layer identity, image derivation over its base and layers |
tpt-repro-media | Media: picture identity held separate from container bytes, codec hazards named |
tpt-repro-data | Data: dataset identity over declared row order, partition-sensitive reductions, scientific jobs |
tpt-repro-ai | AI/ML: model identity, experiment manifests, accelerator hazards, a seed's real reach |
tpt-repro-substitute | Semantic substitution: declared, checkable equivalence conditions, wired into cache validation |
tpt-repro-cli | The tpt-repro command |
Try it
$ cat greet.json
{
"metadata": { "name": "greet", "version": "1.0.0" },
"computation": {
"name": "greet",
"kind": { "type": "emit", "files": { "greeting.txt": "hello" } },
"determinism": { "required": "bit_for_bit", "pin_environment": true }
}
}
$ tpt-repro define greet.json
defined 'greet'
computation c63f1cd50c8d0cf9...
specification 7f491b98824ae448...
canonical form 1018 bytes (schema 1)
$ tpt-repro run greet.json --check
executed 'greet'
computation c63f1cd50c8d0cf9...
execution ca01898dc3eee175...
provenance 19ce19e591ae540d...
outputs:
greeting.txt 5c4a070a35b7ee0b...
reproduction Reproduced: 1/1 outputs identical, demonstrated 'bit-for-bit deterministic'
$ tpt-repro run greet.json
cache hit: this artifact is valid for this computation because
[ok] computation identity: entry is for c63f1cd50c8d
[ok] policy: policy fingerprint f55dfdd85186
[ok] artifact integrity: every stored output still hashes to its address
...
Every command takes --json for machine-readable output. verify and
reproduce exit 2 when the answer is "no", so a CI job can tell a broken
derivation from a broken tool.
Building a specification from a manifest
The adapters are not only reporters. spec turns a manifest into a
specification, and the output is exactly the document define accepts, so the
two compose:
$ cat exp.json
{
"name": "run-42",
"code": "sha256:1111...1111",
"datasets": { "train": "sha256:2222...2222" },
"seed": 42
}
$ tpt-repro spec experiment --from exp.json | tpt-repro define -
built 'experiment:run-42' (experiment)
computation faf1dac81b66dbc2...
determinism best effort (from the default profile)
defined 'experiment:run-42'
computation faf1dac81b66dbc2...
specification 27c0879feced0504...
canonical form 1722 bytes (schema 1)
The same manifest always produces the same identity, and the identity spec
reports is the one define records — that agreement is what the test suite
pins, for all four adapters.
--reproducible opts into the strict profile, which changes both the derived
level and the identity. It is not the default, because a manifest names a
computation and does not by itself control any hazard: without the flag the
specification is held to best effort, which is what the default profile
actually justifies. For spec experiment the flag is refused outright, because
an experiment's determinism comes from the accelerator recorded in its manifest
rather than from a profile — a flag that silently did nothing would be worse
than an error that explains itself.
spec is available for image, transcode, experiment, etl, build and
wasm. The last two take --kind and, for wasm, --emit:
$ tpt-repro spec wasm --reproducible --from w.json 2>&1 >/dev/null
determinism reproducible (from the reproducible profile)
reproducible is the honest ceiling there — never bit_for_bit, because no
compiler flag sorts a HashMap. spec build refuses --reproducible outright,
since a Rust build's contract is fixed at controlled by the adapter: a build
inherits paths it does not control, and there is no profile to select.
Manifests are hand-written, so optional fields default individually — but the
fields identifying what the manifest is for do not. A manifest with no code,
source or reference is rejected rather than quietly addressing the all-zeros
artifact, because a well-formed identity for content nobody has is worse than a
parse error.
Asking a domain what it cannot promise
The domain adapters are not just constructors — each one names the ways its results can fail to reproduce, and the CLI prints them. The default and reproducible profiles are shown side by side, because the gap between them is what a caller has to close.
$ tpt-repro hazards data
hazards: data
float addition is not associative, so a repartition can change the answer: this is the one domain where a hazard moves the values, not just the bytes
default profile:
[GAP] value float summation order - sum in a fixed order, or in a wider or exact type
[GAP] value impure user function - require pure functions, or pin their inputs
[GAP] byte row-group boundaries - fix the row-group size
[GAP] value shuffle order - sort before reducing
[GAP] value thread count - fix the thread count, or reduce order-independently
[GAP] value accelerator path - pin the BLAS and the hardware
[GAP] byte output encoding - fix the codec and its settings
-> justifies best effort
-> justifies best effort over the written bytes
reproducible profile:
[ok] value float summation order - sum in a fixed order, or in a wider or exact type
...
-> justifies bit-for-bit deterministic
-> justifies bit-for-bit deterministic over the written bytes
the gap between the two is what a caller has to close.
Note the two verdicts for data: a job can be exactly reproducible for its
values while the file it writes is not, and reporting only one of those would
mislead in whichever direction you did not ask about. hazards media is the
opposite case — no media hazard can touch a pixel, so its level is a statement
about bytes and never about quality. And hazards wasm --reproducible still
reports gaps on purpose: no compiler flag can sort a HashMap, so the strictest
wasm profile caps at reproducible and never claims bit-for-bit.
Checking a substitution
substitute applies the capstone's rule to two specifications you name. It
exits 2 when the substitution is not permitted, and says exactly why.
$ tpt-repro substitute original.json substituted.json declaration.json
refused: gcc -> clang
the two specifications differ in: toolchain, parameter 'opt-level'
the declaration covers: toolchain
evidence: declared only (policy requires declared)
refused because parameter 'opt-level' is not declared
That refusal is the point. A toolchain swap that also quietly changed the optimisation level is not a toolchain swap, and the tool cannot be talked out of it by asserting the compilers are equivalent.
The parts worth knowing about
Identities are distinct types. SpecificationID, ComputationID,
ExecutionID, ArtifactID and ProvenanceID are separate newtypes, so the
compiler rejects mixing them up. Renaming a specification changes its
SpecificationID and not its ComputationID, because the computation did not
change.
Identity is a golden-tested format. tpt-repro-canon pins its own output
with a test. Changing the encoding without bumping CANON_SCHEMA_VERSION would
silently reinterpret every identity ever derived, so the test fails loudly
instead.
The cache explains itself. A hit is a claim, so validation returns a finding per aspect — identity, policy, cache-enabled, artifacts present, artifact integrity, determinism, trust, reproduction evidence — and every one of them must hold. A miss names what failed.
Determinism is never assumed. A specification declares the level it is
held to; a fresh record demonstrates nothing. Only re-running and comparing
bytes turns that into a claim, and the level claimed is conservative: identical
bytes from an unpinned environment are reproducible, not bit_for_bit.
Substitution is opt-in and justified. Permitting compiler A to stand in for compiler B requires the policy to allow it and the request to declare it and a justification. Checks happen before anything executes.
Isolation is reported, not asserted. A sandbox returns an Enforcement
verdict per control. LocalSandbox really confines the working directory, the
output set and the environment, and honestly reports that it does not stop a
child writing elsewhere, dialling out, or exceeding a memory limit. That
report is recorded in provenance.
The claims are property-tested. tpt-repro-proof checks closure and
invalidation against a deliberately naive reference implementation, because a
property that merely restated the library would prove nothing.
A build is identified by its dependency set, not its command line.
tpt-repro-build hashes the source tree, the lockfile, the profile, the
features, the target and the pinned toolchain, and puts the result in the
specification — so a dependency change moves the ComputationID. Two builds of
the same manifest are reproducible exactly when their product digests match,
and the report says so plainly when they do not.
Development
$ cargo test --workspace # 192 tests
$ cargo clippy --workspace --all-targets -- -D warnings
$ cargo fmt --all -- --check
CI runs the same three, plus a docs build and an MSRV check.
Licence
MIT OR Apache-2.0, at your option. See LICENSE-MIT and LICENSE-APACHE.