Skip to content

About

High-performance, content-addressed developer computation caching engine in Rust. Guarantees zero redundant CPU cycles across builds, code generators, linters, and test suites.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

          ██████╗ ███████╗██╗   ██╗███████╗██╗      ██████╗ ██████╗ ███████╗██████╗
         ██╔══██╗██╔════╝██║   ██║██╔════╝██║     ██╔═══██╗██╔══██╗██╔════╝██╔══██╗
         ██║  ██║█████╗  ██║   ██║█████╗  ██║     ██║   ██║██████╔╝█████╗  ██████╔╝
         ██║  ██║██╔══╝  ╚██╗ ██╔╝██╔══╝  ██║     ██║   ██║██╔═══╝ ██╔══╝  ██╔══██╗
         ██████╔╝███████╗ ╚████╔╝ ███████╗███████╗╚██████╔╝██║     ███████╗██║  ██║
         ╚═════╝ ╚══════╝  ╚═══╝  ╚══════╝╚══════╝ ╚═════╝ ╚═╝     ╚══════╝╚═╝  ╚═╝

 ██████╗ ██████╗ ███╗   ███╗██████╗ ██╗   ██╗████████╗ █████╗ ████████╗██╗ ██████╗ ███╗   ██╗
██╔════╝██╔═══██╗████╗ ████║██╔══██╗██║   ██║╚══██╔══╝██╔══██╗╚══██╔══╝██║██╔═══██╗████╗  ██║
██║     ██║   ██║██╔████╔██║██████╔╝██║   ██║   ██║   ███████║   ██║   ██║██║   ██║██╔██╗ ██║
██║     ██║   ██║██║╚██╔╝██║██╔═══╝ ██║   ██║   ██║   ██╔══██║   ██║   ██║██║   ██║██║╚██╗██║
╚██████╗╚██████╔╝██║ ╚═╝ ██║██║     ╚██████╔╝   ██║   ██║  ██║   ██║   ██║╚██████╔╝██║ ╚████║
 ╚═════╝ ╚═════╝ ╚═╝     ╚═╝╚═╝      ╚═════╝    ╚═╝   ╚═╝  ╚═╝   ╚═╝   ╚═╝ ╚═════╝ ╚═╝  ╚═══╝

                           ██████╗ █████╗  ██████╗██╗  ██╗███████╗
                          ██╔════╝██╔══██╗██╔════╝██║  ██║██╔════╝
                          ██║     ███████║██║     ███████║█████╗
                          ██║     ██╔══██║██║     ██╔══██║██╔══╝
                          ╚██████╗██║  ██║╚██████╗██║  ██║███████╗
                           ╚═════╝╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝╚══════╝

Developer Computation Cache

Run it once. Replay it forever.

Reforge is a local-first computation cache for developer workflows.
Wrap any deterministic command with dcc exec, and identical work
becomes an instant, verified replay instead of a re-run.

Developer Computation Cache (dcc)  ·  Content-addressed  ·  Single-flight  ·  Self-healing  ·  Pure Rust



  $ dcc exec -i src/schema.proto -o gen/schema.rs -- protoc ...
    ✗ MISS   key 9f01ab2…   executed · 1 output captured

  $ dcc exec -i src/schema.proto -o gen/schema.rs -- protoc ...
    ✓ HIT    key 9f01ab2…   replayed from cache · command not run

Rust 1.77+ 6-crate workspace Cross-platform MIT OR Apache-2.0
Content-addressed storage Eviction policies Path confinement 20 milestone test suites

🚀  Get Started 🧠  Understand It 🛡️  Trust It
Quick Start
CLI Reference
Tool Integrations
Why Reforge?
Core Guarantee
Architecture
Cache Keys
Concurrency
Security
Self-Healing
Testing
Platforms
⌨️  cargo install --path crates/cache-cli  →  dcc exec -- <your command>  →  done.

⚙️ How a run works

①

Fingerprint
Hash the inputs, command, declared env vars, tool binary and platform into one key.

②

Look Up
Check the content-addressed store. Every object is verified against its digest before use.

③

Run or Replay
Miss: take the lock and execute. Hit, or lost the race: replay the stored result.

④

Restore Atomically
Outputs are staged in a temp location, then renamed into place. No half-written files.
Miss → ① ② ③ (execute & store) ④   |   Hit → ① ② ④ (command never runs)   |   Can't verify → falls back to running the command


⚖️ Core Guarantee

A cache hit must never change the result of a computation.

If Reforge cannot show that a stored result matches what a fresh run would produce, it runs the command. Being slow is acceptable. Being wrong is not.


🤝 The contract

The guarantee has two sides. Reforge does its part only if the declared inputs describe the command's real inputs.

✅ Reforge guarantees 📝 You provide ↩️ Otherwise, it falls back
A hit is replayed only when the key matches exactly and every stored object passes its SHA-256 check.

Outputs are restored atomically, so your workspace never holds half-written files.
A deterministic command: same inputs, same outputs.

Complete declarations: every file the command reads (-i), every environment variable it depends on (-e), every file it writes (-o).
Corrupt or unverifiable entry: quarantined, then recomputed.

Lock contention or a crashed worker: the next runner executes cleanly.

Any doubt about a stored result: the command runs.

Important

What the key can't see. Reforge hashes what you declare. Files the command reads but you didn't list, the current time, random numbers, network responses and undeclared environment variables are invisible to it. If the command's output depends on any of them, a hit can be stale. Declare everything the command reads, and don't cache commands that aren't deterministic.


🧱 The six pillars

Pillar What it does Why it matters
🏛️ Content-Addressed Storage Files, stdout, stderr and exit codes are stored under their SHA-256 digests. Identical outputs from different tasks share one physical copy. Every read is checked against its address, so silent corruption can't be replayed.
🔑 Five-Vector Cache Key The key combines input file contents, command and arguments, declared environment variables, the executable's content hash and version, and the platform (OS, architecture, pointer width, endianness). Change any one of them and the key changes, so stale entries are never matched.
🏎️ Single-Flight Execution A cross-process advisory lock per cache key. Racing runners wait, then replay the winner's result. Verified with a 50+ process stress harness. No duplicate work on parallel CI workers, and the OS releases the lock if a runner is killed.
📦 Atomic Restoration Outputs are staged in a temporary location, then renamed into place. An interrupted restore never leaves a corrupt or partial workspace.
🛡️ Self-Healing Quarantine Entries that fail checksum verification are moved to a quarantine ledger, and the command is run fresh. Bad data is kept for inspection and never reaches your workspace.
⚙️ Bounded Eviction A hard disk-usage limit with LRU, LFU or FIFO policies, plus garbage collection of unreferenced objects. The cache can't grow without limit, and oversized objects are rejected before they are stored.

💡 What Problem Does This Solve?

Developers re-run the same deterministic work all day. You edit one file, and the whole toolchain runs again over hundreds of files that didn't change. On CI it's worse, because every runner repeats what another runner just finished. That costs CPU time, battery, CI minutes and, most of all, waiting.

❌ Without Reforge ✅ With Reforge
Every command runs in full, every time.
Timestamps decide "did it change?", and they break on git checkouts and fresh clones.
Parallel CI jobs all redo the same work.
Each tool has its own caching rules, or none.
Unchanged work is replayed from the cache instead of re-run.
File contents decide, so a checkout that touches timestamps doesn't invalidate anything.
Racing runners wait for one result and share it.
One wrapper, dcc exec, behaves the same for every tool.

🎯 Where it pays off

Workload The waste Typical tools
⚙️ Code generators Regenerating whole trees when no schema changed. Protobuf, OpenAPI, GraphQL, FlatBuffers
🧹 Linters & formatters Re-scanning thousands of untouched files. ESLint, Prettier, Clippy, Black, Ruff
📦 Asset bundlers Rebuilding unchanged style and script trees. Webpack, esbuild, Tailwind, PostCSS
🔨 Compilers Recompiling the same sources across branches and CI jobs. C/C++, Rust, Go, TypeScript

🔌 How it fits

dcc is a wrapper. It doesn't replace your tools or ask you to change your build system. Put dcc exec in front of a command, declare what the command reads and writes, and Reforge handles the rest.

dcc exec -i src/schema.proto -o gen/schema.rs -- protoc --rust_out=gen src/schema.proto

🚦 Is it right for your task?

👍 A good fit 👎 Not a good fit
The output depends only on declared files, arguments and environment variables.
The command is slow compared to hashing its inputs.
You re-run it often with unchanged inputs.
You want the same caching across several tools.
The output depends on the clock, randomness or the network.
The command reads files you can't list up front.
It's already fast, so hashing costs about as much as running it.
It has side effects beyond its declared outputs, such as deploys or database writes.

Note

Many tools have their own caches, such as ESLint's --cache or Cargo's incremental compilation. Reforge doesn't compete with them. It caches the whole command's result at a layer above them, using the same rules for every tool. Cache hits are only as safe as your declarations, as described in the Core Guarantee.


⚔️ Performance & Comparison

 ┌─────────────────────────────────────────────────────────────────────────────┐
 │  WITHOUT REFORGE     ████████████████████████████████████  full command run │
 │  (every time)                                                               │
 ├─────────────────────────────────────────────────────────────────────────────┤
 │  REFORGE · MISS      ████████████████████████████████████▒ run + store      │
 │  (first time)                                                               │
 ├─────────────────────────────────────────────────────────────────────────────┤
 │  REFORGE · HIT       █▒                                    hash + restore   │
 │  (unchanged inputs)                                                         │
 └─────────────────────────────────────────────────────────────────────────────┘
   
   Illustrative, not to scale. ▒ = Reforge's overhead. Hit cost grows with input/output size.

A miss costs the command's runtime plus a small overhead to store the result. A hit swaps the command's runtime for hashing, verifying and restoring. That's a big win for slow commands and little or none for fast ones.


🧭 Capability comparison

