cd /news/ai-tools/cargo-crap-triaging-duplicates-with-… · home › topics › ai-tools › article
[ARTICLE · art-155198] src=minikin.me ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

cargo-crap: Triaging Duplicates with TypeSafe

Cargo-crap 0.6.0 adds a TypeSafe System One model (jev-latest by default) to triage duplicate-function reports, asking it to classify each pair as same-logic, parameterisable, shared-shape-only or structural-obligation, rate merge value on four levels, and estimate the probability a bug fix in one would be missed in the other. The tool's structural duplicate detection, added in 0.5.0, reports 24 pairs on cargo-crap's own source, and the two example pairs score 1.00 and 0.84 against a default threshold of 0.82, with the model returning a 0.65 divergence probability for the pasted totals and 0.38 for the receipt and shipping-label pair. Typed answers replace free-text chat replies, and --format json returns the verdict with a confidence value.

by read7 min views40 publishedSep 29, 2026
cargo-crap: Triaging Duplicates with TypeSafe
Image: Minikin (auto-discovered)

Duplicate detection cannot tell a pasted copy from a shared idiom. cargo-crap 0.6.0 asks a TypeSafe model which pairs to merge and which to leave.

Here are two pairs of Rust functions that cargo-crap’s duplicate detection puts near the top of its list.

The first pair is one routine, pasted and renamed. The day someone changes the discount in one of them, the other keeps the old rule, and quotes stop matching invoices.

The second pair has nothing in common but writeln!. Merging it would tie a receipt to a shipping label.

Structural similarity scores the first pair 1.00 and the second 0.84. Both are above the default threshold of 0.82, so both are reported, and nothing in the report says which one needs attention.

Since 0.6.0, cargo-crap can ask a [TypeSafe](https://docs.typesafe.ai) model about each pair. The same two pairs now come with a verdict:

## Why structure alone cannot tell them apart[#](#why-structure-alone-cannot-tell-them-apart)

--duplicates arrived in 0.5.0, and it compares functions by shape. Every function is parsed with syn and normalized: function names, variable names, field and path names and literal values are erased, while control flow, operators, statement order and the shape of each call stay. Every subtree of the normalized tree gets a fingerprint, and two functions are compared by the share of fingerprints they have in common (Jaccard similarity).

That is what finds a copy after someone renamed everything in it. After normalization, total += line.price_cents * ... and sum += item.price_cents * ... are the same tree, and they should be.

It also finds shapes that mean nothing. A macro call is one opaque node, so five writeln! calls look alike whatever they print. Two trait impls look alike because the trait decides their signature and often their body.

On cargo-crap’s own source, --duplicates reports 24 pairs. Some are real copies. Many are idioms and shapes a trait imposes. Until now the only way to sort them was to open every pair and read both functions.

Coding agents make that list longer. Copying a working function and renaming it is often the quickest change that passes the tests, and the result looks exactly like the first pair above.

Three narrow questions# #

Triage sends each reported pair to a TypeSafe System One model (jev-latest by default) with both function bodies and their similarity score, and asks three questions.

What kind of duplication is this? One of four answers:

  • same-logic : one routine written twice, differing only in names.
  • parameterisable : the same routine except for a constant, a field or one step, so merging needs a new parameter or a generic.
  • shared-shape-only : a shared idiom, such as a run ofwriteln! or builder calls, with nothing to consolidate.
  • structural-obligation : a shape imposed from outside by a trait, a visitor or a framework.

How much would merging them improve the codebase? A position on four levels: leave-it, optional, worthwhile, should-be-one.

If a bug were fixed in one, would the same fix be needed in the other and be missed there? A probability.

Structure has no answer at all to the third question, and it is the one that points at future bugs. For the pasted totals it comes back at 0.65. For the receipt and the label, 0.38.

Typed answers instead of a chat reply# #

I could have sent both functions to a chat model and asked whether they should be merged. The answer would be a paragraph. I would have to parse it, its wording would change between runs, and nothing in it would say how sure the model was.

A System One model answers in types. The kind is one of the four options, the worth question is a position on the four levels, the divergence question is a probability, and the verdict carries a confidence. With --format json, the pasted totals come back as:

An answer like that is data, and that shaped the rest of the design:

  • It fits on one line under the pair in the terminal.
  • It can be cached. Verdicts are stored under target/ , keyed by a hash of both function bodies, the model and the question set. An unchanged pair is never asked about twice, and a run over an unchanged tree makes no requests at all.
  • It can be tested. The test suite runs against a local stub server that plays back canned answers and injected failures, so none of the tests need the network.
  • Below a confidence floor, 0.5 by default, the line says triage: uncertain and names no kind, instead of passing on a guess.

TypeSafe says its System One models are trained for calibrated decisions. I still read the confidence as a hint, not a guarantee.

Advisory, and opt-in twice# #

cargo-crap gates CI, so triage must never be the reason a build fails. It only annotates. Every pair prints in the same order with the same score, and the exit code does not depend on the verdicts.

When anything goes wrong (no key, no network, an API error on one pair after retries), the whole triage is dropped with one warning, and you get the report you would have had without it. A report where some pairs are judged and others are not would look complete without being complete.

It is also off by default, because it sends source code to a third party. It does nothing until .cargo-crap.toml turns triage on and TYPESAFE_API_KEY is set in the environment, and the key is never read from the config file. The HTTP client sits behind a Cargo feature: the release binaries include it, and a source install leaves it out, with no network code at all, unless you ask for it.

Try it# #

Triage needs cargo-crap 0.6.0 with the triage feature, and a TypeSafe API key. The release binaries include the feature. From source, add it when installing:

Turn on duplicate detection and triage in .cargo-crap.toml:

Then run it. --summary keeps the CRAP table out of the way:

The repository has a small demo in examples/triage-demo: a shop backend with three pairs, each written to be a different kind of duplication. A real run on a colour terminal, where the verdict is red to merge, yellow for a parameter and dim to leave alone:

shipped_weight and shipped_volume differ only in the field they add up. Triage calls them parameterisable: worth merging, but only with a parameter.

The triage guide covers the rest: the JSON fields, the cache, and the settings.

What it does not do# #

  • It does not decide for you. A verdict is advice from a model that saw two functions and nothing else: not the callers, not the history, not the reason the copy was made.
  • It only judges pairs that structural detection already found. A copy edited enough to fall below the similarity threshold never reaches it.
  • It is not always sure. The demo first had a fourth pair: two methods of a Visit trait, which look alike because the trait demands it. I expectedstructural-obligation . The model answereduncertain at 0.46, just under the floor, so the report named no kind, and I took the pair out of the demo.
  • It sends your code to TypeSafe, one request per pair it has not seen. For a private codebase that is a decision for your team, which is why it is off by default.

How I use it# #

The workflow from the first post gets one more step. After an agent’s change passes tests and the CRAP gate, I run the duplicate pass and read only the pairs marked should-be-one or worthwhile. With --format json I sort those by divergence_risk, since a high value marks a copy where the next bug fix is likely to reach only one side.

── more in #ai-tools 4 stories · sorted by recency
── more on @cargo-crap 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/cargo-crap-triaging-…] indexed:0 read:7min 2026-09-29 · —