{"slug": "cargo-crap-triaging-duplicates-with-typesafe", "title": "cargo-crap: Triaging Duplicates with TypeSafe", "summary": "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.", "body_md": "# cargo-crap: Triaging Duplicates with TypeSafe\n\nDuplicate 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.\n\nHere are two pairs of Rust functions that cargo-crap’s duplicate detection puts near the top of its list.\n\nThe 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.\n\nThe second pair has nothing in common but `writeln!`. Merging it would tie a receipt to a shipping label.\n\nStructural 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.\n\nSince 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:\n\n## Why structure alone cannot tell them apart[#](#why-structure-alone-cannot-tell-them-apart)\n\n`--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).\n\nThat 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.\n\nIt 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.\n\nOn 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.\n\nCoding 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.\n\n## Three narrow questions[#](#three-narrow-questions)\n\nTriage 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.\n\n**What kind of duplication is this?** One of four answers:\n\n- `same-logic` : one routine written twice, differing only in names.\n- `parameterisable` : the same routine except for a constant, a field or one step, so merging needs a new parameter or a generic.\n- `shared-shape-only` : a shared idiom, such as a run of`writeln!` or builder calls, with nothing to consolidate.\n- `structural-obligation` : a shape imposed from outside by a trait, a visitor or a framework.\n\n**How much would merging them improve the codebase?** A position on four levels: `leave-it`, `optional`, `worthwhile`, `should-be-one`.\n\n**If a bug were fixed in one, would the same fix be needed in the other and be missed there?** A probability.\n\nStructure 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.\n\n## Typed answers instead of a chat reply[#](#typed-answers-instead-of-a-chat-reply)\n\nI 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.\n\nA 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:\n\nAn answer like that is data, and that shaped the rest of the design:\n\n- It fits on one line under the pair in the terminal.\n- 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.\n- 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.\n- Below a confidence floor, 0.5 by default, the line says `triage: uncertain` and names no kind, instead of passing on a guess.\n\nTypeSafe says its System One models are trained for calibrated decisions. I still read the confidence as a hint, not a guarantee.\n\n## Advisory, and opt-in twice[#](#advisory-and-opt-in-twice)\n\ncargo-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.\n\nWhen 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.\n\nIt 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.\n\n## Try it[#](#try-it)\n\nTriage 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:\n\nTurn on duplicate detection and triage in `.cargo-crap.toml`:\n\nThen run it. `--summary` keeps the CRAP table out of the way:\n\nThe repository has a small demo in [`examples/triage-demo`](https://github.com/minikin/cargo-crap/tree/main/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:\n\n`shipped_weight` and `shipped_volume` differ only in the field they add up. Triage calls them `parameterisable`: worth merging, but only with a parameter.\n\nThe [triage guide](https://github.com/minikin/cargo-crap/blob/main/docs/guides/triage.md) covers the rest: the JSON fields, the cache, and the settings.\n\n## What it does not do[#](#what-it-does-not-do)\n\n- 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.\n- It only judges pairs that structural detection already found. A copy edited enough to fall below the similarity threshold never reaches it.\n- 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 expected`structural-obligation` . The model answered`uncertain` at 0.46, just under the floor, so the report named no kind, and I took the pair out of the demo.\n- 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.\n\n## How I use it[#](#how-i-use-it)\n\nThe workflow from the [first post](https://minikin.me/blog/cargo-crap/) 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.", "url": "https://wpnews.pro/news/cargo-crap-triaging-duplicates-with-typesafe", "canonical_source": "https://minikin.me/blog/cargo-crap-triage/", "published_at": "2026-09-29 00:00:00+00:00", "updated_at": "2026-10-22 10:10:48.720958+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents"], "entities": ["cargo-crap", "TypeSafe", "jev-latest", "Rust", "syn"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/cargo-crap-triaging-duplicates-with-typesafe", "markdown": "https://wpnews.pro/news/cargo-crap-triaging-duplicates-with-typesafe.md", "text": "https://wpnews.pro/news/cargo-crap-triaging-duplicates-with-typesafe.txt", "jsonld": "https://wpnews.pro/news/cargo-crap-triaging-duplicates-with-typesafe.jsonld"}}