Capability ⏱️ Timestamp scripts 🔧 Single-tool caches ⬢ Reforge
🔒 Content-based change detection
SHA-256 of inputs, command, env, tool, platform
❌◐✅
🌐 Works with any command
one wrapper for every tool
◐❌✅
🏎️ Single-flight execution
racing runners wait and replay one result
❌◐✅
🛡️ Corruption detection + quarantine
verify on read, recompute on failure
❌◐✅
🧹 Bounded disk use
LRU, LFU or FIFO with garbage collection
❌◐✅
🔐 Output path confinement
canonical paths, symlink-escape rejection
❌◐✅
💻 Native on Windows, macOS, Linux ◐◐✅
☁️ Shared remote cache
one cache across machines
❌◐❌

✅ built in  ·  ◐ varies by tool or partial  ·  ❌ not provided. This compares approaches, not specific products.


🔬 Measure it on your own project

Hit time depends on how many bytes are hashed and restored, so results on your project matter more than any generic number. hyperfine makes it a few commands.

Install hyperfine:

cargo install hyperfine

Create the demo file (skip this if you already did the Quick Start):

echo 'fn main() { println!("hello from a cached build"); }' > hello.rs

🥶 Cold run: every run is a miss

Wipes the cache before each run, so dcc always executes the command.

Linux / macOS:

hyperfine --prepare 'rm -rf .dcc' 'dcc exec -i hello.rs -o hello -- rustc hello.rs -o hello'

Windows:

hyperfine --prepare 'rmdir /s /q .dcc' 'dcc exec -i hello.rs -o hello.exe -- rustc hello.rs -o hello.exe'

🔥 Warm run: every run is a hit

One warm-up run fills the cache, then every timed run is a replay.

Linux / macOS:

hyperfine --warmup 1 'dcc exec -i hello.rs -o hello -- rustc hello.rs -o hello'

Windows:

hyperfine --warmup 1 'dcc exec -i hello.rs -o hello.exe -- rustc hello.rs -o hello.exe'

📏 Baseline: the same command without Reforge

Linux / macOS:

hyperfine 'rustc hello.rs -o hello'

Windows:

hyperfine 'rustc hello.rs -o hello.exe'

Tip

Compare the three results. The warm run should be far faster than the baseline, and the cold run should be slightly slower than the baseline (the extra time is the cost of storing the result). To test your own command, replace hello.rs, hello and the rustc part with your real inputs, outputs and command.

Note

A tiny demo like this will flatter the hit time. For numbers you can quote, use a real project with realistic input and output sizes.

📊 Results table (fill in after you run the commands)
Workload Baseline Reforge miss Reforge hit Speedup
your workload ms ms ms ×

Include your machine (CPU, RAM, disk, OS), the Reforge version, and the number of runs.


⚖️ Honest limits

  • Not always faster. For commands that finish in milliseconds, hashing can cost as much as running them.
  • Local only. There's no shared remote cache across machines.
  • As safe as your declarations. See the Core Guarantee.

🚀 Quick Start Guide

Install, cache a command, and see a hit in about two minutes.

①

Install
build the dcc binary

②

Cache a command
first run is a miss

③

Replay it
second run is a hit

④

Inspect & manage
stats, verify, prune

1️⃣ Install

Requires: Rust 1.77 or newer, and Git.

Clone the repository:

git clone https://github.com/Ashish6298/Reforge.git

Enter the project folder:

cd Reforge

Install dcc onto your PATH:

cargo install --path crates/cache-cli

Check it worked:

dcc --help
Prefer not to install? Build and run in place

Build:

cargo build --release --workspace

Run (Linux / macOS):

./target/release/dcc --help

Run (Windows):

.\target\release\dcc.exe --help

2️⃣ Cache your first command

This demo compiles a tiny Rust file, so it needs nothing beyond the toolchain you just installed. Run these in any empty folder.

Create the source file:

echo 'fn main() { println!("hello from a cached build"); }' > hello.rs

First run: cache MISS. The command really runs, and dcc stores the result.

dcc exec -i hello.rs -o hello -- rustc hello.rs -o hello

Delete the output (Linux / macOS):

rm hello

Delete the output (Windows):

del hello.exe

Second run: cache HIT. Run the exact same command again. rustc never runs, and dcc restores hello from the store.

dcc exec -i hello.rs -o hello -- rustc hello.rs -o hello

Run the restored program (Linux / macOS):

./hello

Run the restored program (Windows):

.\hello.exe

You should see:

hello from a cached build

On Windows, use -o hello.exe and -o hello.exe in place of hello in the dcc exec and rustc parts. Exact log lines vary by version, and dcc stats shows the hit and miss counts.

Tip

Prove that invalidation works. Change one character in hello.rs and run the dcc exec command again. It becomes a miss, because the input hash changed. Revert the edit and it's a hit again.


3️⃣ Use it on a real command

The flags mean:

Flag Meaning Rule of thumb
-i, --input A file or directory the command reads. Repeat the flag for more. List everything it reads.
-o, --output A file or directory the command writes. These are captured and restored. List everything it produces.
-e, --env An environment variable that affects the result. Add flags, targets and toggles.

Protobuf example. Declare the schema and every file it imports:

dcc exec -i proto/ -o gen/ -- protoc --proto_path=proto --rust_out=gen proto/schema.proto

Warning

A hit is only as safe as your declarations. If the command reads a file you didn't pass with -i, changing that file won't invalidate the cache and you'll get a stale result. Above, the whole proto/ directory is declared so imported files count. See the Core Guarantee.


4️⃣ Check health and manage storage

Show usage, hit/miss counts and capacity:

dcc stats

Scan for corrupted objects:

dcc verify

Quarantine and recover bad entries:

dcc verify --repair

Preview what pruning would remove (always do this first):

dcc prune --strategy lru --max-size-mb 500 --dry-run

Enforce the size limit:

dcc prune --strategy lru --max-size-mb 500

Strategies: lru, lfu, fifo.


🩺 Troubleshooting

Symptom Likely cause and fix
Always a miss Something in the key changes every run: a timestamp in an input, a generated file listed under -i, or a changing environment variable in -e.
A hit, but the result looks stale The command reads something you didn't declare. Add it with -i or -e.
Outputs missing after a hit The command wrote files that weren't declared with -o, so they weren't captured.
dcc: command not found Cargo's bin directory isn't on your PATH (usually ~/.cargo/bin), or you skipped the install step.

🏛️ Architecture Overview

Reforge is a Rust workspace of six crates. Each one has a single job, and the layers depend on each other in one direction only. Hashing knows nothing about storage, storage knows nothing about processes, and the CLI is a thin shell over the rest.

flowchart TB
    CLI["<b>dcc-cli</b><br/>command-line interface"]
    RUN["<b>dcc-runner</b><br/>process execution, locking, replay"]
    INT["<b>dcc-integrations</b><br/>Cargo · Clang/CMake · npm/esbuild · pytest"]
    STO["<b>dcc-storage</b><br/>CAS, eviction, quarantine, GC"]
    COR["<b>dcc-core</b><br/>cache keys, SHA-256, fingerprints"]
    TST["<b>dcc-test-utils</b><br/>failure injection, test harness"]

    CLI --> RUN
    RUN --> STO
    RUN --> INT
    RUN --> COR
    STO --> COR
    TST -. "used by tests" .-> RUN
    TST -. "used by tests" .-> STO
Loading

Arrows mean "depends on". Dotted arrows are test-only dependencies.


🧱 The six crates

Crate Folder Responsibility
🧬 dcc-core
foundation
cache-core Domain models and CacheKey composition. SHA-256 hashing, tool fingerprinting, environment normalization and platform definitions. It has no knowledge of disks or processes.
🗄️ dcc-storage
persistence
cache-storage The on-disk content-addressed store. Chunked metadata persistence, atomic file staging, LRU / LFU / FIFO eviction, the quarantine ledger and garbage collection of unreferenced objects.
⚙️ dcc-runner
execution
cache-runner Runs the command and captures stdout, stderr, exit code and file changes. Enforces path confinement, restores outputs atomically, and handles cross-process single-flight locking.
💻 dcc-cli
interface
cache-cli The dcc command. Colored output for people and JSON output for scripts.
🔌 dcc-integrations
tool adapters
cache-integrations Adapters and automated speedup benchmarks for Cargo, Clang/CMake, npm/esbuild and Python pytest.
🧪 dcc-test-utils
testing
cache-test-utils Multi-OS test harness, chaos failure injectors, corrupted-byte injectors and mock execution environments.

🔁 Life of one dcc exec call

This shows which crate does the work at each step of the run flow.

# What happens Crate
1Parse flags and start the run.dcc-cli
2Hash inputs, command, declared env vars, tool binary and platform into a CacheKey.dcc-core
3Look up the key and verify stored objects against their digests. A failed check moves the entry to quarantine.dcc-storage
4On a miss, take the per-key lock, check again, then execute and capture the result.dcc-runner
5Write the result into the store and apply eviction limits.dcc-storage
6Confine output paths to the workspace, then stage and rename them into place.dcc-runner

🧭 Where to look when you want to change something

I want to… Start in
Change what goes into the cache keycrates/cache-core
Add an eviction strategy or change the store layoutcrates/cache-storage
Change locking, sandboxing or output restorationcrates/cache-runner
Add a CLI flag or output formatcrates/cache-cli
Add support or benchmarks for another toolcrates/cache-integrations
Simulate a crash or corrupted file in a testcrates/cache-test-utils

🧪 Test one crate at a time

Core:

cargo test -p dcc-core

Storage:

cargo test -p dcc-storage

Runner:

cargo test -p dcc-runner

📦 Content-Addressable Storage (CAS)

 ┌──────────────────────────────────────────────────────────────────────────────┐
 │                                                                              │
 │    CacheKey ──────►  ENTRY  ───────►  OBJECT   OBJECT   OBJECT               │
 │   (what you ran)    (the recipe)     (stdout)  (stderr)  (output file)       │
 │                                                                              │
 │                     entries/9f/…      objects/3a/…  objects/e7/…             │
 │                                                                              │
 └──────────────────────────────────────────────────────────────────────────────┘
      An entry says WHAT a run produced. Objects hold the bytes themselves.

