Skip to content
norandomPublic

About

Lesscode is the new leetcode. Less is more. For code bases that are short and handy, precise and dandy.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Lesscode

DOI Release License: MIT

Before: a tired developer eats a plate of spaghetti labelled with nested loops and conditions. After lesscode: the same developer, cheerful, with a bowl of vegetables labelled with single statements.

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.

How it works

Three things you do: see where your tests guard the code, pin and then simplify one function, and let one check decide on the branch before you push

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.

  1. 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.
  2. 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.
  3. The change lands on a Git branch and your tests decide. Your test suite runs on the branch. Then lesscode-core plants 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.

What you can do with it today

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.

Install

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 | sh

Windows (PowerShell):

irm https://github.com/norandom/lesscode/releases/latest/download/install.ps1 | iex

Then 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.1 installs that release instead of the latest.
  • LESSCODE_INSTALL_DIR=/some/dir changes the target directory.
  • LESSCODE_ADD_TO_PATH=1 (Windows only) appends the directory to your user PATH. Without it the script prints the directory and leaves PATH alone. 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.

Build from source

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.toml

The binary is written to native/target/release/lesscode-core (lesscode-core.exe on Windows), or under CARGO_TARGET_DIR if you set it.

Try it on one change

Pick a rewrite you can already review by hand: the file before and the file after. Keep the receipts outside your source tree.

1. Measure the reduction

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

  1. 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.

2. List the write sites

lesscode-core deconvolute analyze --lang typescript \
  --source src/session.ts --out receipts/analysis.json

The 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.

3. See which faults your tests catch

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.json

The 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.

4. Hold files to size ceilings (Rust only so far)

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.json

Paths 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.

5. Stop a push that breaks the ceilings

lesscode-core deconvolute install-hook

This 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-core by name. If the binary is not on PATH, every push fails with lesscode-core: not found until you fix PATH or delete .git/hooks/pre-push.
  • LESSCODE_SKIP_HOOK=1 git push skips 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.

6. Refactor in checked steps

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 ../work

The 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.

What is not checked

  • No command establishes that a rewrite behaves the same as the original. deconvolute run measures 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 invalid and 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-pack accepts.
  • 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-hook writes checks complexity budgets only, and from release 1.0.2 it treats every branch the same. The hook that step install-hook writes also runs the step guard. Version 1.0.1 prints a note on refactor/* and deconvolute/* branches but performs no additional analysis on them.
  • Receipts written with --out may 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.

Try it with one reviewer first

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.

Evidence

The project keeps a dated research log with the experiments, the failed attempts and the revisions to its hypotheses.

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

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.

Where the project is going

  1. 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.
  2. 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.
  3. Deterministic rewrites and synthesis: extend concolic extraction and bounded synthesis to more language semantics and idiomatic transformations.

License

Lesscode is licensed under the MIT License. Third-party dependencies and fixtures retain their own licenses and notices; see the native dependency inventory.

Citation

Releases are archived on Zenodo; the DOI badge above resolves to the latest archived release. Citation metadata is in CITATION.cff.

About

Lesscode is the new leetcode. Less is more. For code bases that are short and handy, precise and dandy.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages