Lesscode is for making a grown codebase smaller without losing features. Code that was written by hand, or by a model, often repeats what the project's framework, its libraries or the language already provide. Lesscode helps you find such places and check the change you make there.
A change counts when there is less code and less structure to maintain for the same features and the same public contracts. Moving code into another module, hiding it behind a wrapper or dropping a feature does not count. What a replacement brings with it counts too: its dependencies, its configuration and any difference in behaviour.
What you type and what you see. The function in the picture is from this repository and happens to be Rust; the same commands work on Python, TypeScript and Java. They are described in docs/steps.md, the reasoning in docs/testability.md.
- Take one tangled piece and write it simpler. Start with a function or module whose job you can name, and leave the rest of the codebase alone. The picture shows a real one from this repository, drawn simplified: the same checks and the same results, with the set-up done once and one way out.
- A model can do the rewriting, inside limits it cannot move. The public surface stays and the places that may write state are declared up front. "Smaller" is counted in tokens, so squeezing more onto a line does not count. Prompts are tuned by search (GEPA, MIPROv2) against mechanical checks, and what a model writes is used only after those checks let it through.
- The change lands on a Git branch and your tests decide. Your test suite runs
on the branch. Then
lesscode-coreplants small faults in the new code, one at a time, in copies of your tree, and reports which ones your tests notice. Where they notice nothing, a cut is not protected yet. You merge, or you don't.
Step 3 is in the binary today: deconvolute run plants the faults, and from
release 1.2.0 step check judges a change on its branch by exit code (both
below). Step 2 exists as a
research pipeline in this repository, not as a one-command workflow, and what
it has delivered so far is modest: candidates that pass their tests and are 6%
to 19% smaller in tokens (R87).
openidea-kiro has your back while you do this: it checks that the specification stays free of contradictions and that a change does not make the code harder to change. Lesscode is the step after that, less code for the same behaviour.
lesscode-core is one native binary for code written in Python, TypeScript,
Java or Rust. The four are treated alike: what the tool knows about a language
is a small data file, and it runs your project's own build and test commands,
whatever they are (pytest, the Node test runner, Maven, Cargo). You give it
source files you already have, and it writes JSON receipts you can read in a
review. No command changes
your sources, and none of the commands in this table makes a network or model
request. (The binary also holds the research commands named
further down; one of them, regenerate, sends a prompt
to a model endpoint you configure.)
| You want to know | Command | Works on |
|---|---|---|
| Is this rewrite a substantial reduction or a cosmetic one? | deconvolute density |
Python, TypeScript, Java, Rust |
| Where does this file decide behaviour and write state? | deconvolute analyze |
Python, TypeScript, Java, Rust |
| Which small faults can be planted in this file? | deconvolute mutate |
Python, TypeScript, Java, Rust |
| Which of those faults do my tests catch? | deconvolute run |
the same four, with your own build and test commands |
| After this small change, do my tests still pass and still catch planted faults as often as before? | step check, with step plan, pin, record, guard |
the same four, in a Git repository; from release 1.2.0 |
| Is this language pack fit for use? | deconvolute admit-pack |
any pack for a compiled-in grammar |
| Are my files still within their agreed size ceilings? | budgets |
Rust only so far |
| Can a push be stopped when those ceilings are broken? | deconvolute install-hook |
a Git repository with a budgets file (Rust only so far) |
deconvolute run and step plan, step pin and step check are the commands
that execute your project's commands. They do so in copies of your source tree
under a work directory you name, never in the tree itself, and they refuse a
work directory inside or around the source. step check and step record
write three files into your repository, at the places your step declaration
names: a verdict, an evidence file and a ledger. Everything else only reads. analyze, mutate and run share one engine,
described in the agnostic driver; what it knows about a
language is a data file of at most 40 lines (native/packs/), not code.
Read what is not checked before you rely on a result.
Releases before 1.1.0 had different analyze and mutate commands, bound to
this project's experiment specimens; their limits are in
R55.
Releases carry prebuilt binaries for five targets. The scripts below download the archive for your machine from the latest release, verify its SHA-256 digest and copy the binary into a directory you own. They do not use administrator rights.
Linux and macOS:
curl -fsSL https://github.com/norandom/lesscode/releases/latest/download/install.sh | shWindows (PowerShell):
irm https://github.com/norandom/lesscode/releases/latest/download/install.ps1 | iexThen check the installation:
lesscode-core --version| Platform | Release archive | Default install directory |
|---|---|---|
| Linux x86_64 | lesscode-core-x86_64-unknown-linux-gnu.tar.gz |
~/.local/bin |
| Linux aarch64 | lesscode-core-aarch64-unknown-linux-gnu.tar.gz |
~/.local/bin |
| macOS Apple Silicon | lesscode-core-aarch64-apple-darwin.tar.gz |
~/.local/bin |
| macOS Intel | lesscode-core-x86_64-apple-darwin.tar.gz |
~/.local/bin |
| Windows x86_64 | lesscode-core-x86_64-pc-windows-msvc.zip |
%LOCALAPPDATA%\Programs\lesscode-core\bin |
Options, set as environment variables before running the script:
LESSCODE_VERSION=1.0.1installs that release instead of the latest.LESSCODE_INSTALL_DIR=/some/dirchanges the target directory.LESSCODE_ADD_TO_PATH=1(Windows only) appends the directory to your userPATH. Without it the script prints the directory and leavesPATHalone. On Linux and macOS the script never edits shell profiles.
The Linux binaries are built on Ubuntu 24.04 and need glibc 2.39 or newer. On
an older distribution, or on any other platform, build from source. You can
also download an archive and its .sha256 file by hand from the
releases page; each
archive contains the binary, the license and the third-party notices.
The install scripts check the SHA-256 digest of the download. From release 1.0.2 on, archives are also signed with the project's Ed25519 key and carry a GitHub provenance attestation; release signing shows how to verify them. Release 1.0.1 itself is not signed.
The tool itself is written in Rust, whatever language you use it on. To build it you need Rust 1.98.1 and a C compiler. native/README.md lists the per-platform prerequisites.
cargo +1.98.1 build --release --locked --manifest-path native/Cargo.tomlThe binary is written to native/target/release/lesscode-core
(lesscode-core.exe on Windows), or under CARGO_TARGET_DIR if you set it.
Pick a rewrite you can already review by hand: the file before and the file after. Keep the receipts outside your source tree.
lesscode-core deconvolute density --lang python \
--baseline before/ratelimit.py --candidate after/ratelimit.py \
--out receipts/density.json--lang is python, typescript, java or rust. The command measures both
files by their syntax, so that layout cannot change the result: tokens are
the names, literals, operators and punctuation outside comments, nodes the
named syntax nodes. This repository keeps one complete pair you can run it on,
a Rust one (native/adapters/rust/baseline_lifecycle.rs and
candidate_lifecycle.rs, with --lang rust). Its receipt reads:
{
"receipt": "deconvolution-density-v2",
"baseline_size": { "tokens": 1846, "nodes": 1460, "lines": 249 },
"candidate_size": { "tokens": 1716, "nodes": 1350, "lines": 149 },
"token_reduction_pct": 7.04,
"tokens_removed": 130,
"node_reduction_pct": 7.53,
"line_reduction_pct": 40.16,
"thresholds": { "min_token_reduction_pct": 20.0, "min_tokens_removed": 400, "min_node_reduction_pct": 10.0 },
"passed": false,
"verdict": "marginal"
}This pair is the reason the command works this way. The candidate has 40%
fewer lines and 7% fewer tokens: most of what looked like a reduction was
denser layout. Releases up to 1.1.0 counted lines and reported this pair as
pass (R86).
The project's rewrites in the other languages measure the same way: 5.6%
fewer tokens for the Java one, 7.5% and 9.8% for two Python ones and 18.8%
for the TypeScript one, where 24% to 44% fewer lines had been reported
(R87).
pass needs at least 20% fewer tokens, at least 400 tokens removed, and at
least 10% fewer nodes. Anything less is reported as marginal with exit code
- A marginal verdict says the change is small; it does not say the change is wrong. Lines are in the receipt for information and do not count. A file that does not parse is refused with exit code 3.
lesscode-core deconvolute analyze --lang typescript \
--source src/session.ts --out receipts/analysis.jsonThe receipt lists every place the file decides behaviour (sites), each state
write as file:line, the function boundaries, and each construct the
language pack declares it cannot follow (for example unsafe code) under
gaps. With --frame-spec FILE you supply a JSON file whose
allowed_write_sites lists the sites you allow (the file also needs
permitted_shell_locations and failure_invariants, which may be empty
lists), and any other site is reported under violations. Exit code 0 means no gaps and no
violations; 1 means at least one.
Write a run configuration that names the files, your test command and, if needed, setup and build commands (the fields are in the agnostic driver):
{
"language": "python",
"source_root": "../my-project",
"files": ["pkg/ratelimit.py"],
"test_paths": ["tests/"],
"setup": [["uv", "sync", "--quiet"]],
"test": ["uv", "run", "--no-sync", "python", "-m", "pytest", "tests/test_ratelimit.py", "-q", "-x"],
"runner_evidence_patterns": [" passed", " failed", " error"],
"invalid_exit_codes": [2, 3, 4, 5]
}lesscode-core deconvolute run --config run.json --work /some/scratch/dir \
--workers 6 --out receipts/run.jsonThe driver copies the source tree into the work directory once per worker,
checks that your tests pass there unmutated, plants one fault at a time, runs
your commands, restores the file and verifies the restoration. The receipt
classes every fault as killed, survived, invalid, timeout or error.
Exit code 0 means the run is complete, 2 that some faults ended in error, 3
that the run is void and nothing in it may be relied on. Stopping the run with
Ctrl-C kills your commands, restores the copy and ends the run as void; faults
already classed are kept and reused by the next run.
This is the one check that does not cover the other three languages yet.
Write a budgets file that names every .rs file under the root you govern:
{
"files": [
{ "path": "lib.rs", "owner": "core team", "allocation": "public arithmetic API" }
]
}lesscode-core budgets --config complexity-budgets.json --root src --out receipts/budgets.jsonPaths are relative to --root. Size is counted in tokens outside comments and
test code, so reformatting cannot move a file over or under its ceiling. Each
file defaults to 2,800 tokens, 300 syntax units and 560 tokens per function
(about 400 and 80 lines); set max_tokens, max_logical_units or
max_fn_tokens per file to change that. An entry written for release 1.1.1
or earlier, with max_physical_lines or max_fn_lines, is still held to
those ceilings in lines until you replace them. A .rs file that is not
listed fails the check, as does a listed file that is missing or over a
ceiling. Exit code 0 means every file is within budget. This
repository's own file is native/complexity-budgets.json.
lesscode-core deconvolute install-hookThis writes .git/hooks/pre-push. On each push the hook looks for
complexity-budgets.json with a src directory at the repository root (or
native/complexity-budgets.json with native/src) and runs the budget check.
A violation blocks the push and names the files.
Know these three things before you install it:
- The hook calls
lesscode-coreby name. If the binary is not onPATH, every push fails withlesscode-core: not founduntil you fixPATHor delete.git/hooks/pre-push. LESSCODE_SKIP_HOOK=1 git pushskips the check for one push. It only works when the binary can be found.- In a repository without a budgets file the hook passes without checking anything.
install-hook refuses to replace an existing pre-push hook unless you pass
--force.
From release 1.2.0. A step is one small change on one branch with one small spec. You declare it in a file, commit the change, and ask:
lesscode-core step check --declaration .kiro/specs/one-exit/step.json --work ../workThe declaration names the language and your own test command, so the same
command judges a pytest project, a Node one, a Maven one or a Cargo one. Exit
code 0 means: the change stays in the files you declared, the scope did
not grow beyond the limit you set, your tests pass before and after, and in no
function do more planted faults go unnoticed than before. Exit code 1 refuses
the step and names the reason. step record puts an accepted step on a ledger
in your repository, and step guard, as a pre-push hook or a pipeline job,
lets a step branch through only with a verdict for exactly the committed code,
tests and declaration.
Before a change, step plan shows per function which planted faults your
tests notice, and step pin tells you whether a new test pins one they did
not. The tooling writes neither tests nor refactorings; it judges what a person
or a model wrote. The whole procedure, with real steps taken in TypeScript,
Java and Rust projects, is in docs/steps.md; an agent skill that walks the phases is
refactor-in-steps. The skill
cannot accept a step; only the exit codes can.
- No command establishes that a rewrite behaves the same as the original.
deconvolute runmeasures how sensitive your tests are to planted faults: a fault they do not catch is a place where a cut is not protected. A run in which every fault is caught is not an equivalence proof. - The planted faults are these: a negated or altered condition, a removed failure signal, a dropped state write, two adjacent checks swapped, and a changed call argument. Other faults (an element dropped from a list, a removed chained call, what a process prints) are not planted.
- In typed languages a share of the planted faults does not compile. They are
reported as
invalidand say nothing about your tests; each still costs a build. - Speed is your project's own build and test time per fault. Measured here: under a second per fault for a Python file, several seconds for Java, and five to twenty seconds for Rust, which rebuilds the crate each time.
- Copies must not share a build cache, in any language. The case we hit is
Cargo with a global
CARGO_TARGET_DIR: set"env": {"CARGO_TARGET_DIR": "target"}in the run configuration, otherwise copies run each other's builds and the run is voided (details). A build tool that keeps its output outside the project directory needs the same care. - The step commands have been used on real steps in Python, TypeScript, Java and Rust projects, one to two steps each. That is a small sample.
- Four languages have a pack. A fifth needs its grammar compiled into the
binary and a pack that
admit-packaccepts. - The budget check reads the files in your working tree, not the commits being pushed. It is the one command that covers Rust only.
- The hook that
deconvolute install-hookwrites checks complexity budgets only, and from release 1.0.2 it treats every branch the same. The hook thatstep install-hookwrites also runs the step guard. Version 1.0.1 prints a note onrefactor/*anddeconvolute/*branches but performs no additional analysis on them. - Receipts written with
--outmay be placed anywhere, including inside your source tree, and an existing file is overwritten. - An accepted step is not shown to behave the same. It passed your tests and lost no fault sensitivity in the functions that were run. What the step commands cannot check at all is listed in docs/steps.md.
The research pipeline behind the published experiments (recorded behavior,
bounded finite-domain comparison, mutation scoring against real test suites,
model-driven regeneration) lives in the library and in the prepare, compare,
admission, surface and regenerate commands. Those need frozen manifests,
recorded evidence and per-specimen setup, and regenerate sends prompts to a
model endpoint you configure. Start from the
working paired example if you want to
use them; they are not a one-command workflow yet.
Use the first trial to find out whether the receipts help a review you already
do. Choose one module whose owner and regular reviewer are both willing, agree
beforehand what a marginal or failing receipt will mean, and compare the
receipt with the diff. The adoption guide describes that
trial, when to stop, and what to settle before turning a check into a gate.
The project keeps a dated research log with the experiments, the failed attempts and the revisions to its hypotheses.
- Research log: the latest decision first, then the record by date.
- Multi-language synthesis (R53): four specimens in Python, TypeScript, Java and Rust, reported with 24% to 44% fewer lines. Measured by syntax the same candidates have 6% to 19% fewer tokens (R87). Its behavioural figures (100 compared cases, all adverse controls killed) are weaker than they read: see the next entry.
- The specimen controls rerun through the driver (R75): what the earlier "4 of 4 killed" figures were, and what the project's own tests catch when the driver plants faults in the same code.
- Three real steps through the step commands (R92): one each in Rust, Java and TypeScript, all accepted, with the numbers before and after.
- A step that was refused, pinned and then accepted (R93), and the fault in the driver that it uncovered.
- Native driver on five real scopes (R73):
deconvolute rungives 556 of 556 mutants the class the Python prototype gave. - Self-application (R54): the native test suite on a clean clone and the pre-push hook tests.
- Released binary on this repository (R55): what an adopter's first session finds, including the gaps listed above.
- Proxy routing trial: a real replacement that removed 23 lines with 1,172 tests passing.
Those results describe specific specimens under finite checks. They are not a claim that the tool will reduce your code by a similar amount, and they are not equivalence proofs.
- Documentation index
- Native core: build, prerequisites, licenses
- Architecture and how to add a language
- CI and release workflows
- Agent skill for deconvolution
- Refactoring in checked steps, why it works this way, and the agent skill
Native checks run on Linux x86_64 and aarch64, Windows x86_64, and macOS on
Intel and Apple Silicon. The earlier Python prototype (lesscode-baseline) was
retired at commit fd0b5b9; its guides remain under docs/ and are marked as
archived.
- Steps that are harder than the ones taken so far: functions no test reaches, which need a fake before they can be pinned, and per-test evidence that openidea-kiro can accept for a step's spec.
- Slice B, hardening against drift in model output: wherever a model writes something the tool then relies on (today: a language pack), measure how much its answers vary between runs and between models, and keep a mechanical barrier that admits only what passes. Measured on two models with two prompt-optimisation engines and closed for now; see the closing summary.
- Deterministic rewrites and synthesis: extend concolic extraction and bounded synthesis to more language semantics and idiomatic transformations.
Lesscode is licensed under the MIT License. Third-party dependencies and fixtures retain their own licenses and notices; see the native dependency inventory.
Releases are archived on Zenodo; the DOI badge above resolves to the latest archived release. Citation metadata is in CITATION.cff.