The store keeps two kinds of records. Objects hold raw bytes and are named after the SHA-256 of those bytes. Entries map a cache key to the list of object digests a run produced. Splitting them is what makes de-duplication and verification cheap.


🗂️ On-disk layout

.dcc/
├── cas/
│   ├── objects/
│   │   ├── 3a/
│   │   │   └── 3a89f4b…      raw output payload bytes
│   │   └── e7/
│   │       └── e712c90…      raw output payload bytes
│   ├── entries/
│   │   └── 9f/
│   │       └── 9f01ab2…      metadata: cache key → object digests
│   └── quarantine/
│       └── corrupted_entries/
└── locks/
    └── <key_hash>.lock       one advisory lock per cache key
Path What lives there Addressed by
cas/objects/ Raw bytes: output files, stdout, stderr. SHA-256 of the content
cas/entries/ Metadata linking one cache key to the digests of its objects. The cache key
cas/quarantine/ Entries that failed verification, kept for inspection. Never replayed
locks/ One lock file per key, for single-flight execution. The cache key

Objects and entries sit in subfolders named after the first two characters of their hash (3a/, 9f/). That keeps any one folder from holding a huge number of files.


♻️ De-duplication in practice

Because an object's name is its content hash, two tasks that produce the same bytes share one file, no matter what the tasks were.

flowchart LR
    E1["📄 Entry A<br/>protoc run"] --> O1[("🧱 3a89f4b…<br/>schema.rs")]
    E2["📄 Entry B<br/>same schema,<br/>different branch"] --> O1
    E2 --> O2[("🧱 e712c90…<br/>build.log")]
    E3["📄 Entry C<br/>another tool"] --> O2
Loading

Entries A and B both point at 3a89f4b…, so its bytes are stored once. Removing one entry doesn't delete an object another entry still uses, and garbage collection removes an object only when nothing refers to it.


✍️ Write path: readers never see half a file

sequenceDiagram
    autonumber
    participant W as Writer
    participant T as Temp file (same filesystem)
    participant S as objects/3a/3a89f4b…
    participant R as Reader

    W->>T: write all bytes
    W->>T: hash the bytes
    W->>S: rename into place (atomic)
    Note over T,S: Same filesystem, so the rename is a single step
    R->>S: open
    S-->>R: complete blob, or nothing yet
Loading
Step Why it's done this way
Write to a temp file first A crash mid-write can only leave a stray temp file, never a damaged object under a real name.
Temp file on the same filesystem A rename within one filesystem is atomic. Across filesystems it turns into a copy, which isn't.
Rename into the final path At every moment, a reader sees either no object or the complete one.

🔍 Read path: verify before you trust

flowchart LR
    A([Read object]) --> B[Load bytes]
    B --> C{SHA-256 of bytes<br/>equals filename?}
    C -- "yes" --> D[/✅ Use it/]
    C -- "no" --> E[/🛡️ Quarantine<br/>and recompute/]
Loading

An object's name is the hash of what it should contain, so the check needs no extra bookkeeping. It re-hashes the bytes and compares. If a disk fault, an interrupted write or tampering changed even one byte, the hash won't match, and the bytes are never restored into your workspace.


🧭 What the design gives you

Property How the store provides it Result
💾 Space efficiency Identical content maps to one object path. Duplicate outputs cost disk once.
⚛️ Atomic writes Temp file plus rename on the same filesystem. No partial blobs are ever visible.
✅ Self-verifying reads The filename is the expected hash. Corruption is detected on every read.
🔒 Immutable objects A name always means the same bytes. Changing content changes the name. No in-place edits to go wrong.
🧹 Safe cleanup Entries are separate from objects, so unreferenced objects can be found and removed. Eviction and GC don't break live entries.

🔎 Explore your own store

See usage, hit and miss counts, and capacity:

dcc stats

Machine-readable stats:

dcc stats --json

Check every object against its digest:

dcc verify

List quarantined entries:

dcc quarantine list

Browse the objects folder (Linux / macOS):

ls .dcc/cas/objects

Browse the objects folder (Windows):

dir .dcc\cas\objects

Note

Treat .dcc/ as managed by dcc. Don't edit or delete files inside it by hand. Use dcc prune to shrink the cache and dcc verify to check it. A hand-edited object fails its hash check, and the entry that used it is quarantined.


🔒 Invalidation Engine & Cryptographic Hashing

 ┌──────────────────────────────────────────────────────────────────────────────┐
 │                                                                              │
 │    📄 inputs   ⌨️ command   🌱 env   🔧 tool   💻 platform                 │
 │        │           │          │         │          │                         │
 │       H( )        H( )       H( )      H( )       H( )     one hash each     │
 │        └───────────┴──────────┼─────────┴──────────┘                         │
 │                               ▼                                              │
 │                        SHA-256( all five )                                   │
 │                               │                                              │
 │                               ▼                                              │
 │                    🔑 CacheKey  9f01ab2c…                                    │
 │                                                                              │
 └──────────────────────────────────────────────────────────────────────────────┘
      Same five vectors in, same key out. Change any one, and the key changes.

A CacheKey is a fingerprint of one computation. Two runs get the same key only when everything that could affect the result is the same. That's how Reforge decides between "replay the stored result" and "run it again".

$$\text{CacheKey} = \text{SHA-256}\Big(\mathcal{H}(\text{Inputs}) \parallel \mathcal{H}(\text{Command}) \parallel \mathcal{H}(\text{Env}) \parallel \mathcal{H}(\text{Tools}) \parallel \mathcal{H}(\text{Platform})\Big)$$

Each vector is hashed on its own, and the five digests are joined and hashed again. Because every digest has the same fixed width, one vector's bytes can't blur into the next.


🧬 The five vectors

Vector What goes into the hash Invalidates when Does not invalidate when
📄 1. Inputs
-i
Normalized relative paths plus the content hash of each input file. A declared file's bytes change, or a file is added, removed or renamed. Only the file's timestamp changes (for example after a fresh git checkout).
⌨️ 2. Command The program name and the ordered list of arguments. Any argument is added, removed, changed or reordered. Nothing changes about the command line.
🌱 3. Environment
-e
The key and value of each declared environment variable. A declared variable's value changes, or it is set or unset. An undeclared variable changes.
🔧 4. Tool The content hash of the executable binary, or its version string. The tool is upgraded, downgraded or rebuilt. The tool is reinstalled with identical bytes.
💻 5. Platform OS, architecture, pointer width and endianness. The same command runs on a different OS or CPU architecture. The same platform, run again.

🔄 How the key is built

flowchart TB
    subgraph IN["Ingest"]
        A["📄 Input files<br/>normalized path + content hash"]
        B["⌨️ Command<br/>binary name + ordered args"]
        C["🌱 Environment<br/>declared key/value pairs"]
        D["🔧 Tool<br/>binary hash / version"]
        E["💻 Platform<br/>OS · arch · pointer width · endianness"]
    end
    A --> HA["H(Inputs)"]
    B --> HB["H(Command)"]
    C --> HC["H(Env)"]
    D --> HD["H(Tools)"]
    E --> HE["H(Platform)"]
    HA & HB & HC & HD & HE --> K["SHA-256 of all five digests"]
    K --> KEY(["🔑 CacheKey"])
    KEY --> LOOK["Look up in the store"]
Loading

💥 One change, a completely different key

SHA-256 has an avalanche property. Changing a single byte of any vector produces a key with no resemblance to the old one. That's why a key can't be "close" to another key, and why there is no partially stale hit.

  hello.rs   fn main() { println!("hello"); }     →  9f01ab2c 4e7d…   (illustrative)
  hello.rs   fn main() { println!("hellp"); }     →  b83e5d10 a29c…   one character changed

These keys are illustrative. Real keys are 64 hex characters.


🚧 What the key can't see

Warning

The key covers what you declare. Undeclared files the command reads, the current time, random numbers, network responses and undeclared environment variables are all invisible to it. If the output depends on any of them, a hit can be stale. Declare every input with -i and every relevant variable with -e, and don't cache commands whose output isn't deterministic. See the Core Guarantee.

✅ Covered by the key ⚠️ Your responsibility
Contents of files passed with -i
Program name and all arguments, in order
Variables passed with -e
The tool's binary or version
OS, architecture, pointer width, endianness
Files the command reads but you didn't list
Variables the command reads but you didn't list
Anything time-, random- or network-dependent
Commands with side effects beyond their declared outputs

🧪 See each vector in action

Use the hello.rs demo from the Quick Start. Run each experiment after a first, unchanged run, so there is a cache entry to compare against.

Baseline run (fills the cache):

dcc exec -i hello.rs -o hello -- rustc hello.rs -o hello

📄 Input change → miss

Edit the source (Linux / macOS):

echo 'fn main() { println!("changed"); }' > hello.rs

Edit the source (Windows):

Set-Content hello.rs 'fn main() { println!("changed"); }'

Run again:

dcc exec -i hello.rs -o hello -- rustc hello.rs -o hello

Expect a miss, because the file's content hash changed. Restore the original text and run again for a hit.

⌨️ Command change → miss

Add an argument:

dcc exec -i hello.rs -o hello -- rustc hello.rs -o hello -O

Expect a miss, because the argument list is part of the key.

🌱 Declared variable change → miss

Run with a declared variable (Linux / macOS):

MODE=a dcc exec -i hello.rs -o hello -e MODE -- rustc hello.rs -o hello

Change its value (Linux / macOS):

MODE=b dcc exec -i hello.rs -o hello -e MODE -- rustc hello.rs -o hello

Run with a declared variable (Windows):

$env:MODE="a"; dcc exec -i hello.rs -o hello.exe -e MODE -- rustc hello.rs -o hello.exe

Change its value (Windows):

$env:MODE="b"; dcc exec -i hello.rs -o hello.exe -e MODE -- rustc hello.rs -o hello.exe

