Changelog
Release notes for the rust-doctor CLI, read directly from the GitHub releases.
0.7.0Breaking
The score moves to core-v4 in 0.7.0, and a value can move by several points on a workspace where no line changed. Read Breaking changes before upgrading a gate. Same 62 rules, same tier ceilings, same three bands: what changed is what one site costs, which rules the report tells you to repair first, and what travels with a finding.
Breaking changes
A site is charged by its tier and discounted by its measured rate
Under core-v3 a site of await_holding_lock cost what a site of useless_vec cost, and clippy::indexing_slicing, adjudicated wrong on all forty sites the corpus showed it, still took a healthy workspace's reliability to the forties. A site is now weighed by the tier of its rule, P3 one and every tier above doubling the one below, and a rule the corpus measured is discounted by its smoothed noise rate, where a rule it never adjudicated is charged whole (bc8f7df). λ moves to four on security and six on dependencies with the weights, so a lone P1 security site and a lone duplicate major score what they scored before.
| Severity | Weight | Tier | Weight | |
|---|---|---|---|---|
| Error | 2 | P0 | 8 | |
| Warning | 1 | P1 | 4 | |
| Info | 0 | P2 | 2 | |
P3 | 1 |
Info weighs nothing and keeps the score authoritative: a println! in a bin target is published at that level, since the streams are the program's output.
Every pinned corpus repository moves, and every one moves up, because the discount applies to the rules the healthy population trips most:
| Workspace | 0.6.0 | 0.7.0 |
|---|---|---|
| rust-doctor (self-scan) | 88 | 94 |
| thiserror | 73 | 85 |
| hexyl | 74 | 93 |
| fd | 75 | 87 |
| ripgrep | 80 | 90 |
| anyhow | 87 | 88 |
| vibesql | 68 | 73 |
| artifexprocal | 74 | 79 |
thiserror and hexyl read Needs work under core-v3, which no reader of either crate would have called them. The ten healthy repositories now all read Great; the healthy median sits 6.5 points above the agent one, against 8.5 before, because the agent population is scanned with Clippy off and its structural findings are discounted at rates the healthy population measured.
Upgrade path. If a CI script compares the score against a threshold, re-measure and raise the threshold rather than keeping the old one, which the new scale will clear for the wrong reason:
rust-doctor . --yes --json | jq '{value: .audit.score.value, model: .audit.score.model}'
A --scope baseline gate carries no threshold and is unaffected. A share link written by 0.6.0 names m=core-v3 and rust-doctor.com reads it as a retired model rather than comparing it with a current report.
What to repair first is one climb through the ceilings
audit.score.projected_rule_ids used to rank rules by expected repair value, which ignored the ceilings: one P1 finding beside sixty-five P3 sites projected the score the ceiling already held it at, and the withheld list never named anything. Each of the three places now goes to the rule whose repair, on top of the ones already named, gives the most points back through the ceilings, discounted by its measured rate, so the rule holding the ceiling comes first. A rule the corpus found wrong more often than right is named apart under audit.score.withheld_rule_ids (bc8f7df). On this repository:
- Fix the top 3 rules to reach a projected 97/100: rust_doctor::cargo::duplicate_major_versions, clippy::print_stderr, clippy::print_stdout
+ Fix this rule to reach a projected 100/100: rust_doctor::cargo::duplicate_major_versions (33% noise on 1 site)
+ clippy::indexing_slicing (98% noise on 40 sites) and clippy::string_slice (98% noise on 40 sites) report here but are left out
schema_version is 17, and a diagnostic carries the toolchain's replacement
"diagnostics": [
{
"code": "clippy::indexing_slicing",
+ "suggestion": {
+ "replacement": "items.get(index)",
+ "applicability": "maybe-incorrect"
+ },
The member is present when Clippy wrote a replacement for the finding's own span, absent otherwise, and the frozen v7 archive still projects. applicability is what rustc rates the replacement: machine-applicable is the only level safe to apply unread. A reader used to be told to use get at a site where the exact expression had already been written.
Upgrade path. Anything asserting schema_version == 16 asserts 17. In the Rust library, report::Diagnostic gains a public field and presentation::GroupDiagnostic two, which breaks a struct literal and nothing else; read them from inspect(InspectRequest) rather than constructing one.
Behavior changes
Same signatures, different output.
A duplicate major names who requires each version
Crate "winnow" is resolved with incompatible major versions 0.7.15 (through ra_ap_syntax), 1.0.4 (through toml).
The message used to name the two majors and nothing about which dependency to bump. Each version is now attributed to the member that requires it or to the member's dependency it arrives under, read off the lockfile's own graph (bc8f7df). The finding stays a warning whoever requires it, since the binary embeds both copies either way.
The linear report prints what a reader needs once
The help is printed once per group rather than under every site. The replacement is printed under its site as Replace with: ..., with the applicability beside it unless it is machine-applicable. A Rule: or Share: line is never wrapped, so a URL stays one thing to copy. A group with no site in production code folds onto one Unscored: line. The self-scan's verbose report went from 1160 lines to 397 for the same findings.
The agent handoff names the ceiling
Ceiling: the score is held at 65/100 by P1 findings; repair clippy::await_holding_lock before anything else.
Target: repairing the rules below reaches a projected 100/100.
An agent used to receive the shortlist with nothing about why the value would not move once the shortlist was repaired.
Internal
Eight of this release's thirteen commits change nothing observable outside the repository.
The corpus record is republished under the model, and its distribution gate no longer asks each population to span two bands: ten healthy crates all reading Great is the calibration succeeding, and the gate refuses the two populations landing in one band together and a healthy median under the agent one instead (bc8f7df). corpus.yml now also runs on a pull request that edits one of the five producers or the score, which is where a detector can be narrowed until it still matches its fixture and no longer matches real code (0181f99).
AGENTS.md went from 300 lines to a map of one to three lines per rule, each naming the document under docs/ that holds the reasoning, and three gates keep it that way: a word ceiling per document that ratchets down with the words removed, a check that every path and link the documents cite resolves, and tests/docs_contract.rs, which recomputes every number the documents state from the binary rather than rereading it (817bbe3, e4f0565, 2a1380f, ae50e32, 1909201, 98576f6). Running the last one is what found three stale numbers in the map. The v7 projection helper builds its prefix from SCHEMA_VERSION rather than from a literal a bump had to remember (700584f); docs/ joins the crate's exclude list, since the test that reads it does not ship.
Three clock-bound tests stopped measuring the machine. The alias map's timing bound became a ratio between a hundred-line unit and a thousand-line one measured at the same moment, after the 2 ms wall clock went red beside a release build (d6e055f); the presentation latency budget reads the allowance the CI already declares for every other clock, after the macOS runner measured 131 ms against a pipeline that takes 28 ms on both sides of core-v4 (5600378); and the snapshot a scan is checked against for mutation leaves out a file that vanished between listing and reading, after a transient file under .git took two tests down with a bare NotFound (1fc2419).
There is no fix subcommand and none is coming: the tool never writes into a workspace it scans. The suggestion above is what an agent or a reader applies.
Install
npx rust-doctor@latest
cargo install rust-doctor
Full changelog: https://github.com/arthjean/rust-doctor/compare/v0.6.0...v0.7.0
0.6.0Breaking
Test code stops weighing on the score in 0.6.0. A #[cfg(test)] mod tests; declared out of line was a gate the traversal dropped at the file boundary, so every finding in src/**/tests.rs was charged to production, and the record the score is calibrated against carried the same contamination. On this repository that was 61 of 105 findings and 9511 lines.
Read Breaking changes before upgrading a gate. Same 62 rules, same core-v3 arithmetic, same λ table, same tier ceilings: what changed is which code the tool calls production, and what the report ranks first once it knows.
Breaking changes
A #[cfg(test)] module declared out of line no longer counts as production
// src/thing.rs
#[cfg(test)]
mod tests; // the gate stopped here in 0.5.x
The gate now travels with the traversal, so src/thing/tests.rs is test code and every finding it raises carries context: "tests" (3f405bc). A file several targets reach abstains to production when they disagree, which is the same unanimity rule the source kernel already applied to a package and a target.
Both halves of the density move, and not always the same way:
| Workspace | 0.5.0 | 0.6.0 | production lines |
|---|---|---|---|
| rust-doctor (self-scan) | 89 | 88 | 35590 → 26079 |
| anyhow | 86 | 87 | 4113 → 4113 |
| artifexprocal | 75 | 74 | 10843 → 10664 |
Sixteen of the eighteen pinned corpus repositories score exactly what they scored, because a crate keeping its tests inline or under tests/ was already read correctly. The two that moved show the two directions. anyhow gained a point with its line count unchanged: an orphan file under tests/ that cargo never compiles was charged to the numerator while contributing nothing to the denominator, and its reliability density fell from 1.2157 to 0.9725. artifexprocal lost one with no finding moving at all: excluding test code shrinks the denominator too, so 10664 production lines carrying the same maintainability numerator read as a slightly higher density.
Upgrade path. If a CI script compares the score against a threshold, re-measure and move the threshold, rather than assuming the direction:
rust-doctor . --yes --json | jq '{value: .audit.score.value, lines: .audit.production_lines}'
A gate written as --scope baseline, which is what dogfood.yml and the workflow rust-doctor generates both run on a pull request, carries no threshold and is unaffected.
schema_version is 16, and a policy rule carries corpus_reviewed_sites
"policy": {
"rules": [
{
"id": "clippy::indexing_slicing",
"corpus_noise_basis_points": 9762,
+ "corpus_reviewed_sites": 40
},
The member is additive, absent on a rule the corpus never adjudicated, and the frozen v7 archive still projects (d2aea1a).
Upgrade path. Anything asserting schema_version == 15 asserts 16. A consumer that reads members by name and ignores the ones it does not know needs no change. In the Rust library, report::PolicyRuleReport gains a public field, which breaks a struct literal and nothing else; read the value from inspect(InspectRequest) rather than constructing one.
A rate published alone cannot be weighed: 33 % measured on one adjudicated site and 33 % measured on forty are the same number and not the same claim. The two members move together, so a rule carrying neither is a rule the corpus never adjudicated rather than a rule measured at zero.
Behavior changes
Same signatures, different output.
The ranking is smoothed, so an unmeasured rule is no longer free
corpus_noise_basis_points is now the sample read through a Beta(1,1) prior, (false positives + 1) / (reviewed + 2), instead of the raw quotient (d2aea1a). It ranks and it has never penalized: what a rule costs the score is still what it reported.
| Rule | 0.5.0 | 0.6.0 | sites |
|---|---|---|---|
rust_doctor::structure::oversized_unit | 8000 | 3415 | 39 |
rust_doctor::structure::near_duplicate_function_body | 8000 | 6250 | 30 |
clippy::indexing_slicing | 10000 | 9762 | 40 |
clippy::rc_buffer | 0 | 1429 | 5 |
rust_doctor::cargo::duplicate_major_versions | 0 | 3333 | 1 |
clippy::mem_forget | 10000 | 6667 | 1 |
Two effects, both intended. A rule measured on one site no longer reaches 0 % or 100 %, so a single adjudication cannot rank a rule first or bury it. And a rule the corpus never adjudicated is ranked at 5000 basis points, the prior read at no observations, where 0.5.0 gave it a full-weight discount of zero and ranked it above every measured rule. rust_doctor::structure::crate_level_allow and rust_doctor::structure::unreasoned_allow_attribute left the table entirely: every site the corpus had reviewed for them was this crate's own test code, and the corrected context leaves them unmeasured.
What the report tells you to fix first moves with it. On this repository:
- rust_doctor::cargo::duplicate_major_versions, clippy::print_stdout, rust_doctor::structure::near_duplicate_function_body
+ rust_doctor::cargo::duplicate_major_versions, clippy::print_stderr, clippy::print_stdout
The report names the sample wherever it names a rate
rust_doctor::cargo::duplicate_major_versions (33% noise on 1 site) reports here but is left out of the ranking
clippy::todo (unmeasured)
A rule the corpus never adjudicated is named as unmeasured rather than shown as a number, because printing the middle of the prior as a measurement publishes an assumption as an observation (d2aea1a).
An orphan file takes its context from its own path
A file no Cargo target compiles used to default to production wherever it sat. It now reads its context off its path, so an uncompiled tests/test_ffi.rs is test material (bc11dcc). This is what moved anyhow above: a file with no target has no lines in the denominator, so charging its findings to the numerator was a density with two different populations in it.
A clone family whose every member is test code is not charged to production
A structural family is one diagnostic naming every member through related, and it was charged to production whenever any member was. It is now charged only when the members agree, which is the same unanimity the rest of the kernel applies (3f405bc).
Fixed
- The measured rates the report ranks by are the rates the deepened corpus scopes produced. Two healthy scopes had been adjudicated on five sites each and read as near-certain noise, which is what kept
rust_doctor::structure::oversized_unitout of the ranking at 80 % (df31c8d, 6fc8e92). Adjudicated over their whole subpopulation, 39 and 30 sites, the two read 34 % and 63 %.
Internal
Twenty-nine of this release's thirty-five commits change nothing observable outside the repository, and most of them exist to make the rates above falsifiable rather than asserted.
tests/corpus.json now records an adjudicated site as a pair of independently judged passes rather than a verdict, publishes the agreement of the two passes as Cohen's κ beside Gwet's AC1 from the same table, and weighs every rate with the Wilson 95 % interval it rests on (52d8839, 98d234b, 76b2504, dd5e54c). Where a rule is measured on both populations, the record publishes the difference of the two rates as a Newcombe hybrid score interval and a separation verdict, so a gap that the samples cannot distinguish is published as indistinguishable rather than as a number (c16fbac). One of the four is separated today; the other three carry below_nominal_coverage.
Every published site is anchored to a run that located it. adjudication.position_proof is a blake3 digest over the identity of every reviewed site and every adjudicated pair, with the toolchain and the date of the run that confirmed them, recomputed offline on every cargo test, so a site typed in by hand at a line no scan ever reported fails the suite (07a919d, f80f2ee). corpus.yml now also runs on a pull request that edits the record or its harness, which is the one case where the answer can have moved without anyone starting the job by hand (674d421).
The record was republished under the corrected context, and a test checks the production context of a reviewed site against a fact the scan cannot launder (ccea01a, aaab650, bfe1bf7).
Install
npx rust-doctor@latest
cargo install rust-doctor
Full changelog: https://github.com/arthjean/rust-doctor/compare/v0.5.0...v0.6.0
0.5.0Breaking
Every score this tool reports moves in 0.5.0. The scale stopped being an occurrence step function over a file count and became a density: distinct scored sites weighed by severity, divided by the production lines the workspace actually holds, read through an exponential per dimension. The same defect in a hundred files and in ten thousand used to read as the same number, and a rule firing a thousand times cost what it cost at twenty.
Read Breaking changes before upgrading a gate. Same 62 rules, same tier ceilings, same dimension weights: what changed is the arithmetic between a finding and a number, and it is calibrated against a measurement of eighteen public repositories rather than chosen.
Breaking changes
Every score moves: the model is core-v3
| Corpus repository | 0.4.0 | 0.5.0 |
|---|---|---|
| smol | 100 | 99 |
| async-channel | 99 | 95 |
| hexyl | 98 | 74 |
| anyhow | 94 | 86 |
| log | 94 | 90 |
| bytes | 93 | 84 |
| serde_json | 93 | 86 |
| thiserror | 93 | 73 |
| ripgrep | 91 | 80 |
| fd | 90 | 75 |
Ten of ten move, by up to 25 points, on code that did not change (5189f3f, 71d4a88).
core-v2 charged a rule by its severity times an occurrence step that saturated at twenty-one sites. Two consequences followed from that shape and neither was intended: the same code duplicated ten times scored strictly worse than the original, and past twenty-one sites a workspace could add defects for free. The penalty is now a rate:
site density D = Σ (distinct scored sites × severity weight) ÷ denominator
dimension score = round(100 · exp(−D / λ))
Severity weights are 2 for an error and 1 for a warning or an info; an unknown severity is not charged. The denominator is production kilolines for clippy, the source kernel and the structural pass, and 1 for cargo-health and the repository pass, because no amount of source dilutes a missing lockfile. A workspace under two kilolines is charged against two, so a 120-line crate is not scored on a denominator that makes one finding fatal.
λ is the whole calibration, and a dimension sitting exactly at its λ scores 37:
| Dimension | λ |
|---|---|
security | 1.0 |
reliability | 10.0 |
maintainability | 3.0 |
performance | 4.0 |
dependencies | 3.0 |
Security is the tightest, because one security finding per two kilolines is already a workspace in trouble. Reliability is the loosest, because the correctness lints fire densely on code nobody would call broken.
Upgrade path. If a CI script compares the score against a threshold, re-measure your own workspace and move the threshold to what this model reads, rather than to what the old one did:
rust-doctor . --yes --json | jq '{model: .audit.score.model, value: .audit.score.value, dimensions: .audit.score.dimensions}'
Two properties survive the change and can be relied on. The tier ceilings are unchanged, so a P0 finding caps the score exactly where it capped it before, and every_tier_caps_at_the_value_it_capped_at_before walks all four through a real audit to prove it. And on a pull request, both dogfood.yml and the workflow rust-doctor generates run --scope baseline, which reports what a change introduces rather than an absolute number, so a gate written that way carries no threshold and is unaffected.
is_valid refuses a stored core-v2. A report deserialized from disk is validated against the shipped model, so a v0.4.x report replayed through this version is rejected rather than silently rescored. Rescan instead of migrating: the old score is not recoverable from the stored block, because the density it would need was never written down.
schema_version is 15, and the audit block carries production_lines
"audit": {
"source_files": 58,
+ "production_lines": 35590,
"categories": [ ... ],
"score": {
- "model": "core-v2",
+ "model": "core-v3",
...
The member is additive and the frozen v7 archive still projects, which is what proves no historical field disappeared or changed type (71d4a88).
Upgrade path. Anything asserting schema_version == 14 asserts 15. Anything asserting model == "core-v2" asserts "core-v3". A consumer that reads members by name and ignores the ones it does not know needs no change.
production_lines is lines of production Rust, test code excluded, counted by the same walk that produces the findings, so the numerator and the denominator are drawn from one population by construction. It is a floor rather than an exact count whenever the scan could not read part of the workspace, and that is the same condition under which audit.score.authoritative is already false.
Audit::build takes the production line count (Rust API only)
- Audit::build(source_files, status, &diagnostics)
+ Audit::build(source_files, production_lines, status, &diagnostics)
Breaks a Rust caller of the library only; the CLI and the JSON report are covered above (71d4a88). A caller that does not have a line count has no correct value to pass: the score is a density, and a denominator invented at the call site produces a number no measurement backs. Call inspect(InspectRequest) and read report.audit instead, which is the path that counts the lines for you.
Behavior changes
Same signatures, different output. Nothing to edit, but a snapshot or a log scraper may read differently.
A failed scan prints two sections, not twelve
A run that dies in the toolchain preflight still holds an inventory of the workspace it never scanned, and the score was built from that count against zero diagnostics. So a failed scan printed Scan failed, then Scanned 1 files, then No issues found., then All 0 occurrences across 0 findings, then a 100 / 100 face (c6b89c5).
A failure now renders the scope it was attempted under and the stage that failed, and nothing else. The other ten sections count, tally, rank or score a population no producer ever produced. Anything grepping the terminal output of a failed run for a score line will stop finding one, which is the point.
The two reports draw the same bar
The linear report cut its bar with a min of its own while the interactive one asked score_block::bar_width, so the two were one rounding apart from drawing different bars for the same score. There is now one function that sizes a bar (d8cfec2).
rust-doctor skill install writes a skill that states core-v3
The embedded agent skill described the retired model's arithmetic, so an agent read every number it was handed against a scale the binary no longer computes (26f908f). It now states the per-dimension curve, the severity weights, the per-producer denominators and the kiloline floor, each checked against src/audit/density.rs rather than restated from memory.
Two contract tests keep it true: one rejects any core-v token that is not the shipped model across every markdown file of the skill, and the other derives the three score bands from score_block::label_for rather than freezing them.
Re-run the install in any workspace that took the 0.4.x skill. The refusal to overwrite is the creation of the skill directory itself, so remove the old directory first:
gio trash .claude/skills/rust-doctor && rust-doctor skill install
Added
A share url names the model its score was read on
- https://rust-doctor.com/share?s=74&e=3&w=11&f=58
+ https://rust-doctor.com/share?s=74&m=core-v3&e=3&w=11&f=58&l=35590
A share url published a score and its counts and nothing about the scale, so the page it opens had to guess whether a 74 came from core-v2 or from core-v3: two models publishing the same shape over the same rules for very different values (fece80e). The model comes right after the score under m and is never omitted, and l carries the production line count the score was read against, under the same bound as every other count. The payload still carries no path, no environment variable and no user data, and the contract test now holds the whole query rather than a prefix of it.
Fixed
- A toolchain probe quotes what the toolchain said and names the remedy.
Clippy exited with status exit status: 101was the whole message a scan died on when the toolchain had no clippy component, which is whatdtolnay/rust-toolchaininstalls by default under the minimal profile. It named neither the missing component nor the one command that installs it, and cargo had already written both to stderr, where the probe sent them to/dev/null. Both streams are now read bounded, stderr on a thread of its own so a probe filling one pipe cannot deadlock the read of the other (a53166b).
Internal
Fifteen of this release's twenty-two commits change nothing observable outside the repository, and most of them exist to make the new scale falsifiable rather than asserted.
tests/corpus.json was regenerated over eighteen repositories under cargo 1.97.1, clippy 0.1.97 and rustc 1.97.1, and each observation now records the production line count the scan measured and the per-dimension density that count was the denominator of, so every published score recomputes from the record alone (b8705ec). The λ table is pinned beside the toolchain version and frozen against that measurement, because a λ moved without a new measurement republishes every recorded score under a model that never produced it; src/audit/tests/lambda_freeze.rs fails naming the dimension that moved (4ce38da).
score_distribution is what the scale proves about itself. Each population publishes its own spread: healthy 73 to 99 with a median of 85.0, agent-written 68 to 85 with a median of 76.5, a combined spread of 31 and 8.50 points between the two medians. core-v2 published one population and a boolean saying it had collapsed into a single band, which measured nothing beyond the collapse.
The rest is test infrastructure that was reporting failures nobody caused: the deterministic-report helper hashed .git/index and the lock files beside it, which git rewrites on its own (614261f, 537d9ea), the git oracle's fixture repositories now turn commit signing off where they already isolate the author (339cf45), and a probe test spawns sh -c "exit 1" rather than /bin/false, which macOS does not ship (90956b1).
Install
npx rust-doctor@latest
cargo install rust-doctor
Full changelog: https://github.com/arthjean/rust-doctor/compare/v0.4.0...v0.5.0
0.4.0Breaking
cargo install rust-doctor installs this tool again: crates.io and npm now publish from the same tag, and 0.4.0 is the first version number that means the same code on both registries. The agent skill ships inside the binary, so rust-doctor skill install writes it into a workspace with no network. And the code frame stopped going blank: every finding below roughly line two hundred of a file printed Code frame unavailable in 0.3.x, which is most of a file rust_doctor::structure::oversized_unit reports on at a thousand lines.
Three changes break something. Same 62 rules, same score model, same schema_version: 14.
Breaking changes
A failed dependency pass now drops complete and the authoritative flag
| Workspace whose dependency pass failed | 0.3.3 | 0.4.0 |
|---|---|---|
status | "complete" | "partial" |
complete | true | false |
audit.score.authoritative | true | false |
errors[] | already carried stage: "dependencies" | unchanged |
ExecutionResult::is_complete enumerated four producers by hand and consulted three of them, so cargo_health published its error into the report while the verdict above it ignored the error entirely. A workspace whose .cargo/config.toml or lockfile could not be read therefore published a dependencies error under "status": "complete", with the score still calling itself authoritative (00525bb).
Breaking because a gate reading complete will now refuse a report it used to accept. That is what the field was always supposed to say: the invariant this repository documents is that a pass that fails degrades to a complete report carrying a ReportError at its stage with the authoritative flag dropped, and dependencies was the stage where it was not held.
Upgrade path: if a CI gate reads complete or authoritative and your workspace has a manifest or .cargo/config.toml the pack cannot read, that gate goes red where it used to pass. Read errors[] to see which stage failed, and fix the manifest rather than relaxing the gate:
rust-doctor . --yes --json | jq '{status, complete, errors, authoritative: .audit.score.authoritative}'
A refused comparison reports its own stage and cause
Comparison over DIAGNOSTIC_LIMIT diagnostics | 0.3.3 | 0.4.0 |
|---|---|---|
errors[].stage | "baseline" | "delta" |
errors[].code | "baseline-limit-exceeded" | "delta-limit-exceeded" |
The refusal answered with the git baseline's own failure, so a run that hit the diagnostic ceiling told its reader that the git baseline snapshot exceeded a limit. The snapshot was fine, and nothing in that module shells out to git: the reader was sent to look at the one thing that had not failed (456214f).
Upgrade path: anything matching on baseline-limit-exceeded matches delta-limit-exceeded instead. The report shape is unchanged, member for member.
AuditCategory drops its four bare severity fields (Rust API only)
- let errors = category.errors;
- let warnings = category.warnings;
+ let errors = category.occurrences.errors;
+ let warnings = category.occurrences.warnings;
AuditCategory carried errors, warnings, info and unknown beside occurrences, which already held the same four numbers. Keeping the copy true took a recopy pass and four clauses of is_valid, and it still let the two drift at the one place that mattered: share_url summed the alias while totals summed the original (58f9b1b).
The four are now a wire spelling rather than state: Serialize emits them from occurrences, so the projection cannot disagree with its source. The JSON report is unchanged, member for member - schema_version stays at 14 and the frozen v7 archive still projects. This breaks only a Rust caller of the library, and the replacement is category.occurrences, a SeverityCounts carrying errors, warnings, info, unknown and total.
Behavior changes
Same signatures, different results. Nothing to edit, but a gate or a snapshot may read differently.
--scope baseline no longer charges a branch with structural findings older than it
Every structural message states a number the next edit moves: the line count of a file, the occurrence count of a clone family, the two figures of a hotspot. The delta matched those diagnostics on the message plus a source excerpt, so one line added to a file retired its oversized_unit finding and published a new one in its place. --scope baseline, which a pull-request gate runs, charged the branch with a finding older than it and credited it with a fix nobody made (d2e2bc1).
A structural diagnostic is now matched through structural_identity, the position-free fingerprint the structural pass already computes: the rule and the normalized content, no span, no count, no path. Expect delta.introduced and delta.fixed to shrink on any branch that touched a file already carrying a structural finding.
The report body ranks by what repairing a rule is worth
The score names the three rules to fix by expected_repair_value, the cost a rule carries discounted by the rate the corpus adjudicated it wrong, and names what it withheld for measured noise. The body ordered its groups by the raw cost, so the report could print that a rule was withheld as noise and then put it at the top of what to work down. The two calls also disagreed on their population: the body ranked a rule that only ever fired in a test as though it had cost points (7515176).
The body now ranks by the same key the score projects from, over the same population the score charges. The order of what to fix first changes on any workspace where a noisy rule out-counted a costly one.
A scoped rebuild keeps its own completeness
rebuild_for_scope recovered whether the source count was whole from score.authoritative, which also carries the scan status and whether every diagnostic was catalogued. One uncatalogued rule anywhere therefore made every narrower scope non-authoritative, for a reason that had nothing to do with the inventory (7ebac69). --scope baseline rebuilds through this path on every comparison, so a report that used to lose its flag for an unrelated reason keeps it.
Added
rust-doctor skill install
rust-doctor skill install # writes ./.claude/skills/rust-doctor/
rust-doctor skill install <PATH> # into another workspace
The skill an agent loads to drive the tool now lives in this repository and ships inside the binary, embedded with include_str! (bcba24c, 4c9808f). An install reaches no network, and a binary carries the skill of its own version rather than whatever the latest branch holds.
The copy it replaces had drifted past usefulness: it documented --plan, --score, --fix, --install-deps and --diff <ref>, none of which this binary has ever accepted, so an agent following it died on its first command. It described 19 kebab-case rules against a catalog of 62, credited findings to cargo-audit, cargo-geiger and cargo-deny, which no producer calls, and stated a scoring model with no tier ceiling in it. tests/skill_contract.rs now checks every long flag it documents against --help, every rule id against catalog(), and the rule count it states against the same list.
The refusal to overwrite is the creation of the skill directory itself, so either the whole skill lands or nothing does.
crates.io publishes from the same tag as npm
cargo install rust-doctor installs this version. Until now crates.io served 0.2.0, published in June 2026 from this same account before the rewrite: same name, same author, a different product. The 0.3 line only ever shipped to npm.
The two registries are now published by one workflow from one tag, each authenticating the run by its OIDC identity, so no registry token is stored anywhere. The numbers below 0.3.0 still mean different things on the two registries, because npm unpublished 0.1.1 through 0.2.0 and crates.io has no unpublish at all. 0.4.0 is the first version that means the same code on both.
cargo install rust-doctor # 0.4.0, the same binary npx resolves
npx rust-doctor@latest .
Every published package carries its license text
All six npm tarballs and the crate now ship LICENSE-MIT and LICENSE-APACHE. Each package declared MIT OR Apache-2.0 and carried neither text: npm auto-includes a license only from a package root and only under a name it recognizes, which LICENSE-MIT is not. The wrapper was also the one package published from the checkout rather than from a staged directory, which is what left it out of reach; it is now staged through the same path the packed smoke test installs from, so what a release uploads is what the proof installed.
Fixed
- A finding is framed wherever it sits in the file. The code frame read a byte prefix of eight kilobytes and then asked whether the reported line was inside it, which made the bound decide reach rather than work: roughly two hundred lines fit, and every finding below them printed
Code frame unavailable. The same prefix cut a multi-byte character in half at the cap, and the file then failedfrom_utf8as a whole, so one accented comment at the wrong offset cost a valid file every frame it had, including the ones on its first line (e777068). - The caret stays inside the text the frame shows. A span pointing into the part the sanitizer cut left carets hanging under nothing, and a span ending on a later line had its end column read against the line being shown:
oversized_uniton a four hundred line function drew a single caret under the signature, because the function's closing brace sits in column two (140b22d). - The two reports lay the frame out from the same gutter. The linear report hard-coded four columns for the line number, the interactive one computed its own, so a frame reaching line ten thousand slid the source row one column right in one report and not the other.
CodeFrame::gutter_widthis now the one answer, with four columns as its floor rather than its width (19f49a6). - A named pipe under a
.rsname no longer stops the report. The frame checkedis_fileon the handle it had already opened, and opening a named pipe blocks insideFile::openuntil a writer shows up.symlink_metadatanow answers before the open, in addition to the check on the handle rather than instead of it (fd45384). - A file whose name carries a percent sign keeps its proof. A path the report publishes is percent-encoded, and the delta's evidence loader opened that spelling literally, so
src/100%25.rsresolved to no file and every finding in it fell back to matching on its message, the weakest identity the module has (389c5d9). - Every git outcome is reported at the stage of its own call. Two of the four exits from the process layer were hard-coded to
scopewhoever ran them, so a baseline snapshot whose git flooded stderr publishedstage: "scope"in the JSON (4409d93). - The generated CI workflow is created rather than checked then written.
installtested for the file and then wrote it, and anything landing between the two would have been taken over, which made "never over an existing file" a likelihood rather than the guarantee it is documented as.create_newis now the refusal itself (720e801). - A truncation mark carries the style that survived the cut, rather than the style of the first span dropped (5398d91).
- A group's representative occurrence and its help are resolved once. Three consumers each answered "which occurrence stands for this group" on their own and gave three different answers (0f812d0).
Internal
Roughly fifty of this release's seventy-odd commits change nothing observable outside the repository: every module of the crate that sat over the 1000 lines its own rust_doctor::structure::oversized_unit rule reports at was split under it, tests included, and eleven the_X_holds_the_size_bound tests now keep it that way. The report module alone went from one file of 3226 lines to a wire format, an assembly, a normalizer and a sanitizer. The self-scan test that used to freeze src/report.rs as the crate's largest violation was replaced by a gate: no_unit_of_this_crate_s_own_source_is_a_hotspot fails on any oversized_unit or complex_function under src/, where its predecessor failed the day the violation was repaired.
Install
npx rust-doctor@latest
cargo install rust-doctor
Full changelog: https://github.com/arthjean/rust-doctor/compare/v0.3.3...v0.4.0
0.3.3Breaking
Every URL the tool prints now points at rust-doctor.com. Until this version they pointed at a rust-doctor.vercel.app project that had been deleted, so a scan run with 0.3.0 through 0.3.2 handed its reader three dead links: the docs line, the rule page of every finding, and the share URL. Upgrade if you use any of them. Nothing else changed: same 62 rules, same score, same report, same schema_version: 14.
Breaking changes
The published host moved to rust-doctor.com
| Surface | 0.3.0 to 0.3.2 | 0.3.3 |
|---|---|---|
| Docs line of the linear report | https://rust-doctor.vercel.app/docs | https://rust-doctor.com/docs |
| Rule page of a finding | https://rust-doctor.vercel.app/rules/<rule id> | https://rust-doctor.com/rules/<rule id> |
| Share URL built from a score | https://rust-doctor.vercel.app/share?s=<score> | https://rust-doctor.com/share?s=<score> |
| Branding line of the score block | Rust Doctor (https://rust-doctor.vercel.app) | Rust Doctor (https://rust-doctor.com) |
Breaking because a share link produced by a 0.3.2 scan resolves on a host this version no longer names, and because anything that asserted on the old host in a snapshot test or a scraped report now mismatches. There is no redirect from the deleted project: rewrite stored links to rust-doctor.com (f2f65bf).
The rule link printed under a finding (Rule: in the linear report, Learn more: in the interactive review) moves with it. The rule id stays percent-encoded, so clippy::same_warning reads https://rust-doctor.com/rules/clippy%3A%3Asame_warning. The --json report never carried these URLs and is unchanged.
Install
npx rust-doctor@latest
Full changelog: https://github.com/arthjean/rust-doctor/compare/v0.3.2...v0.3.3
0.3.2Patch
The binary now publishes its own rule catalog, and the GitHub Actions workflow it writes installs a pinned published launcher instead of compiling rust-doctor from git on every run. Nothing about how a scan behaves changed: same 62 rules, same score, same report, same schema_version: 14.
Added
-
rust-doctor rules listprints every catalogued rule with its category, producer, default level, tier and help. It reads no filesystem and needs no workspace: the catalog is what the binary was compiled with, so the command answers the same thing everywhere.rust-doctor rules list # one tab-separated line per rule: id, category, tier, help rust-doctor rules list --json # the full record, including producer and default levelIt exists so that whatever publishes the rule list reads it from the tool. The website had drifted to 125 rules from before the rewrite, describing detectors that no longer exist, because nothing mechanical connected the two. The test compares the command against the shipped catalog rather than against a frozen count, so the two stay true as the catalog grows (8544eaf).
-
rust_doctor::catalog()andrust_doctor::CatalogEntryare the same projection for library callers.CatalogEntrycarriesid,category,producer,default_level,tierandhelp, and is now the only public shape of a rule:RuleDefinition, which the scan compiles against, stays crate-private. No previously exported item was removed or renamed.
Changed
-
The generated CI gate installs the published launcher, pinned.
.github/workflows/rust-doctor.yml, written from the CI entry of the interactive report, used to runcargo install --locked --git https://github.com/arthjean/rust-doctor rust-doctoron every CI run, which compiled the tool from whatever the default branch happened to hold. It now runsnpm install -g rust-doctor@<version>, pinned to the version of the binary that wrote the file. The pin comes fromCARGO_PKG_VERSIONrather than a string in the template, so a release cannot forget to move it, and a generated gate keeps scanning with the rule set its author saw.Workflow files already written by 0.3.0 or 0.3.1 are not touched: rust-doctor never overwrites an existing
.github/workflows/rust-doctor.yml. Delete yours and re-run the CI entry to pick up the pinned form.
Install
npx rust-doctor@latest
Full changelog: https://github.com/arthjean/rust-doctor/compare/v0.3.1...v0.3.2
0.3.1Patch
A release-pipeline version. No file under src/ changed: the scanner, the catalog, the report and the CLI behave exactly as 0.3.0, and only the release workflow, the manifests and the lockfiles moved. Upgrading is safe and optional.
What it exists for: 0.3.0 was published to npm with a granular access token, because npm only accepts a trusted publisher for a package that already exists. This release is the first run of the token-free path the repository now declares, exercised on a real publication rather than on a dry run. The token was revoked afterwards.
Changed
- The release authenticates by OIDC instead of a stored token. Each of the six published packages names this repository's
release.ymlas its trusted publisher, so the registry authenticates the run by its workflow identity and the job carries noNPM_TOKEN. A leaked secret can no longer publish, and a workflow that is moved or renamed stops being trusted rather than staying authorized. Trusted publishing requires npm 11.5.1 and Node 22 ships npm 10, hence the upgrade step now in the job (634aab7).
Fixed
- A manual release run between two releases no longer fails.
npm publish --dry-runstill asks the registry and refuses a version it already serves, so validating the candidate ahead of the idempotence skip turned everyworkflow_dispatchrun red for stating something true. The skip guards the dry-run as well, and the validation that matters happens on the run after a version bump, where nothing is published yet (dcd6dc1).
Unchanged from 0.3.0
Every tarball is still published with --provenance, tying it to the commit and the workflow run that built it. That was already true of 0.3.0 and is not new here.
Install
npx rust-doctor@latest
Full changelog: https://github.com/arthjean/rust-doctor/compare/v0.3.0...v0.3.1
0.3.0Breaking
Machine-output consumer? The report shape is versioned: read JSON report.
rust-doctor 0.3.0 is a rewrite, not an increment. The scan is now five producers over one source walk, the rule set is a catalog of 62 rules whose precision is measured against ten pinned public repositories, and a run on a terminal ends in an interactive review instead of a wall of text. Every 0.2.0 flag except --json and --verbose was removed or renamed, a rust-doctor.toml written for 0.2.0 is rejected at startup, and the five external tools the old passes shelled out to are gone along with the network access they needed.
Upgrading from 0.2.0 is a migration. Read the breaking changes before this version reaches CI.
Breaking changes
The CLI surface was replaced
| 0.2.0 | 0.3.0 |
|---|---|
--diff <BASE> | --scope files --base <REF>, or --scope baseline --base <REF> to report only what the change introduced |
--fail-on error|warning|info|none | --blocking error|warning|none. There is no info level |
--score | Removed. Read .audit.score.value out of --json |
--sarif | Removed. --json is the only machine format |
--fix | Removed. Nothing writes to your source files |
--plan | Removed. The report ranks the top three rules on its own |
--mcp | Removed. There is no MCP server in this version |
--install-deps, --offline | Removed, and moot: no external tool is invoked and nothing reaches the network |
--project <NAMES> | Removed. A scan covers the whole workspace |
--no-project-config | Removed |
rust-doctor setup | Removed. The GitHub Actions workflow and the agent handoff are entries in the interactive report |
| (none) | --yes, which skips the interactive report and prints the linear one. Required for scripted and agent-driven runs |
| (none) | --rule <RULE_ID>=<LEVEL> and --category <CATEGORY>=<LEVEL> to override a level for one run |
Before and after, for the two CI shapes that break loudest:
# 0.2.0: fail the build on any error, scanning only what changed
rust-doctor --diff origin/main --fail-on error
# 0.3.0
rust-doctor --yes --scope baseline --base origin/main --blocking error
# 0.2.0: read the score
SCORE=$(rust-doctor --score)
# 0.3.0
SCORE=$(rust-doctor --yes --json | jq '.audit.score.value')
--scope files and --scope baseline both require --base <REF>, and --base without one of them is refused, so a half-written scope fails at parse time instead of silently scanning everything.
rust-doctor.toml has a new schema, and unknown keys are refused
The file name did not change. Everything inside it did, and the document is parsed with deny_unknown_fields, so a 0.2.0 configuration fails the scan rather than being partly ignored.
# 0.2.0
lint = true
dependencies = true
fail_on = "error"
[score]
fail_below = 80
[ignore]
rules = ["hardcoded-secrets"]
[rules_config.complexity]
severity = "warning"
threshold = 15
# 0.3.0
blocking = "error"
[categories]
performance = "off"
[rules]
"clippy::indexing_slicing" = "off"
"rust_doctor::structure::complex_function" = "warn"
[structure]
cyclomatic-threshold = 15
cognitive-threshold = 20
There is no replacement for score.fail_below, ignore.files or the per-rule enabled flag: a rule is switched off by setting its level to off, and the gate is decided by blocking alone.
Rule identifiers are namespaced
Every kebab-case identifier of 0.2.0 (hardcoded-secrets, complexity, and the rest) is gone. A rule id now names its producer: clippy::indexing_slicing, rust_doctor::source::disabled_tls_verification, rust_doctor::cargo::missing_lockfile, rust_doctor::structure::oversized_unit, rust_doctor::repo::tracked_secret_file. Any config key, ignore list, or JSON consumer keyed on an old identifier matches nothing. rust-doctor --json publishes the full active policy under .policy.rules, which is the shortest way to find the new name of a rule you used to configure.
Exit codes changed meaning
| Code | 0.2.0 | 0.3.0 |
|---|---|---|
| 0 | Scan completed, gates passed | Report complete and gate passed |
| 1 | Setup error (MCP, wizard, --install-deps) | Gate failed, or the report is incomplete |
| 2 | Scan error | The scan failed |
| 3 | Quality gate failed | Never returned |
A CI job branching on exit code 3 will never fire again. The gate failure now returns 1 (b66d076, 31ac84c).
Inline suppression comments no longer exist
// rust-doctor-disable-line and // rust-doctor-disable-next-line are not read by any producer. Suppression is Rust's own #[allow(...)], and the structural pass reports the suppressions that carry no reason: rust_doctor::structure::unreasoned_allow_attribute, stacked_allow_attribute and crate_level_allow (47ccf89, 9df6f93).
The JSON report is a different document
--json emits a new shape carrying schema_version: 14, with audit, policy, scope, project, toolchain, scan, diagnostics, delta, errors, summary and gate at the top level. No 0.2.0 consumer survives. Paths in it stay workspace-relative, with no absolute path, no environment variable and no user data.
The external tool passes were removed
cargo-audit, cargo-deny, cargo-geiger, cargo-machete and cargo-semver-checks are no longer invoked, installed, or checked for. Advisory-database findings, license policy, unsafe counting and semver checks therefore disappear from the report. Keep running those tools directly if you depend on them: rust-doctor no longer speaks for them, and no longer reaches the network for anything.
The scan compiles fewer targets, and publishes fewer findings
Two deliberate reductions, both of which make a report smaller than the one a previous version printed on the same code:
- Cargo's default targets replace
--all-targets. Tests, benches, examples and build scripts are no longer compiled. Measured on the corpus, 69.9% of the curated pack's findings came from there, and 1252 of the 1279 findings of the self-scan. A diagnostic that still comes from a non-shipped target carries thecontextit comes from, read from the Cargo target kind and never guessed from a path: it stays in the report and in both tallies, and stops weighing on the score and blocking the gate (7de61c4). - A Clippy lint the catalog does not name is never raised.
-A clippy::allopens the lint section of the Clippy command, before the-Wflags that raise the catalogued rules. An uncatalogued warning used to arrive with no category, no tier and no help, and its presence alone cost the score its authoritative flag. rustc is not a Clippy lint group, so rustc's own diagnostics still reach the report (7c67882).
If your score rises on upgrade without a line of source changing, this is why.
The minimum supported Rust version is 1.95
Up from 1.85. The crate is edition 2024 in both versions.
Installation moved to npm
The release publishes six npm packages: the rust-doctor launcher plus five @rustdoctor/<platform> binaries (linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64) as exact-version optional dependencies (92cc32a). crates.io still serves 0.2.0, so cargo install rust-doctor installs the previous generation of the tool. Build from source or install through npm to get this one.
What the scan does now
- Five producers, one walk. Curated Clippy lints, a native source kernel, Cargo manifest health, a structural pass and a repository hygiene pass. A producer that fails degrades to a complete report carrying an error at its own stage rather than aborting the scan.
- 62 rules, measured rather than asserted. 37 selected Clippy lints, 16 native detectors and 9 structural rules.
tests/corpus.jsonpins ten public repositories by commit and records the adjudicated false-positive rate of every rule (001a79c). - The ranking is decided by that measurement. The three rules the report tells you to fix first are ranked by what repairing each is expected to be worth: its cost to the score discounted by how often the corpus found it wrong.
clippy::string_slicefired 40 times across the ten repositories with not one true positive, and no longer heads a scan (31ac84c). The rate each rule was ranked by is published in the report (52302de). - Structural rules. Duplicate and near-duplicate function bodies, complexity and size hotspots, orphan module files, unreferenced features, and unreasoned suppressions. A clone family is one finding naming every member, and its identity is its shape rather than its position, so a baseline comparison does not report an old family as introduced by the branch (b72f074, 2c31617, a53ab6e).
- An interactive report. On a terminal, the scan ends on the score, a review of the findings one rule at a time, an entry that writes
.github/workflows/rust-doctor.yml, and a handoff that hands the work to Claude Code, Codex or Cursor. It never takes the alternate screen and never runs under--json,--yesor--verbose(b66d076, 5320963). - Every finding is one the tool can explain. A published diagnostic carries a catalogued rule with its category, its tier and its help, or the report says so and drops its own authoritative flag rather than scoring what it cannot account for.
Trust boundary
Inspecting a workspace runs cargo clippy inside it, and Cargo executes that workspace's build.rs files and procedural macros. Inspect trusted local paths only. Never scan a path taken from an issue, a bug report, or any other untrusted source. The four native producers compile nothing: they parse source text, read manifests, and ask git what it tracks.
The tool reaches no network, uploads nothing, and emits no telemetry.
Install
npx rust-doctor@latest # no install
npm install -g rust-doctor # or globally
Scanning still needs a Rust toolchain in the workspace, since Clippy is one of the five producers.
Full changelog: https://github.com/arthjean/rust-doctor/compare/v0.2.0...v0.3.0
0.2.0Feature
rust-doctor 0.2.0 is the largest release since the project went public — 31 commits spanning a new autofix engine, a workspace-deadlock fix, sharper diagnostics, and a hardened supply chain. npm packages are now published with Trusted Publishing (OIDC) and signed provenance attestations — no long-lived tokens anywhere in the pipeline.
Highlights
- Autofix.
rust-doctorcan now apply machine-applicable fixes to your source, not just report them — turning diagnostics into one-command cleanups. - Workspace deadlock fixed. Scanning a workspace with more members than CPU cores could deadlock. Scan-root parallelism is now bounded by available OS threads.
- Fewer false positives. The
hardcoded-secretsrule is now gated on value entropy and shape, and skips test code — far less noise on real codebases. - Signed, tokenless publishing. Every npm artifact ships with a provenance attestation minted over OIDC. Nothing in CI holds a publishing credential.
Features
- fixer: apply machine-applicable fixes to source files
- scan: report per-pass timings and tracing-based logging
- output: flag heuristic findings and clarify score / exit-code semantics
- mcp: cancel on timeout, harden scope, and align score config
- cli:
run()extracted frommaininto the library, so the binary's full pipeline is reusable as a crate
Fixes & reliability
- scan: bound scan-root parallelism with OS threads to fix the workspace deadlock
- process: SIGKILL the whole subprocess group on timeout (no orphaned children)
- scan: isolate the analysis target dir to avoid clobbering the project's own build
- suppression: anchor inline suppressions to absolute path identity
- config: reject unknown fields to catch config typos early
- rules: gate
hardcoded-secretson value entropy/shape and skip test code - mcp: add
pub(super)visibility totool_router/prompt_routermacros (
#1) — thanks for the first community contribution
Supply chain & tooling
- ci: npm publishing via Trusted Publishing (OIDC) + provenance, least-privilege workflow permissions, and a tag/crate-version guard
- lints: enforce the panic/unwrap restriction lint set
- Added
rustfmt.toml,.editorconfig, issue/PR templates, security policy, and code of conduct
⚠️ Behavior changes (review before upgrading)
- Exit-code & score semantics were clarified. If you gate CI on rust-doctor's exit code or score, re-check the thresholds against this release.
- Unknown config keys now error. A
rust-doctor.toml(or[package.metadata.rust-doctor]) with typos or stale keys that were previously ignored will now fail fast. Fix or remove unknown fields.
Install
# npm (prebuilt binary, all platforms)
npm i -g rust-doctor
# crates.io
cargo install rust-doctor
# shell installer (Linux / macOS)
curl -sL https://github.com/ArthurDEV44/rust-doctor/releases/latest/download/install.sh | bash
Full changelog: https://github.com/ArthurDEV44/rust-doctor/compare/v0.1.20...v0.2.0
0.1.20Patch
Remediation release: a full audit driven by rust-doctor's own findings took the repo score from 74.9 to 82+.
Code quality & architecture
- Deduplicate six
is_*_available()checks into a sharedprocess::is_cargo_subcommand_available() - Decompose
print_score_boxinto focused render helpers; extract the MCPServerHandlerinto a dedicatedmcp/handler.rsmodule - Enable nursery lints in
Cargo.toml
Performance
- Memoize file reads in the clippy filter (no more re-reading per diagnostic)
- Eliminate double hashing in the cache via an
is_fresh_with_hash/update_with_hashAPI
Security & supply chain
- Add
deny.tomlwith advisory, license, and ban policies (yanked = "deny"), plus acargo deny checkstep in CI - Harden
.gitignorewith secrets/credential patterns
Testing & DX
- Tests for terminal rendering,
main.rsgate functions, and 6 more custom rules in the integration suite - Pre-commit hooks (
cargo fmt+cargo clippy),CONTRIBUTING.md, and a Keep a ChangelogCHANGELOG.md
Docs
- Setup wizard demo video (12s, Remotion) in the README, plus setup docs on the website and FAQ
assets/excluded from the crates.io package
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.19...v0.1.20
0.1.19Patch
Patch Changes
Dogfooding: rust-doctor scanned itself and three warnings got fixed.
- mcp: replace a
diags[0]index withfilter_map+.first() - setup: replace an
agents[i]index withfilter_map+.get(i) - setup: replace
Box<dyn Error>with a typedSetupErrorenum, following the project error convention
Reliability dimension 99 -> 100.
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.18...v0.1.19
0.1.18Patch
Senior Rust reviewer skill
The setup wizard's SKILL.md now turns the agent into a senior Rust code reviewer instead of a linter wrapper. A new "Rust Expert Context" section carries 32 concrete flags across five domains (error handling, security, async/tokio, performance, architecture) that the agent applies while reading each flagged file, catching issues beyond what rust-doctor itself detects.
Sources: Rust API Guidelines, Alice Ryhl (Tokio), Luca Palmieri, RustSec advisories, the Rust Performance Book, Effective Rust, Oxide RFD 400.
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.17...v0.1.18
0.1.17Patch
Deeper skill template
The SKILL.md installed by rust-doctor setup produced shallow scans: agents ran one command and summarized the output. The rewritten template enforces a three-pass pipeline:
- Scan & Capture:
--jsonfor structured data plus--planfor priorities - Triage: P0-P3 classification by severity and category
- Investigate & Fix: mandatory source read before reporting, concrete before/after code for each P0/P1 finding, and a post-fix re-scan
Hard Rules and DO NOT sections explicitly ban shallow-analysis patterns (summary tables without source reads, generic advice without call sites, skipping verification).
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.16...v0.1.17
0.1.16Patch
Setup wizard UX
- Default recommendation is now CLI + Skills rather than MCP Server
- Agent selection offers "All" or a space-to-toggle multi-select for specific agents
- The wizard now asks before overwriting an existing rust-doctor skill or MCP config instead of silently replacing it
- The closing "try asking" message names rust-doctor explicitly so the agent knows which tool to invoke
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.15...v0.1.16
0.1.15Patch
Setup wizard
New rust-doctor setup subcommand: configure rust-doctor for AI coding agents (Claude Code, Cursor, Windsurf) in one interactive run. Two installation modes:
- MCP Server: writes a stdio server entry into the agent's MCP config file (
~/.claude.json,~/.cursor/mcp.json, ...) with crash-safe atomic writes (tempfile + rename) - CLI + Skills: installs a SKILL.md that teaches the agent to drive the CLI directly
The wizard auto-detects installed agents, prompts via dialoguer, and guards against non-TTY environments so it can never hang CI.
Docs
- Fix the score formula, lint count, and library example across README and website
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.14...v0.1.15
0.1.14Patch
Version bump only, cut to get a clean release pipeline run. No code changes over v0.1.13.
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.13...v0.1.14
0.1.13Patch
Restructure
Flat src/*.rs reorganized into domain modules:
src/passes/{security,static_analysis,quality}/for the analysis passessrc/output/{mod,score,terminal}.rsfor renderingsrc/mcp/{mod,tools,prompts,helpers,rules,types}.rsfor the MCP server, with prompt templates extracted into their own module
No behavior change intended; the scan orchestrator gained pipeline documentation.
Cross-tool AI config
The repo now carries first-class configuration for AI coding agents contributing to rust-doctor itself: AGENTS.md, .cursor/rules/rust-doctor.mdc, .github/copilot-instructions.md, .aiexclude for Gemini Code Assist, and recommended hooks in CLAUDE.md.
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.12...v0.1.13
0.1.12Patch
MCP: pure markdown reports
Claude Code renders structuredContent as raw JSON in the expand view, which made the v0.1.11 text+JSON split no better in practice. The scan tool now returns a single markdown report: score, dimensions, diagnostics grouped by severity with rule names, counts, example locations, and fix guidance. No JSON at all.
Report size: ~12K characters, versus ~30K grouped JSON (v0.1.10) and ~489K raw JSON (v0.1.8).
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.11...v0.1.12
0.1.11Patch
Patch Changes
- mcp: the scan tool now returns the markdown summary as a text content block (what the LLM reads first) and the grouped diagnostics as
structuredContent, instead of embedding the summary inside the JSON blob. Same pattern as production MCP servers like PostHog: text for the model, structured data for clients.
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.10...v0.1.11
0.1.10Patch
MCP: scan output built for LLM consumption
The scan tool used to return every individual diagnostic: 1,574 items and ~489K characters on a 201-file project, blowing past Claude Code's context window and forcing manual jq extraction.
- Diagnostics are now grouped by rule (sorted by severity then count) with up to 3 example locations each: ~25K characters for the same project
- A markdown summary (score, dimensions, top issues) rides along in the output
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.9...v0.1.10
0.1.9Patch
MCP improvements
- Complete tool annotations on all 4 tools:
title,readOnlyHint,destructiveHint=false,idempotentHint=true,openWorldHint=false, with a test locking the values in - MCP logging (
notifications/message) during scan and score execution for real-time visibility - Progress notifications on the score tool (previously scan only)
- The
deep-audit/health-checkprompts and the skill now prefer Context7 for documentation lookup and Exa for web research when those MCP tools are available, with graceful fallback to native WebSearch/WebFetch
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.8...v0.1.9
0.1.8Patch
Claude Code skill
- New
/rust-doctorslash command as a skill-based alternative to the MCP server:skills/rust-doctor/ships a SKILL.md with a 4-step pipeline (scope, scan, interpret, guide) plus reference docs for the rules, score interpretation, and suppression syntax. Installable vianpx skills addor manual copy.
Rules
- New
high-cyclomatic-complexityrule (Architecture category), bringing the custom rule count to 19 - Fix pre-existing severity mismatches between docs and source:
blocking-in-async: Warning -> Errortokio-spawn-without-move: Warning -> Erroractix-blocking-handler: Error -> Warningstring-from-literal: Warning -> Info
MCP
- New
deep-auditprompt: a 6-phase expert Rust audit workflow
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.7...v0.1.8
0.1.7Patch
Patch Changes
- packaging: exclude the Remotion video project (
rust-doctor-video/) from the crates.io package. No code changes.
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.6...v0.1.7
0.1.6Patch
Version bump only: the v0.1.5 npm publish partially succeeded, so 0.1.6 re-publishes all packages cleanly. No code changes.
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.5...v0.1.6
0.1.5Patch
Features
- cli: new
--install-depsflag: checks the six external tools rust-doctor drives (clippy, cargo-deny, cargo-audit, cargo-geiger, cargo-machete, cargo-semver-checks) and installs the missing ones (clippy via rustup, the rest via cargo install) - output: skipped passes now show up in the terminal score box with an install hint, and
--scorewarns when the score is incomplete because tools are missing - clippy: a missing tool is now reported as
Skippedinstead ofFailed
Also in the repo
- Project website (Next.js) with dark/light/system themes and SEO structured data, plus a Remotion project generating the README demo video
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.4...v0.1.5
0.1.4Patch
Patch Changes
- output: compute the score box width from unicode display width instead of
charcount, fixing misaligned box borders when the content contains wide glyphs - tests: the discovery test now reads
CARGO_PKG_VERSIONinstead of a hardcoded version string, so it no longer breaks on every release bump
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.3...v0.1.4
0.1.3Patch
Patch Changes
- process: drain subprocess stderr on a background thread. A tool writing enough stderr to fill the pipe buffer could deadlock the whole scan; this was the cause of MCP scans hanging indefinitely.
- geiger: send cargo-geiger stderr to null instead of piping it
- mcp: add an absolute 5-minute timeout to the scan and score tools. A stuck subprocess now returns a clear error instead of hanging the agent session.
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.2...v0.1.3
0.1.2Patch
Patch Changes
- npm: remove the
binfield from the five@rust-doctor/*platform packages. It conflicted with the main package's bin symlink at install time; the main package'sinstall.jspostinstall resolves the platform binary itself.
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.1...v0.1.2
0.1.1Patch
Patch Changes
- npm: fix the publish pipeline:
publish.shnow creates each platform package'sbin/directory before extracting the binary (git does not track empty directories), so the@rust-doctor/*platform packages actually ship their binaries - npm: pin the main package's
optionalDependenciesto the matching platform-package version
Full Changelog: https://github.com/arthjean/rust-doctor/compare/v0.1.0...v0.1.1
0.1.0Feature
Initial public release of rust-doctor: scan, score, and fix your Rust codebase with one command.
What's in the box
- Unified scanning: 700+ clippy lints with severity overrides and category mapping, plus 18 custom AST rules (via syn) covering error handling, performance, security, async anti-patterns, and framework-specific issues (tokio, axum, actix-web)
- Dependency auditing: CVE detection via cargo-audit, supply-chain policy via cargo-deny, unused dependencies via cargo-machete, unsafe-code metrics via cargo-geiger, API breakage via cargo-semver-checks
- Health score: 0-100 with per-dimension breakdown and the ASCII doctor face
- MCP server: plug into Claude Code, Cursor, or any MCP-compatible agent with
npx rust-doctor --mcp - Diff mode: scan only changed files for fast CI feedback
- Workspace support: scan all crates or select specific members
- Inline suppression:
// rust-doctor-disable-next-line <rule> - Output modes: terminal,
--json,--score, NO_COLOR respected - Library crate: drive rust-doctor programmatically via
lib.rs
Install
- npm:
npm install -g rust-doctor(prebuilt native binary, no Rust toolchain required) - crates.io:
cargo install rust-doctororcargo binstall rust-doctor - Shell installer (Linux/macOS) and PowerShell installer (Windows) attached below
Full Changelog: https://github.com/arthjean/rust-doctor/commits/v0.1.0