tpt-repro

Rust

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

0 stars0 forks0 watchersApache License 2.0

Languages

Rust100.0%
README

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

CrateWhat it is for
tpt-repro-coreDomain model: specifications, the five distinct identities, policies, resource requirements, determinism contracts
tpt-repro-canonCanonical encoding, canonical hashing, schema identity, stable normalization
tpt-repro-casContent-addressed storage: the CasBackend trait, filesystem and in-memory backends
tpt-repro-graphThe computation DAG: closure, cycle detection, incremental invalidation
tpt-repro-execExecution: runners, output capture, cancellation, resource accounting, receipts
tpt-repro-provenanceThe provenance graph, lineage, toolchain/environment capture, signed receipts
tpt-repro-cacheIdentity-and-policy keyed cache, self-explaining validation, trust policies, eviction
tpt-repro-reproduceReproduction requests, permitted substitutions, verification reports
tpt-repro-sandboxExecution isolation: filesystem, network, environment, capabilities, limits
tpt-repro-proofProperty-based verification of the claims above
tpt-repro-buildRust builds: manifest capture, incremental planning, reproducibility reports
tpt-repro-wasmWASM builds: mandatory targets, toolchain capture, determinism profile
tpt-repro-ociOCI images: deterministic layer identity, image derivation over its base and layers
tpt-repro-mediaMedia: picture identity held separate from container bytes, codec hazards named
tpt-repro-dataData: dataset identity over declared row order, partition-sensitive reductions, scientific jobs
tpt-repro-aiAI/ML: model identity, experiment manifests, accelerator hazards, a seed's real reach
tpt-repro-substituteSemantic substitution: declared, checkable equivalence conditions, wired into cache validation
tpt-repro-cliThe 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.