Expect a miss on the second run, because MODE is declared and its value changed.

🙈 Undeclared variable change → still a hit

Change a variable you did not declare (Linux / macOS):

OTHER=1 dcc exec -i hello.rs -o hello -- rustc hello.rs -o hello

Expect a hit. OTHER isn't in the key, so changing it changes nothing. This is the boundary described above, made visible.

Check the outcome of any run with dcc stats. The exact log wording depends on the version.


📐 Design notes

Choice Why
Content hashes, not timestamps Timestamps change on checkouts and fresh clones without the content changing, and can stay the same when content does. Hashing bytes answers the real question.
Hash each vector, then combine Fixed-width digests can't be confused with each other, so a value moving from one vector to another can't produce the same key.
Declared environment only Hashing the whole environment would make the key change with every shell session. Declaring the relevant variables keeps keys stable and puts you in control.
Hash the tool binary A compiler upgrade can change output with identical inputs. Including the tool makes stale results from an old version impossible.
Include the platform The same source can compile to different bytes on different OSes or architectures, so results are never replayed across them.

🏎️ Concurrency & Single-Flight Execution

 ┌──────────────────────────────────────────────────────────────────────────────┐
 │  WITHOUT SINGLE-FLIGHT      three runners, same task                         │
 │                                                                              │
 │    A  ████████████████████                                                   │
 │    B  ████████████████████                                                   │
 │    C  ████████████████████         total work: 3 × the command's runtime     │
 │                                                                              │
 ├──────────────────────────────────────────────────────────────────────────────┤
 │  WITH SINGLE-FLIGHT         same three runners                               │
 │                                                                              │
 │    A  ████████████████████                                                   │
 │    B  ░░░░░░░░░░░░░░░░░░░░▒        total work: 1 × runtime + 2 small replays │
 │    C  ░░░░░░░░░░░░░░░░░░░░▒                                                  │
 │                                                                              │
 └──────────────────────────────────────────────────────────────────────────────┘

     ░ waiting on the lock     ▒ verify + replay     Illustrative, not to scale.

On a CI farm or a parallel build, several workers often ask for the same computation at the same moment. Without coordination, they all miss the cache together and all do the same work (the "thundering herd"). Reforge prevents this with single-flight execution: for each cache key, exactly one runner executes the command, and everyone else waits and reuses its result.


🎬 Three runners, one key

sequenceDiagram
    autonumber
    participant A as Runner A
    participant B as Runner B
    participant C as Runner C
    participant L as Lock (this key)
    participant S as Store

    A->>S: look up key
    B->>S: look up key
    C->>S: look up key
    S-->>A: miss
    S-->>B: miss
    S-->>C: miss
    A->>L: acquire
    L-->>A: granted
    B->>L: acquire
    C->>L: acquire
    Note over B,C: blocked, waiting for A
    A->>A: execute the command
    A->>S: commit the entry
    A->>L: release
    L-->>B: granted
    B->>S: check again
    S-->>B: hit
    L-->>C: granted
    C->>S: check again
    S-->>C: hit
    B->>B: replay outputs
    C->>C: replay outputs
Loading

Waiters take the lock one at a time. Each one re-checks the store first, finds A's entry, and never runs the command.


🧩 The three mechanisms

Mechanism How it works What it prevents
🔐 Per-key advisory lock Each cache key has its own lock file in .dcc/locks/. The OS grants it to one runner at a time and makes the others wait. Duplicate execution of the same task across processes.
🔁 Double-checked lookup After acquiring the lock, a runner looks in the store again before deciding to execute. A waiter re-running work that the previous lock holder just finished.
🧯 Crash-safe release The OS releases the lock when its holder's process ends, including after kill -9. Stale locks and deadlocks after a worker dies mid-run.

🧭 What each runner decides

flowchart TB
    A([dcc exec]) --> B{Entry in store<br/>and verified?}
    B -- "yes" --> R1[/✅ Replay outputs/]
    B -- "no" --> L["Acquire per-key lock<br/>(waits if another runner holds it)"]
    L --> C{Entry appeared<br/>while waiting?}
    C -- "yes" --> U1[Release lock] --> R2[/✅ Replay outputs<br/>command never runs/]
    C -- "no" --> X["Execute the command"]
    X --> S["Commit entry to the store"]
    S --> U2[Release lock] --> R3[/📦 Restore outputs/]
Loading

The first lookup makes ordinary hits fast, since they never touch the lock. The second one, after the lock is granted, is what makes the race safe.


🔁 Why the second check matters

Between "I saw a miss" and "I got the lock", another runner may have finished the job.

❌ Without the re-check ✅ With the re-check
B sees a miss and waits for the lock.
A finishes and releases it.
B gets the lock and runs the command again, even though A's result is sitting in the store.
B sees a miss and waits for the lock.
A finishes and releases it.
B gets the lock, looks again, finds A's entry, and replays it.

🛟 When things go wrong

Situation What happens
The lock holder is killed (kill -9, crash, power loss) The OS releases the lock. The next waiter gets it, re-checks, finds nothing stored, and executes the command itself. No manual cleanup.
A lock file is left on disk Harmless. The file is only a name for the OS lock, and it isn't "held" unless a live process holds it.
Runners want different keys They use different locks, so they run in parallel and never block each other.
A stored object fails its checksum The entry is quarantined and treated as a miss, so the lock holder re-executes.

📏 Scope and limits

Note

Advisory locks coordinate cooperating processes. They stop dcc runners from stepping on each other. They don't stop an unrelated program from touching the same files. Lock behaviour also depends on the filesystem, and locking on network filesystems (NFS, SMB) can be unreliable, so test before sharing one .dcc/ directory across machines. Coordination is local to one cache directory. It doesn't span separate caches or separate hosts.

✅ Covered ⚠️ Not covered
Many dcc processes on one machine sharing a cache directory
CI jobs running in parallel against the same workspace cache
Crash of any runner at any point
Runners on different machines with separate caches
Non-dcc programs writing into the same files
Shared network filesystems with unreliable locking

🧪 See it happen

A slow command makes the effect visible. This one takes about five seconds. Run it from an empty folder.

Create an input file:

echo data > input.txt

🐧 Linux / macOS

Start three identical runs at the same time:

for i in 1 2 3; do dcc exec -i input.txt -o out.txt -- sh -c "sleep 5 && echo done > out.txt" & done; wait

Check the counts:

dcc stats

The three runs should finish in about five seconds total, not fifteen. dcc stats should show one miss and two hits.

🪟 Windows

Run this in three PowerShell windows within a second or two of each other:

dcc exec -i input.txt -o out.txt -- powershell -Command "Start-Sleep 5; 'done' | Out-File out.txt"

Check the counts:

dcc stats

Reset between attempts (Linux / macOS):

rm -rf .dcc out.txt

Reset between attempts (Windows):

Remove-Item -Recurse -Force .dcc, out.txt

🔥 Stress test

The repository includes a test that races many processes on the same key.

Run the concurrency suite:

cargo test -p dcc-runner --test concurrency_tests

⚙️ Multi-Strategy Eviction & Garbage Collection

 ┌──────────────────────────────────────────────────────────────────────────────┐
 │                                                                              │
 │   DISK BUDGET   ██████████████████████████████████████░░░░░░░   over limit   │
 │                                                                              │
 │        │  1. EVICT    pick entries by strategy (LRU · LFU · FIFO)            │
 │        ▼                                                                     │
 │   DISK BUDGET   ████████████████████████░░░░░░░░░░░░░░░░░░░░░░   entries gone │
 │                                                                              │
 │        │  2. GC       delete objects no entry refers to anymore              │
 │        ▼                                                                     │
 │   DISK BUDGET   ██████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░   under limit │
 │                                                                              │
 └──────────────────────────────────────────────────────────────────────────────┘

           Illustrative, not to scale.  █ used   ░ free

A cache that only grows will eventually fill your disk. Reforge keeps it within a size limit you set, in two stages. Eviction chooses which entries to drop. Garbage collection then deletes the objects that no remaining entry uses. Splitting the work this way is what makes it safe when entries share objects (see CAS de-duplication).


🎯 The three strategies

Strategy Evicts first Best when Watch out for
🕐 LRU
Least Recently Used
The entry with the oldest last-access time. Work comes in phases: what you used lately is what you'll use next. A good default for developer machines. A long-idle entry that is normally popular can still be evicted.
📊 LFU
Least Frequently Used
The entry with the fewest total accesses. A stable set of tasks is reused constantly, such as shared CI caches. New entries start with few accesses, so they can be evicted before they have had a chance to prove useful.
📥 FIFO
First In, First Out
The entry with the oldest creation time. You want simple, predictable turnover and don't care about usage. It ignores use entirely, so a very popular entry can be evicted just because it's old.

🧮 Same cache, three different victims

Five entries, and the cache is over its limit. Each strategy ranks them differently.

Entry Created Last used Times used Note
Aday 1day 92oldest entry
Bday 2day 340popular, but idle
Cday 3day 85
Dday 4day 41used once
Eday 5day 1012recent and used
Strategy Eviction order (first to go on the left) First victim
🕐 LRU B → D → C → A → E B: nothing has touched it since day 3, despite its 40 uses.
📊 LFU D → A → C → E → B D: it has been used only once.
📥 FIFO A → B → C → D → E A: it was created first, though it was used recently.

Example data is made up to show the difference. Entries are removed in order until the cache is back under its limit.


🔄 What a prune does

flowchart TB
    A([dcc prune]) --> B{Cache over<br/>the size limit?}
    B -- "no" --> Z[/Nothing to evict/]
    B -- "yes" --> C["Rank entries by the chosen strategy<br/>LRU · LFU · FIFO"]
    C --> D["Remove entries one by one,<br/>starting with the first victim"]
    D --> E{Under the limit<br/>yet?}
    E -- "no" --> D
    E -- "yes" --> F["Garbage-collect objects<br/>that no entry refers to"]
    F --> G[/✅ Cache within budget/]
Loading

🧹 Garbage collection: why it's a separate step

