From 5.4.0 to 5.7.0
Four releases. One adds the command that answers the question the other four commands only
gesture at. Two are bugs found by running clines on real repositories rather than by
reading its source — including one where clines was measuring its own output. The last
removes 95.7% of what npm install clines downloads.
5.4.1 — clines -v
It printed unknown option and exited 1. Commander reserves
-V for the version and leaves -v free, but -v is
what node, npm and git accept, so it is what people type.
The fix rewrites -v only in the global position, so
clines count --config -v still treats -v as a path rather than a
request for the version. -V and --version are unchanged.
5.5.0 — clines was counting its own reports
--html defaults to writing into the working directory, which is where people
put it. That file is then part of the repository, so the next run measures it. On
facebook/react, a dup.html (1.2 MB) and a
cx.html left over from earlier runs added
11,403 phantom code lines — 3.4% of the repo — and took HTML from 19 files to 21.
HTML 21 files 12,260 code <- with two clines reports present
HTML 19 files 857 code <- actual
Nothing about this was exotic: it is reachable by following the documented usage. Reports
now carry <meta name="generator" content="clines" />, and the role
classifier treats that — or a clines report <title> — as
generated, which is excluded from headline figures by default.
Detection is deliberately narrow. A page that merely mentions clines, or whose title is
clines usage guide, stays source. Verified two ways: against the real
leftover artifacts in react, and by round-trip — writing all three report types into a
fixture leaves that fixture's count byte-identical.
5.6.0 — clines refactor
Complexity is a poor priority list. It ranks a file you never open above one you edit every week, because it is largely a proxy for size. Measured over two years of react's history:
- 250 of the 1,113 files with any complexity did not change at all. A complexity ranking puts many of them near the top.
- Ranking by complexity × changes overlaps ranking by complexity alone by only 14 of 20 files on react, and 12 of 20 on bootstrap. The two lists are not the same list.
So clines refactor (alias rf) reads
git log --name-only once — one cheap call, no per-file blame — joins the
change count to complexity density and token cost, and returns a verdict per file.
| verdict | meaning |
|---|---|
refactor |
complex and changed often — you pay for this repeatedly |
split |
expensive to read and changed often, though the logic is simple |
watch |
changed often but cheap to read |
quiet |
rarely touched |
inert |
untouched in this window — leave it alone |
The ranking is by tokens × changes: what reading that file for every change to it
has already cost, and what it will keep costing. --price 3 turns that into
dollars per million tokens.
$ clines refactor ../react --price 3
Refactor: 1,113 files weighed against 2,367 commits since 2 years ago
Judged against this repo: changed often means 3+ changes, dense means 16.7 cx per 100 lines, costly
means 2,420 tokens.
refactor 122 files complex and changed often — you pay for this repeatedly
split 162 files expensive to read and changed often, though the logic is simple
watch 257 files changed often but cheap to read
quiet 322 files rarely touched
inert 250 files untouched in this window — leave it alone
Ranked by what re-reading them has cost, in tokens
File Verdict Changes Cx/100 Tokens Re-read Cost
packages/react-…ackend/fiber/renderer.js refactor 151 20.0 64k 9.6M $28.83
packages/react-…src/ReactFlightServer.js refactor 134 17.3 56k 7.5M $22.41
packages/react-…t/ReactFiberConfigDOM.js refactor 114 16.7 54k 6.1M $18.39
The thresholds are quantiles of the repository being measured, not fixed numbers:
dense and costly are its 75th percentile of density and tokens,
and changed often is the median change count among files that changed,
floored at 2. Verdicts are therefore relative — every repository has a top quartile — and
the thresholds are printed so any verdict can be checked against them.
Two things were cut after measuring them. Temporal coupling — which files change together
— found 432 pairs on react and zero on bootstrap, too repository-dependent to put
in default output. And files with no decision points at all are excluded, because before
that react's scripts/error-codes/codes.json came back as split:
true in the arithmetic, useless as advice.
5.7.0 — 95.7% of the install was a validator
zod existed in clines to validate clines.json: thirteen fixed
fields, twelve string[] and one boolean. It used six of zod's
exports — array, string, boolean,
object().strict(), .default() and ZodError. No
refinements, transforms, unions, coercion or async.
A clean npm install --omit=dev, before and after:
| install | files | config import | heap | |
|---|---|---|---|---|
| 5.6.0 | 5,368 KB | 611 | 4.1 ms | 1,263 KB |
| 5.7.0 | 232 KB | 15 | 1.1 ms | 204 KB |
Part of that was drift nobody noticed: from 3.25, zod's v3 package began shipping the
entire v4 and v4-mini codebases plus TypeScript sources, taking it from 0.67 MB to 3.43 MB
packed, and a ^3.24.1 range floated onto it.
Pinning back to ~3.24.4 was the obvious cheap fix, and it was measured and
rejected. For a consumer already on zod 3.25 or 4.x, npm cannot dedupe a non-overlapping
range, so it installs both copies —
5,968 KB across two zods, worse than doing nothing — and 3.24.x has had no release
since May 2025, so a future advisory would land above the pinned range.
The replacement is 133 lines with no dependency. The interesting part is how it was checked: a 59-case table generated from the zod build before zod was deleted — 14 accepted configs compared field by field, 45 rejected ones compared against the exact error string clines prints. Building it caught three divergences that would otherwise have shipped:
| input | zod | first attempt |
|---|---|---|
{"respectGitignore": null} |
rejects | silently defaulted to true |
{ignore: {folders: […], dirs: "bad"}} |
field error, then unknown key | unknown key first |
Object.create({inherited: true}) |
flags the inherited key | did not see it |
The first is the one that mattered: ?? treats null as absent,
while zod's .default() only fires on undefined. A config saying
do not respect .gitignore — written wrongly, but written deliberately — would have
been accepted and inverted. Every config error message clines prints is byte-identical to
what 5.6.0 printed, and the exit code is still 1.
Upgrading
npm install --save-dev clines@5.7.0
No breaking changes. Two things to expect: if you had left clines' own HTML reports inside
a repository you measure, your counts will drop to their correct values (5.5.0), and
userConfigSchema is still exported for anyone using clines as a library,
alongside the new parseUserConfig and ConfigError.
clines refactor needs a git repository. Everything else works without one.