Adds a second question about the same tree: not "what is duplicated" but
"what does nothing reach". A new `basta` crate builds the import graph from
the project's entry points and walks it, reporting unused files, unused
exports, unused module-private declarations, unused imports and — opt-in —
unused class members, across JavaScript, TypeScript, JSX, TSX and Python.
It ships two ways: `jscpd --dead-code` inside the existing binary, and a
standalone `basta` command. Both reuse the walker, the filters, all fifteen
reporters and the `--threshold` / `--exit-code` gates, so nothing new has to
be learned to run it.
Because it is a graph traversal rather than a reference count, dead code
cascades: a helper whose only caller is itself dead is reported too. Every
finding carries a confidence score and, below 100, the reasons it might be
wrong — `eval`, an unrecognised decorator, a wildcard re-export, a name seen
only in a string, a file in the scan that did not parse. `--min-confidence`
sets the floor at 60.
Entry points come from `package.json`, `pyproject.toml`, framework and
file-name conventions, and anything a shell script, CI workflow, Makefile or
Dockerfile names by path. CommonJS is read alongside ESM. A project that
renames its own import paths is read on its own terms: `paths` and `baseUrl`
from `tsconfig.json` and `jsconfig.json` are resolved, through a relative
`extends` chain and per package in a monorepo. A file named only by a path
string — a worker spawned through `new URL`, a build entry, a vitest setup
file — counts as used.
Languages plug in through one trait, `basta::lang::Analyzer`, which owns
parsing, specifier resolution, entry-point conventions, manifests, alias
configs and path traits. Nothing outside `lang/` is language-specific;
`docs/basta-extending.md` walks through adding one.
Validated against knip, fallow, vulture, skylos, fossil-mcp and repowise on
trending repositories. On a corpus of ten of them — 11k files, 3.3M lines —
every one of basta's 1,806 findings was checked against an import graph
rebuilt independently of it: no unused-file, unused-import or unused-symbol
claim survived as wrong.
Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
Rust / crates.io
- report the invoked binary name in --version/--help (jscpd vs cpd)
- add repository, documentation, keywords, categories to all crates
- exclude tests/ from the jscpd crate package; add cargo-binstall metadata
- expand the crates.io README and render it on docs.rs
npm
- add funding field to jscpd, cpd and jscpd@4 packages
- add jscpd-linux-arm64-musl platform package and clearer unsupported-platform error
- enable provenance for the v4 changeset publish
Pipelines
- rust.yml: windows-latest in the matrix, drop dead schedule guard
- release.yml: cosign-sign release assets, floating v5 tag, LICENSE in tarballs,
invoke the new docker.yml (GHCR multi-arch image) after each release
- audit.yml: nightly cargo audit, cargo deny and pnpm audit
- Dockerfile: multi-stage image from release binaries (distroless), mcp target for Glama
Docs / community
- README: shields badges, single primary install per OS, GitHub Action snippet,
link to jscpd.dev, v5.1 features, 15 reporters, 224 formats, 8 platforms
- docs: @v5 action tag, Docker usage, baseline section, current crate versions
- YAML issue forms with engine selector, config.yml, CODEOWNERS
- remove stale .github/CONTRIBUTING.md stub, custom.md, TODO.md; untrack rust/jscpd-report.json
- examples: mark as v4 API, add a Rust cpd-finder example
The workspace declared rust-version = "1.87", set once when the Rust
workspace was added (0ddf611, June 2026) and never revisited. crates.io
publishes that field verbatim, so the jscpd crate page advertised a floor
the crate cannot actually build on:
$ cargo +1.93 check -p jscpd
error: rustc 1.93.1 is not supported by the following packages:
oxc_allocator@0.147.0 requires rustc 1.96.0
...
The real floor is 1.96.0, forced by the 13 oxc_* crates that reach the
published crate through the mandatory oxc_parser -> cpd-tokenizer -> jscpd
chain. ignore, globset and askama independently require 1.88. The number
was probably accurate at the time — oxc was pinned to 0.133 then — and
drifted silently as oxc raised its own MSRV across later bumps.
Nothing caught the drift because every workflow pins the toolchain to
1.97.1, so no job ever built at the declared MSRV. This adds an `msrv` job
that reads rust-version straight out of Cargo.toml and runs cargo check
against exactly that toolchain, so the next oxc bump fails loudly instead
of quietly falsifying published metadata.
The check uses `cargo +<version>` rather than relying on the toolchain
installed by the action: rust-toolchain.toml pins 1.97.1 for development
and would otherwise take precedence, making the job test the wrong
compiler.
rust/README.md claimed 1.87+ and pointed at rust-toolchain.toml, which
actually pins 1.97.1 — the two never agreed. It now states the MSRV and
explains what the pin is for.
Verified: cargo +1.96 check --workspace --all-targets --locked passes, so
1.96 is achievable including dev-dependencies.
- Add curl install method (jscpd.dev/install.sh) to README Quick Start
- Remove unused badges (Ukraine, jscpd SVG, npm downloads, codecov,
FOSSA, Open Collective, nodei.co) from README; keep npm version,
license, CI, and NPM badge
- Remove Star History and GA tracker from README; restore NPM badge
- Add GA tracker to all sub-READMEs
- Set homepage to https://jscpd.dev in all package.json files
- Set homepage to https://jscpd.dev in all Cargo.toml files