Removing an entry doesn't automatically free its objects. Another entry may still need them.

flowchart LR
    subgraph BEFORE["Before eviction"]
        EA["📄 Entry A"] --> O1[("🧱 3a89f4b…")]
        EB["📄 Entry B"] --> O1
        EB --> O2[("🧱 e712c90…")]
    end
    subgraph AFTER["After evicting Entry B, then GC"]
        EA2["📄 Entry A"] --> O1b[("🧱 3a89f4b…<br/>kept: A still uses it")]
        O2b[("🧱 e712c90…<br/>deleted: nothing refers to it")]
    end
    BEFORE --> AFTER
Loading
Object state What GC does
Referenced by at least one entry Keeps it, even if other entries that shared it were evicted.
Referenced by no entry Deletes it and frees the disk space.

🚪 Pre-insertion capacity check

Before storing a new object, Reforge checks it against the capacity limit. An object larger than the whole limit is rejected up front, and nothing else is disturbed.

❌ Without the check ✅ With the check
An oversized object arrives.
Eviction removes entry after entry trying to make room.
The object still doesn't fit, and the useful cache is gone for nothing.
An oversized object arrives.
It's rejected immediately.
The existing cache is untouched.

🧭 Picking a strategy

Your situation Try
Personal machine, work moves between projects and brancheslru
CI cache, the same tasks recur constantlylfu
You want the simplest, most predictable turnoverfifo
Not surelru, then check dcc stats and compare hit rates

🛠️ Manage your cache

See how full the cache is:

dcc stats

Preview a prune without deleting anything (always do this first):

dcc prune --strategy lru --max-size-mb 500 --dry-run

Prune with LRU:

dcc prune --strategy lru --max-size-mb 500

Prune with LFU:

dcc prune --strategy lfu --max-size-mb 500

Prune with FIFO:

dcc prune --strategy fifo --max-size-mb 500

Check the result:

dcc stats

Tip

Run the --dry-run command with each strategy and compare what each would remove. It's a safe way to see the differences from the table above on your own data.


📏 Limits and behaviour to know

Note

Eviction removes cached results, never your source files or real outputs. An evicted entry just turns into a miss the next time it's needed, and the command runs again. Eviction costs time, not correctness. A smaller limit means more misses, so size it to your working set.

✅ Guaranteed by the design ⚠️ Worth knowing
Objects shared by surviving entries are never deleted
Oversized objects are rejected before insertion
Strategies only change which entries go, not correctness
--dry-run shows the plan before anything is removed
An evicted entry costs a full re-run the next time
LFU can penalize brand-new entries
FIFO ignores how useful an entry is
A limit smaller than your working set causes constant churn

🛡️ Self-Healing & Quarantine

 ┌──────────────────────────────────────────────────────────────────────────────┐
 │                                                                              │
 │    📖 READ        🔍 CHECK           🚧 ISOLATE         ♻️ RECOVER         │
 │                                                                              │
 │   load object ──► SHA-256 matches ──► ✅ use it                              │
 │        │          the filename?                                              │
 │        │                                                                     │
 │        └────────► ❌ mismatch ──────► move to quarantine ──► re-run command  │
 │                                                          └─► store fresh     │
 │                                                              entry           │
 │                                                                              │
 └──────────────────────────────────────────────────────────────────────────────┘
      A bad entry costs one extra run. It never reaches your workspace.

Disks fail, power drops mid-write, and files get edited by accident. A cache that trusts what it reads would hand you corrupted output and call it a hit. Reforge doesn't trust the disk: every object is checked against its own hash before it's used. When a check fails, the entry is set aside and the command runs again, so the failure costs you time and not correctness.


🔄 What happens on a failed check

sequenceDiagram
    autonumber
    participant R as Runner
    participant S as Store
    participant Q as Quarantine
    participant C as Your command
    participant W as Workspace

    R->>S: look up key
    S-->>R: entry found
    R->>S: read object, re-hash bytes
    S-->>R: hash does not match the filename ❌
    R->>Q: move the bad entry aside
    Note over R,Q: kept for inspection, never replayed
    R->>R: log a warning
    R->>C: execute fresh
    C-->>R: stdout, stderr, exit code, outputs
    R->>S: store a new, valid entry
    R->>W: restore outputs (atomic)
Loading

🔍 What gets checked

Record How it's verified If it fails
🧱 Objects
cas/objects/
The bytes are re-hashed with SHA-256 and compared to the object's filename, which is its expected hash. No extra bookkeeping is needed. The entry that uses it is quarantined and the command re-runs.
📄 Entries
cas/entries/
Metadata linking a cache key to its object digests is validated when read. Unreadable or inconsistent metadata is handled like a bad object. Quarantined, then recomputed.

🩺 What it protects you from

What went wrong How it shows up Result
Hardware fault or bit rot
a flipped bit on disk
The re-computed hash differs from the filename. Quarantined, re-run.
Power loss or crash mid-write Bytes are cut short. Writes go through a temp file and an atomic rename, so a partial object normally never gets a real name. If one appears anyway, the hash catches it. Quarantined, re-run.
Accidental hand-edit
someone changed a file in .dcc/
The content no longer matches its name. Quarantined, re-run.
Tampering with stored bytes Changed bytes give a different hash. Quarantined, re-run.

🧭 Three promises

🚫 Never restored

Data that fails its check is never written into your workspace. You either get verified bytes or a fresh run.

🗃️ Kept, not destroyed

Bad entries are moved to .dcc/cas/quarantine/, not deleted. You can look at what went wrong, and the build isn't interrupted.

♻️ Recovers by itself

A warning is logged, the command re-runs, and a fresh valid entry replaces the bad one. Nobody has to intervene.

🗂️ Working with quarantined entries

List what's in quarantine:

dcc quarantine list

Scan the whole cache for corrupted objects, without waiting for a read to hit them:

dcc verify

Scan and recover bad entries:

dcc verify --repair

Empty the quarantine once you're done inspecting it:

dcc quarantine clear

Note

dcc quarantine clear deletes the quarantined files for good. If you want to investigate why data went bad, look first and clear afterwards.

Check dcc verify (proactive) Verify on read (automatic)
When it runs When you run it Every time an object is used
What it covers The whole cache Only the objects this run needs
Good for After a crash, a disk scare or a restored backup Everyday safety, with no action from you

🧪 Try it: corrupt an object on purpose

Use the hello.rs demo from the Quick Start. You'll damage a stored object and watch Reforge notice.

Fill the cache with a first run:

dcc exec -i hello.rs -o hello -- rustc hello.rs -o hello

Delete the output, so a replay has to restore it (Linux / macOS):

rm hello

Delete the output, so a replay has to restore it (Windows):

del hello.exe

Corrupt one stored object by appending a byte (Linux / macOS):

echo corrupt >> "$(find .dcc/cas/objects -type f | head -n 1)"

Corrupt one stored object by appending a byte (Windows):

Add-Content (Get-ChildItem .dcc\cas\objects -Recurse -File | Select-Object -First 1).FullName "corrupt"

Run the same command again:

dcc exec -i hello.rs -o hello -- rustc hello.rs -o hello

Expect a warning, then a normal run. The output file is rebuilt from a fresh execution, not from the damaged object.

See the quarantined entry:

dcc quarantine list

Confirm the cache is healthy again:

dcc verify

On Windows, use hello.exe in place of hello in the dcc exec and rustc commands. The exact warning text depends on the version.


📏 Scope and limits

Note

This detects changes. It doesn't stop them. The checksum tells Reforge that stored data no longer


🔒 Security & Directory Escape Prevention

 ┌──────────────────────────────────────────────────────────────────────────────┐
 │                                                                              │
 │    📥 a path arrives            🚧 THREE CHECKS               📁 result     │
 │    (a flag, or a cache                                                       │
 │     entry being restored)                                                    │
 │                                                                              │
 │    ../../etc/passwd   ──►  1. absolute / ../ sanitized  ──►  ❌ rejected     │
 │    link/out.txt       ──►  2. symlinks resolved         ──►  ❌ rejected     │
 │    target/gen/a.rs    ──►  3. canonical path inside     ──►  ✅ written      │
 │                               workspace_dir?                                 │
 │                                                                              │
 └──────────────────────────────────────────────────────────────────────────────┘
      Only paths that end up inside the project workspace are ever written.

When Reforge restores a cached result, it writes files into your project. That is the moment a bad path could do damage: an output declared as ../../somewhere/else, a folder that is secretly a symlink to a system directory, or a damaged or hostile cache entry that lists an absolute path. Reforge checks every output path before writing, and refuses anything that would land outside the workspace.


🧱 The three protections

Protection The attack it stops How it works Outcome
📍 Canonical path containment An output path that reaches outside the project through .. or a roundabout route. Every declared output path is canonicalized (resolved to its one true absolute form) and must lie strictly inside workspace_dir. Rejected
🔗 Symlink escape prevention Writing through a symlinked directory that points at /etc, C:\Windows or any other outside location. Outputs that pass through an intermediate directory symlink to an external location are refused. Rejected
🧼 Absolute path sanitization A malicious or damaged cache entry that lists /etc/cron.d/x or ../../.ssh/authorized_keys as an output. Absolute paths and ../ traversal tokens can't escape the designated root, even when they come from stored metadata and not from your command line. Rejected

🎬 Why canonicalizing first matters

A path that looks safe can point somewhere else once resolved.

Path as written Where it really goes
gen/../../secrets.txt One folder above the project. It starts in gen/ but climbs out.
build/out.txt, where build is a symlink to /etc /etc/out.txt, outside the workspace, although the text looks harmless.
/home/you/.bashrc An absolute path with no relation to the project.

Comparing the text of a path isn't enough. Reforge resolves the real destination first and judges that.


🔄 What happens on each restore

