I deleted ten thousand lines of my own tool

A diff made almost entirely of deleted lines, with a handful kept

The most useful thing I did to my own project this year was delete most of it.

rx is a local CI and task runner for Rust workspaces. You define your pipeline once in rx.toml, and rx ci runs the same fmt, clippy, test and build steps on your laptop that run in CI. That is the entire product.

It was not the entire product six months ago. The distance between those two sentences is one hundred files and eleven thousand deleted lines.

What it was trying to be

The original pitch was a unified toolchain manager: one binary in front of the whole Rust experience. Toolchain management, release automation, dependency auditing, SBOM generation, telemetry, a plugin system, a background daemon. Roughly forty commands.

You can watch the sprawl happen in the commit log immediately before the correction. A lockfile audit with a reproducibility score. Cache export and import for portable CI. Doctor, enhanced with project analysis and suggestions. First-class .env support with validation. Speculative check.

Every one of those was a pleasant afternoon. None of them was a bad idea in isolation. Together they were a tool with no thesis.

That is the thing about scope creep that nobody warns you about properly. It does not arrive as one bad decision you could have caught in review. It arrives as a long sequence of individually defensible ones, each of which makes the product slightly harder to explain, until you cannot answer the only question that matters.

The question that ended it

The question was: what does rx do that Cargo does not?

There is a real answer. Cargo has no concept of a pipeline you run identically in both places. It will not tell you which workspace packages a change actually affects. It will not run your independent tasks concurrently. Those three things are worth a tool.

Everything else I had built was a worse version of something that already existed. rustup manages toolchains, and it is better at it. cargo-release publishes, and it is better at it. cargo-audit audits. sccache caches compilation. I was not competing with those tools, I was shipping thinner copies of them inside a binary nobody had asked to be a platform.

So the product document now opens with what rx is, and then spends more space on what it is not: not a Cargo replacement, not a toolchain manager, not a release tool, not a scaffolding tool, not an SBOM tool, not a compiler cache, not a plugin platform, not a daemon. The line I had to write for myself was that these are all legitimate tools, as separate products. Any of them can be invoked from an rx task.

Writing the non-goals took an afternoon. It should have been the first afternoon.

Deleted, not deprecated

The correction ran as four phases across four pull requests. The totals: one hundred files changed, 1,744 insertions, 11,893 deletions. Phase two on its own removed 10,491 lines across seventy-nine files.

Deleted, not deprecated. Deprecation is a promise you make to users, and it costs real time to keep. Pre-1.0 is the short window where that promise is not yet owed, and I would rather spend it correcting the shape of the thing than politely maintaining forty commands I had already concluded were mistakes. The project is pre-1.0 precisely so this kind of correction stays cheap. That sentence is in the repository now, mostly so that future me reads it before adding a plugin system again.

The feature I was proudest of is gone

Phase one deleted a file called semantic_hash.rs. Three hundred and fifty three lines, removed whole.

It did something I still think is clever. It hashed a crate’s public API, and when the API had not changed, it skipped rebuilding the crates that depend on it. Change a function body, and dependents do not need recompiling. Change a signature, and they do. In a large workspace that is an enormous saving, and when I demonstrated it, it felt like the best thing in the tool.

It was also wrong, in the specific way that matters most.

It could skip rebuilding a crate whose implementation had changed. Not often. In the ordinary case it was correct and fast. But a build tool that occasionally does not rebuild your changed code is not a fast build tool. It is a debugging session you do not know you are in.

That is the part worth sitting with. The failure does not announce itself as “the cache was wrong.” It presents as “my change did nothing,” which sends you to inspect your own logic first, then your assumptions, then your sanity. You can lose an afternoon to it before the tool even becomes a suspect, because the tool is supposed to be the ground you are standing on.

It is a shape worth learning to recognise: an optimisation that is right almost always, and quietly catastrophic in the fraction of cases where it is not. The correct response is the unglamorous one. Delete it and take the slower, correct path.

The cache ships turned off

What survives is a more modest thing: a content-addressed artifact store, fingerprinted with xxh3, with atomic writes so a half-written entry can never be read as a whole one.

It is disabled by default.

The fingerprint covers the build profile, the RUSTFLAGS rx generates, Cargo.toml, Cargo.lock, and the .rs files under src/. It does not cover the target triple, package selection, feature flags, the toolchain version, build.rs, .cargo/config.toml, compilation-relevant environment variables, files pulled in by include_bytes! and include_str!, or sources under tests/, benches/ and examples/.

That list is published, in the repository, under a heading that says “known correctness limits.” Restoration is off unless you turn it on, and it is never used for --package or --target builds at all, because those are exactly the cases the fingerprint cannot distinguish.

An artifact cache with an incomplete fingerprint is not a fast cache. It is a correctness bug with a performance benefit attached, and the benefit is what stops you from fixing it. There were three honest options: finish the fingerprint, hand the whole job to sccache, or ship it off by default and write down precisely why. I took the third while I decide between the first two.

I have come to think the list of things your cache does not hash is worth more than any benchmark you would like to quote. The benchmark is a claim about speed. The list is a claim about what you actually understand.

What was left

The pipeline defined once and run in both places. Affected-package selection, resolved once and passed to a single Cargo invocation as repeated -p flags rather than a loop of per-package builds. Concurrency for independent tasks. Automatic mold and lld detection, with the result cached so the probe is not repeated. One task executor, one workspace model.

And a deliberate refusal: compilation scheduling stays with Cargo. rx does not try to be a compiler cache, because Cargo owns target/ and sccache already does the other thing well.

What I would tell the version of me who started it

Write the non-goals first. Not because you will follow them, but because the first time you notice you are about to violate one, you will have to say out loud why, and about half the time you will not have a good reason.

And notice this: every feature I deleted was a feature I had enjoyed building. The lockfile scoring, the semantic hashing, the plugin hooks. That correlation is not an accident, it is a diagnostic. The parts of a tool that are fun to build are the parts most likely to be about you rather than the person using it.

Pre-1.0 is not the phase where you add everything. It is the only phase where deleting is still free.

No account, no tracking. One vote per reader.