flowchart TB
    A(["Output path<br/>from your flags or a cache entry"]) --> B{"Absolute path or<br/>../ traversal?"}
    B -- "yes" --> X[/"❌ Rejected"/]
    B -- "no" --> C["Resolve to the canonical path,<br/>following symlinks"]
    C --> D{"Inside<br/>workspace_dir?"}
    D -- "no" --> X
    D -- "yes" --> E{"Passes through a directory<br/>symlink to an outside location?"}
    E -- "yes" --> X
    E -- "no" --> F["Stage outputs in a temp location"]
    F --> G[/"✅ Rename into place"/]
Loading

🧭 Threat model

Who or what What Reforge does
A mistaken flag
you typed -o ../out
Rejects the path before anything is written outside the project.
A damaged cache entry Verifies it, and rejects paths that would escape. See Self-Healing & Quarantine.
A hostile cache entry
a crafted entry that lists an escaping path
The path checks apply to restored entries too, so replaying one can't write outside the workspace.
A symlink planted in the workspace Writes that would travel through it to an outside location are refused.

🚧 What this does not do

Important

Reforge confines where it writes restored files. It does not sandbox the command you run. The wrapped command executes with your normal user permissions and can read or write anything you can. Only run commands you trust, exactly as you would without dcc. Path confinement protects the restore step. It isn't process isolation.

✅ Covered ⚠️ Not covered
Output paths that climb out with ../
Absolute output paths
Directory symlinks leading outside the workspace
Hostile paths inside stored cache entries
What the command itself reads, writes or runs
Files the command writes that you didn't declare with -o
Network access by the command
Other users who can write to your .dcc/ directory

✅ Practical guidance

Do Avoid
Keep declared outputs inside the project folder.
Keep .dcc/ owned by you, with normal file permissions.
Review commands before wrapping them in dcc exec.
Run dcc verify after restoring a cache from backup or another source.
Sharing a writable .dcc/ with users or processes you don't trust.
Copying a cache from an untrusted source and running with it as-is.
Assuming dcc limits what the wrapped command can touch.

🧪 Try it: watch an escape get refused

Run these in an empty folder. Every command that should be refused is a safe test, because it uses harmless commands and creates nothing.

Create an input file:

echo data > input.txt

🚫 Traversal: ../ in an output path

Declare an output outside the workspace:

dcc exec -i input.txt -o ../escaped.txt -- echo test

Expect a rejection with an error about the path leaving the workspace. Nothing should be created outside the folder.

Confirm nothing was written (Linux / macOS):

ls ../escaped.txt

Confirm nothing was written (Windows):

dir ..\escaped.txt

Both should report that the file doesn't exist.

🚫 Absolute path

Declare an absolute output path (Linux / macOS):

dcc exec -i input.txt -o /tmp/dcc-escape-test.txt -- echo test

Declare an absolute output path (Windows):

dcc exec -i input.txt -o C:\Windows\Temp\dcc-escape-test.txt -- echo test

Expect a rejection.

🚫 Symlink escape

Create a folder outside the workspace and a symlink to it (Linux / macOS):

mkdir ../outside && ln -s ../outside link

Declare an output through the symlink:

dcc exec -i input.txt -o link/out.txt -- echo test

Check the outside folder stayed empty:

ls ../outside

Expect a rejection, and an empty folder.

Windows: creating a symlink needs Developer Mode or an elevated prompt.

mkdir ..\outside; cmd /c mklink /D link ..\outside

Then run the same dcc exec command as above, and check with dir ..\outside.

The exact error wording depends on the version. What matters is that the run is refused and nothing appears outside the workspace.


🔬 Run the security test suite

Run the security tests:

cargo test -p dcc-runner --test security_tests

💻 CLI Command Reference

 ┌──────────────────────────────────────────────────────────────────────────────┐
 │                                                                              │
 │   dcc                                                                        │
 │    ├── exec         run a command with caching                               │
 │    ├── stats        show usage, hit/miss counts, capacity                    │
 │    ├── verify       check every stored object against its hash               │
 │    ├── prune        enforce a size limit (LRU · LFU · FIFO)                  │
 │    └── quarantine                                                            │
 │         ├── list    show entries that failed verification                    │
 │         └── clear   delete them for good                                     │
 │                                                                              │
 └──────────────────────────────────────────────────────────────────────────────┘

🧭 At a glance

Command What it does Changes data? Jump to
dcc exec Runs a command, or replays its stored result. Yes (adds entries, writes outputs) ↓
dcc stats Shows cache usage, hit and miss counts, and capacity. No ↓
dcc verify Scans the cache for corrupted objects. Only with --repair ↓
dcc prune Removes entries until the cache fits a size limit. Yes (use --dry-run first) ↓
dcc quarantine Lists or clears entries that failed verification. list no · clear yes ↓

⚡ dcc exec: run a command with caching

Syntax:

dcc exec [OPTIONS] -- <COMMAND> [ARGS]...
  dcc exec   -i src/   -o gen/   -e MODE   --dir app   --   protoc --rust_out=gen a.proto
             │         │         │          │             │   └── everything after -- is your command
             │         │         │          │             └────── the separator: dcc's flags end here
             │         │         │          └──────────────────── where to run the command
             │         │         └─────────────────────────────── env variables that affect the result
             │         └───────────────────────────────────────── files the command writes
             └─────────────────────────────────────────────────── files the command reads

Important

Always put -- before your command. It tells dcc where its own options end. Without it, flags meant for your command (like -o for rustc) could be read as dcc options.


🎛️ Options

Option Meaning Part of the cache key?
-i, --input <PATH> A file or directory the command reads. Repeat the flag for more than one. Yes. Contents are hashed.
-o, --output <PATH> A file or directory the command writes. Captured after a run, restored on a hit. No. These are what gets stored.
-e, --env <VAR> An environment variable whose value affects the result. Yes. Name and value are hashed.
--dir <PATH> The working directory to run the command in. Defaults to the current directory. See the checklist below.
--no-stdout Don't capture or replay standard output. See the checklist below.
--no-stderr Don't capture or replay standard error. See the checklist below.

Besides declared outputs, dcc also stores and replays the command's stdout, stderr and exit code, unless you turn those off. See how the key is built.


📚 One example per option

Minimal: one input, one output:

dcc exec -i hello.rs -o hello -- rustc hello.rs -o hello

Several inputs (repeat -i):

dcc exec -i main.cpp -i include/ -o main.o -- clang++ -O3 -c main.cpp -o main.o

Several outputs (repeat -o):

dcc exec -i proto/ -o gen/ -o docs/api.md -- ./generate.sh

A directory as an input (everything inside it is hashed):

dcc exec -i src/ -o dist/ -- npx esbuild src/index.ts --bundle --outdir=dist

Declare an environment variable that affects the result:

dcc exec -i src/ -o dist/ -e NODE_ENV -- npx esbuild src/index.ts --bundle --outdir=dist

Declare several variables (repeat -e):

dcc exec -i src/ -o dist/ -e NODE_ENV -e API_URL -- ./build.sh

Run in a different directory:

dcc exec --dir app -i src/ -o dist/ -- npm run build

Don't replay standard output:

dcc exec --no-stdout -i input.txt -o out.txt -- ./process.sh

Don't replay standard error:

dcc exec --no-stderr -i input.txt -o out.txt -- ./process.sh

Turn off both:

dcc exec --no-stdout --no-stderr -i input.txt -o out.txt -- ./process.sh

Warning

A hit is only as safe as your declarations. Every file the command reads belongs under -i, every variable that changes its result belongs under -e, and every file it writes belongs under -o. See the Core Guarantee.


📊 dcc stats: see what's in the cache

Syntax:

dcc stats [--json]

Human-readable summary:

dcc stats

Machine-readable output for scripts:

dcc stats --json

Pretty-print the JSON with jq, if you have it installed:

dcc stats --json | jq .

Use it to check hit and miss counts, see how full the cache is against its limit, and confirm that a run behaved the way you expected.


🔍 dcc verify: check integrity

Syntax:

dcc verify [--repair]

Scan the whole cache for corrupted objects:

dcc verify

Scan and recover bad entries:

dcc verify --repair

Objects are re-hashed and compared with their names, as described in Self-Healing & Quarantine. Run it after a crash, a disk scare or restoring a cache from a backup. Ordinary runs already verify what they read.


🧹 dcc prune: enforce a size limit

Syntax:

dcc prune --strategy <lru|lfu|fifo> --max-size-mb <MB> [--dry-run]
Option Meaning
--strategyWhich entries go first: lru (least recently used), lfu (least frequently used) or fifo (oldest created). See Eviction & GC.
--max-size-mbThe size limit, in megabytes.
--dry-runShow what would be removed, and remove nothing.

Preview first, always:

dcc prune --strategy lru --max-size-mb 500 --dry-run

Prune to 500 MB, least recently used first:

dcc prune --strategy lru --max-size-mb 500

Prune to 500 MB, least frequently used first:

dcc prune --strategy lfu --max-size-mb 500

Prune to 500 MB, oldest first:

dcc prune --strategy fifo --max-size-mb 500

Pruning only removes cached results. A removed entry becomes a miss the next time it's needed, and the command runs again.


🗃️ dcc quarantine: inspect isolated entries

Syntax:

dcc quarantine <list|clear>

See what failed verification:

dcc quarantine list

Delete the quarantined files permanently:

dcc quarantine clear

Note

Look with list before you clear. Clearing deletes the evidence of what went wrong.


🔁 Common workflows

When you want to… Do this
Cache a slow command Wrap it in dcc exec with -i, -o and -e declared.
Check that caching works Run the same command twice, then dcc stats. Expect one miss and one hit.
Keep the cache from growing dcc prune --dry-run first, then the same command without the flag.
Recover after a crash or a disk fault dcc verify, then dcc verify --repair if it reports problems.
Investigate corruption dcc quarantine list before clearing anything.
Feed numbers into a script or dashboard dcc stats --json.

🚫 Common mistakes

Mistake What happens and how to fix it
Leaving out -- Flags for your command can be confused with dcc options. Always separate them: dcc exec … -- your-command.
Forgetting an input Editing that file doesn't invalidate the cache, and you get a stale hit. Declare everything the command reads.
Forgetting an output The file isn't captured, so it's missing after a hit. Declare everything the command writes.
Declaring a generated file as an input It changes every run, so you always get a miss.
Pruning without --dry-run Entries are removed with no preview. Preview first.
Clearing quarantine too early The evidence is deleted. Run list first.

🧾 Quick reference

Command Purpose
dcc exec [OPTIONS] -- <COMMAND> [ARGS]...Run with caching
dcc stats [--json]Usage and hit/miss metrics
dcc verify [--repair]Integrity scan, optionally repair
dcc prune --strategy <lru|lfu|fifo> --max-size-mb <MB> [--dry-run]Enforce capacity
dcc quarantine listShow quarantined entries
dcc quarantine clearDelete quarantined entries

🔌 Polyglot Integrations

One Execution Model. Any Toolchain.

dcc is language-agnostic by design.

It does not replace your compiler, bundler, generator, test runner, or static analyzer. Instead, it sits around the existing command and adds deterministic computation caching to workflows that were never designed with caching in mind.

Whether the workload is Rust compilation, a C++ translation unit, a TypeScript bundle, or a Python analysis pass, the execution model remains the same:

                    ┌──────────────────────────────┐
                    │        Developer Task        │
                    │  Build · Generate · Test · CI│
                    └──────────────┬───────────────┘
                                   │
                                   ▼
                    ┌──────────────────────────────┐
                    │          dcc exec            │
                    │                              │
                    │  Hash → Resolve → Lock →     │
                    │  Execute / Replay → Restore  │
                    └──────────────┬───────────────┘
                                   │
                     ┌─────────────┴─────────────┐
                     │                           │
                  CACHE HIT                  CACHE MISS
                     │                           │
                     ▼                           ▼
              CAS Replay                  Run Original Tool
              + Output Restore             + Capture Outputs
                     │                           │
                     └─────────────┬─────────────┘
                                   ▼
                         Deterministic Result

🧩 Supported Workflow Patterns

Ecosystem Typical Workload What dcc Tracks Example Tool
🦀 Rust Compilation, code generation, tests Source tree, arguments, environment, toolchain cargo
⚙️ C / C++ Translation units, generated artifacts Source/header inputs, compiler flags, tool identity clang++
🌐 Web / TypeScript Bundling and asset generation Source tree, package metadata, build arguments esbuild
🐍 Python Static analysis, linting, generated artifacts Package sources, configuration, environment mypy

The integration boundary stays intentionally thin: dcc observes the declared inputs and outputs, while the underlying tool remains responsible for the actual computation.


🦀 Rust / Cargo

Cache expensive compilation, code-generation, or test workflows without changing the underlying Cargo command.

dcc exec \
  -i src/ \
  -o target/out.bin \
  -- cargo build --release

Execution flow:

src/
 │
 ├── Rust source files
 ├── Cargo configuration
 └── declared build inputs
        │
        ▼
   dcc CacheKey
        │
   ┌────┴────┐
   │         │
  MISS      HIT
   │         │
   ▼         ▼
 cargo     CAS Replay
 build     + Restore

⚙️ C / C++ / CMake

Cache standalone compiler invocations while incorporating both source files and header dependencies into the declared input set.

dcc exec \
  -i main.cpp \
  -i include/ \
  -o main.o \
  -- clang++ -O3 -c main.cpp -o main.o

Changing a tracked source/header or compiler argument produces a different cache key, preventing the previous result from being reused.


🌐 Web / TypeScript

Cache deterministic frontend build and bundling operations across JavaScript/TypeScript projects.

dcc exec \
  -i src/ \
  -i package.json \
  -o dist/ \
  -- npx esbuild src/index.ts \
     --bundle \
     --outdir=dist

This allows unchanged source trees and build parameters to replay previously generated artifacts directly from the local CAS.


🐍 Python

Cache static-analysis workflows where repeatedly scanning an unchanged package would otherwise perform the same computation.

dcc exec \
  -i my_package/ \
  -o .mypy_cache/ \
  -- mypy my_package/

The same execution model applies to compatible linters, analyzers, generators, and other deterministic command-line workloads.


🔗 The Common Interface

Regardless of the ecosystem, integration follows one consistent pattern:

dcc exec
   │
   ├── -i  Input files / directories
   ├── -o  Expected output files / directories
   ├── -e  Environment variables
   │
   └── --  Original command + arguments

That means teams do not need a separate caching implementation for every language.

        Rust       C/C++       TypeScript       Python
          │           │             │              │
          └───────────┴─────────────┴──────────────┘
                              │
                              ▼
                         ┌─────────┐
                         │   dcc   │
                         └────┬────┘
                              │
                ┌─────────────┴─────────────┐
                ▼                           ▼
           Cache Miss                  Cache Hit
        Execute Normally             Replay from CAS

💡 Why This Matters

dcc is intentionally positioned below the language layer and above the operating-system process boundary.

Your build system, compiler, bundler, generator, or analyzer continues doing what it already does. dcc adds the missing execution layer around it:

  • No language-specific cache format required
  • No rewrite of the underlying tool
  • One CLI model across multiple ecosystems
  • Content-addressed result storage
  • Deterministic invalidation
  • Cross-process duplicate-execution prevention
  • Atomic output restoration

Bring the command. dcc decides whether it needs to run.


📋 20-Point Definition of Done & Engineering Audit Matrix

From Feature Completion to Engineering Verification

Reforge is not considered complete simply because its features are implemented.

Each milestone represents a specific engineering capability that has been implemented, exercised, and verified through dedicated automated test suites — covering correctness, concurrency, storage integrity, security, failure recovery, reliability, and performance.

The result is a verification trail spanning the complete execution lifecycle:

┌─────────────────────────────────────────────────────────────────────────────┐
│                        REFORGE VERIFICATION PIPELINE                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  FOUNDATION       DETERMINISM       CONCURRENCY       STORAGE               │
│      │                 │                 │                │                 │
│      ▼                 ▼                 ▼                ▼                 │
│   M1 ─ M4          M5.1 ─ M5.5       M6.1 ─ M6.5       M7 ─ M10             │
│      │                 │                 │                │                 │
│      └─────────────────┴─────────────────┴────────────────┘                 │
│                              │                                              │
│                              ▼                                              │
│                         INTEGRATIONS                                        │
│                              │                                              │
│                           M11 ─ M13                                         │
│                              │                                              │
│              ┌───────────────┴────────────────┐                             │
│              ▼                                ▼                             │
│          SECURITY                         RESILIENCE                        │
│        M14.1 ─ M14.3                    M15 ─ M17                           │
│              │                                │                             │
│              └───────────────┬────────────────┘                             │
│                              ▼                                              │
│                    END-TO-END VERIFICATION                                  │
│                         M18 ─ M19                                           │
│                              │                                              │
│                              ▼                                              │
│                    RELEASE ASSURANCE                                        │
│                    M20.1 ─ M20.3                                            │
│                              │                                              │
│                              ▼                                              │
│                       ┌────────────┐                                        │
│                       │ VERIFIED   │                                        │
│                       │ ENGINE     │                                        │
│                       └────────────┘                                        │
└─────────────────────────────────────────────────────────────────────────────┘

🧪 Milestone Verification Matrix

Milestone Engineering Capability Verification Focus Test Suite Status
M1–M4 🧱 Core Foundation Domain models, SHA-256 digest ingestion, cache-key construction and invalidation unit_tests_milestone_18_2 PASSED ✅
M5.1–M5.5 🔐 Deterministic Invalidation Input files, command arguments, environment variables, tool versions and platform state integration_tests PASSED ✅
M6.1–M6.5 ⚡ Single-Flight Concurrency Lock acquisition, contention handling and 50-process concurrent execution concurrency_tests PASSED ✅
M7–M10 📦 CAS & Storage Lifecycle Content-addressable storage, eviction strategies, capacity enforcement and garbage collection eviction_tests PASSED ✅
M11–M13 🔌 Polyglot Integrations Build-tool integrations and benchmark-based speedup analysis cache-integrations PASSED ✅
M14.1–M14.3 🛡️ Security Hardening Directory traversal rejection, symlink escape prevention and filesystem boundary enforcement security_tests PASSED ✅
M15–M17 🧯 Failure Recovery Failure injection, chaos recovery and corrupted-entry quarantine failure_injection_tests PASSED ✅
M18–M19 🔄 End-to-End Reliability Complete workflow lifecycles and high-load same-key writers concurrency_suite_18_4 PASSED ✅
M20.1 🧬 Release Integrity Release-quality validation, metadata self-healing and corrupted CAS recovery release_audit_milestone_20 PASSED ✅
M20.2 💪 Reliability Assurance Process-crash resilience and strict capacity-bound verification reliability_audit_20_2 PASSED ✅
M20.3 🚀 Performance Assurance Execution overhead measurement and cache speedup verification performance_audit_milestone_20_3 PASSED ✅

🔍 What the Audit Covers

The milestone matrix intentionally progresses from internal correctness to real-world operational behavior:

                    ┌───────────────────────┐
                    │   CORE CORRECTNESS    │
                    │ Models + Hashing      │
                    └───────────┬───────────┘
                                │
                                ▼
                    ┌───────────────────────┐
                    │ DETERMINISTIC CACHE   │
                    │ Key + Invalidation    │
                    └───────────┬───────────┘
                                │
                                ▼
                    ┌───────────────────────┐
                    │ CONCURRENT EXECUTION  │
                    │ Locks + Single Flight │
                    └───────────┬───────────┘
                                │
                                ▼
                    ┌───────────────────────┐
                    │ STORAGE INTEGRITY     │
                    │ CAS + GC + Eviction   │
                    └───────────┬───────────┘
                                │
                                ▼
                    ┌───────────────────────┐
                    │ SECURITY & RECOVERY   │
                    │ Isolation + Healing   │
                    └───────────┬───────────┘
                                │
                                ▼
                    ┌───────────────────────┐
                    │ RELEASE ASSURANCE     │
                    │ Reliability + Perf.   │
                    └───────────────────────┘

✅ Definition of Done

A milestone is considered verified when its associated implementation and automated verification suite have completed successfully.

The audit therefore evaluates more than the happy path. It explicitly exercises:

  • Deterministic cache-key generation
  • Multi-dimensional invalidation
  • Concurrent same-key execution
  • CAS integrity and object lifecycle
  • Capacity and eviction behavior
  • Filesystem boundary enforcement
  • Symlink and traversal rejection
  • Corruption detection and quarantine
  • Process-failure recovery
  • End-to-end execution lifecycles
  • Release integrity
  • Performance and cache-speedup behavior

The goal is not simply to prove that Reforge works.

The goal is to continuously verify the conditions under which Reforge is allowed to trust a cached computation.

🏁 Audit Status

╔══════════════════════════════════════════════════════════╗
║                  REFORGE AUDIT STATUS                    ║
╠══════════════════════════════════════════════════════════╣
║  Engineering Milestones        20+                       ║
║  Verification Areas            Correctness → Performance ║
║  Automated Suites              Dedicated per capability  ║
║  Reported Milestone Status     PASSED                    ║
╚══════════════════════════════════════════════════════════╝

The milestone matrix represents the project's current engineering verification record, rather than a claim that every possible workload or environment has been exhaustively tested.


🧪 Testing & Verification

 ┌──────────────────────────────────────────────────────────────────────────────────┐
 │                                                                                  │
 │   Every guarantee in this README has a test suite behind it.                     │
 │                                                                                  │
 │   🔒 key invalidation   🏎️ concurrency   🛡️ self-healing   🔐 security         │
 │   ⚙️ eviction           🔌 integrations   📋 release, reliability,performance  │
 │                                                                                  │
 └──────────────────────────────────────────────────────────────────────────────────┘

Reforge's promises (a hit never changes the result, racing runners share one execution, corrupt data is never replayed, outputs can't escape the workspace) are each checked by automated tests. You can run all of them at once or one guarantee at a time.


▶️ Run everything

All unit and integration tests across the workspace:

cargo test --workspace

🎯 Run one guarantee at a time

Suite What it checks Guarantee
security_tests Output paths can't escape the workspace: ../ traversal, absolute paths and symlink escapes are rejected. Security
concurrency_tests Many processes racing on one key: lock contention, one execution, the rest replay. Single-flight
failure_injection_tests Injected crashes and corrupted bytes: detection, quarantine and recovery. Self-healing
release_audit_milestone_20 Release-quality audit, including metadata self-healing and corrupted CAS data. CAS
reliability_audit_milestone_20_2 Process-crash resilience and capacity bounds. Eviction
performance_audit_milestone_20_3 Overhead and speedup verification. Performance

Security and directory-escape prevention:

cargo test -p dcc-runner --test security_tests

Concurrency and lock contention:

cargo test -p dcc-runner --test concurrency_tests

Failure injection and chaos self-healing:

cargo test -p dcc-runner --test failure_injection_tests

Release audit:

cargo test -p dcc-runner --test release_audit_milestone_20

Reliability audit:

cargo test -p dcc-runner --test reliability_audit_milestone_20_2

Performance audit (build in release mode so timings are meaningful):

cargo test -p dcc-runner --test performance_audit_milestone_20_3 --release

The full milestone breakdown is in the audit matrix.


🏗️ Run one crate

Core (keys and hashing):

cargo test -p dcc-core

Storage (CAS, eviction, quarantine):

cargo test -p dcc-storage

Runner (execution, locking, restore):

cargo test -p dcc-runner

🔧 When a test fails

You want to… Add this to the command
See printed output from passing tests-- --nocapture
Run tests one at a time-- --test-threads=1
Run only tests whose name contains a word-- <name>
List the tests in a suite without running them-- --list
Get a full stack trace on a panicset RUST_BACKTRACE=1

Show output while a suite runs:

cargo test -p dcc-runner --test concurrency_tests -- --nocapture

Run one suite single-threaded (useful if you suspect timing interference):

cargo test -p dcc-runner --test security_tests -- --test-threads=1

List the tests in a suite:

cargo test -p dcc-runner --test failure_injection_tests -- --list

Run only tests with "symlink" in their name:

cargo test -p dcc-runner --test security_tests -- symlink

Get a full backtrace (Linux / macOS):

RUST_BACKTRACE=1 cargo test -p dcc-runner --test concurrency_tests

Get a full backtrace (Windows):

$env:RUST_BACKTRACE=1; cargo test -p dcc-runner --test concurrency_tests

📏 What the tests do and don't tell you

Note

Passing tests show that Reforge behaves as specified on the machine that ran them. They don't prove that your own dcc exec commands are declared correctly. A missing -i is invisible to any test suite, so review declarations as described in the Core Guarantee. Timing-based suites, such as concurrency and performance, can vary with machine load. If one fails once, re-run it before assuming a regression.


🌐 Cross-Platform Support

🪟

Windows
x86_64 · MSVC

🍎

macOS
Apple Silicon · Intel

🐧

Linux
x86_64 · aarch64

Reforge is written in Rust and designed to run natively on all three. It uses the operating system's own file locking, atomic rename and path handling, and it doesn't depend on a Unix shell.


🧭 What differs by platform

Area 🪟 Windows 🍎 macOS / 🐧 Linux What to do
Output names Programs end in .exe No extension Declare the real filename with -o.
Symlinks Creating them needs Developer Mode or an elevated prompt Available to any user Only matters for the symlink demos.
Path separators \ and / / Use forward slashes in scripts you share across systems.
Filename case Usually case-insensitive Linux is case-sensitive. macOS usually isn't. Keep casing consistent so the same command hashes the same paths everywhere.
Shell in your command PowerShell or cmd sh, bash or zsh Wrap the tool directly (cargo, rustc) instead of shell one-liners where you can.

🔁 Caches don't cross platforms

The cache key includes the OS, architecture, pointer width and endianness. A result built on Linux is never replayed on Windows or macOS, or on a different CPU architecture. That's intentional, since the same source can compile to different bytes on each. It does mean each platform builds its own cache.


⚠️ Filesystem caveats

Note

Locking and atomic renames work as designed on local disks. Network filesystems (NFS, SMB) and some synced folders can behave differently, so keep .dcc/ on a local drive and don't share one writable cache directory over a network without testing it first.


✅ Check it on your machine

Build:

cargo build --workspace

Run the tests:

cargo test --workspace

Run the concurrency suite, which exercises locking on your OS:

cargo test -p dcc-runner --test concurrency_tests

Run the security suite, which exercises path handling on your OS:

cargo test -p dcc-runner --test security_tests

Found a difference on your platform? Please open an issue with your OS, architecture and the command you ran.


📄 License & Contributing

License

Reforge is dual-licensed. You may use it under either of these licenses, whichever suits your project:

⚖️  MIT License   ·   📜  Apache License 2.0

MIT is short and simple. Apache-2.0 adds an explicit patent grant.

MIT Apache-2.0
Style Very short and simple Longer and more detailed
Commercial use, modification, redistribution Allowed Allowed
Explicit patent grant No Yes
Keep copyright and license notice Yes Yes, and mark files you changed
Often chosen when You want the simplest terms You want the patent protection

This table is a summary to help you choose, not legal advice. The license files are what count.

Unless you explicitly state otherwise, any contribution you intentionally submit for inclusion in Reforge, as defined in the Apache-2.0 license, will be dual-licensed as above, without any additional terms or conditions.


Contributing

Contributions of every kind are welcome: bug reports, fixes, tests, documentation and ideas. Please review our Contributing Guide and Code of Conduct for full engineering guidelines and workflows.

Contribution What helps most
Report a bug Your OS and architecture, the exact dcc exec command, what you expected and what happened. A stale cache hit is a real bug: say which file wasn't declared.
Propose a feature Describe the problem first, then your idea. For anything touching cache keys, eviction or path checks, open an issue before writing code.
Send a fix One change per pull request, with a test for any behaviour you change. Small pull requests get reviewed fastest.
Improve the docs Try the Quick Start and fix whatever doesn't work. Typos, unclear steps and missing examples all count.
Add tests Failure cases especially: corrupted data, crashes mid-run, unusual paths and platform differences.

Open an issue here: github.com/Ashish6298/Reforge/issues


Set up your workspace

Fork the repository on GitHub, then clone your fork:

git clone https://github.com/<your-username>/Reforge.git

Enter the folder:

cd Reforge

Create a branch:

git checkout -b my-change

Build everything:

cargo build --workspace

Run the tests (before and after your change):

cargo test --workspace

Format the code:

cargo fmt --all

Lint it:

cargo clippy --workspace --all-targets

Push your branch, then open a pull request on GitHub:

git push origin my-change

Pull request checklist

  • The change does one thing.
  • cargo test --workspace passes.
  • New or changed behaviour has a test.
  • cargo fmt and cargo clippy are clean.
  • The README is updated if a command or behaviour changed.
  • You say which OS you tested on.
  • No safety check was weakened (hash verification, path confinement, locking).
  • Any change to cache-key rules is explained, since it invalidates users' caches.

Areas that need extra care

If you change Keep in mind
Cache key rules (crates/cache-core) A change can silently invalidate every user's cache, or keep a stale one. Discuss it in an issue first.
Storage and eviction (crates/cache-storage) Objects shared by surviving entries must not be deleted, and every read must stay verified.
Locking, restore and paths (crates/cache-runner) Outputs must never land outside the workspace, and a killed runner must never leave the cache stuck.

Reporting a security issue

Please don't open a public issue for a vulnerability. Contact the maintainer privately through the GitHub profile first, and include what you found, how to reproduce it and which platform you used. Public disclosure can wait until a fix is ready.


Reforge

Built with ❤️ in Rust for high-performance developer workflows.

About

High-performance, content-addressed developer computation caching engine in Rust. Guarantees zero redundant CPU cycles across builds, code generators, linters, and test suites.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages