Part of lesscode-core from release 1.1.0. It replaces the text-matching analysers and operators of the
releases before, which were bound to this project's experiment specimens. Spec:
.kiro/specs/agnostic-annotation-driver/. Evidence: research entries R65, R67 and R73 under
docs/research/2026-11-10/ and R75, R80 and R81 under docs/research/2026-11-11/.
The driver finds the places in a source file where behaviour is decided, plants small faults there (mutants), and can run a project's own tests to see which faults they catch. Everything it knows about a language is in a data pack; the same engine serves Rust, Python, TypeScript and Java.
All are subcommands of lesscode-core deconvolute. Exit codes: 0 success, 1 finding, 2 incomplete, 3 bad
input, unsupported language or a void run.
| Command | What it does | Writes to the source tree |
|---|---|---|
analyze --lang L --source F [--frame-spec S] [--out R] |
Lists the sites of a file by kind, the state-write sites, the function boundaries and the constructs the pack declares as gaps. Exit 1 on a gap or on a write outside the frame specification | no |
mutate --lang L --source F --out-dir D [--out R] [--limit N --seed S] |
Writes every valid mutant of a file into D, with a receipt naming site, operator and hash. Exit 1 when there is none |
no |
run --config C --work W [--workers N] [--annotations A] [--no-reuse] [--only N,...] --out R |
Mutates the configured files in copies under W, runs the project's commands against each mutant and classes it. With functions in the configuration (per file, a list of function names) only sites inside those functions are mutated; --only runs the mutants with the given numbers of a run with the same configuration. Both from release 1.2.0. Exit 2 when errors remain, 3 when the run is void |
no; only the copies change, and each is restored and verified |
admit-pack --pack P --lang L [--controls K --source-root S] [--out R] |
Checks a language pack before it is used. Exit 1 when it is rejected | no |
merge-annotations --config C [--min N] --out M LIST... |
Validates several annotation lists and writes the annotations of the sites that at least N lists name (default 2), for use with run --annotations. Exit 1 when no site is kept. From release 1.1.1 |
no |
run is the one command of this group that executes a project's commands. It refuses a work directory that
is the source root, lies inside it or contains it. The step commands plan, pin and check
(refactoring in checked steps, from release 1.2.0) run the same engine under the same rule.
run --annotations A mutates only the sites a list of annotations names; each annotation is validated and a
rejected one never becomes a mutant. Such a list usually comes from a model, and a model does not name the
same sites twice: two runs on one file shared 62% to 86% of their sites in one measurement, and the sites
that at least two of three runs name shared 74% to 96% (R83, R85). merge-annotations builds that steadier
list. It is steadier, not fixed. Without annotations the driver mutates every eligible site, and that
selection does not vary.
| Kind | What it is | Operators |
|---|---|---|
branch_condition |
A condition that decides control flow, or a returned yes-or-no expression | negate; swap paired operator tokens |
failure_state |
A place that signals failure to the caller | remove or rewrite the signal; swap a returned literal |
state_write |
An assignment, an in-place update, or a call that stands alone as a statement | drop the statement |
error_order |
A check that is directly followed by another check | swap the two |
pure_shell_boundary |
A function or method definition | none; reported by analyze |
call_argument |
The argument list of a call (a pack may leave this kind out) | swap a literal argument for its partner; empty a string argument; swap the first two arguments (Python and TypeScript packs only: in Java and Rust the swap rarely builds). At most two literal changes and one swap per call |
A mutant that does not parse, or differs from the original only in comments or white space, is not written and not run. A swap that says the same as the negation at the same site is left out.
| Class | Meaning |
|---|---|
killed |
The tests failed |
survived |
The tests passed: a gap in the tests, or a change that makes no difference |
invalid |
The mutant did not build, or the tests could not be loaded |
timeout |
A step reached its time limit |
error |
The command could not start, the machine ran out of a resource, or the test runner left no sign that it ran. This says nothing about the mutant; it is retried once and never stored |
A run is void, and nothing in it may be relied on, when the unmutated tests do not pass before or after the mutants, when a mutant could not be run by any copy, when a restoration could not be verified, or when the run was stopped from outside.
Stopping a run: on Ctrl-C, Ctrl-Break, a termination signal or a closing console the driver kills the
project command it is waiting for with everything that command started, writes the original file back into
the copy, writes the receipt as void and exits with code 3. Mutants classed before the stop stay in the
outcome store and are reused by the next run. A driver that is killed outright (kill -9, the task manager)
cannot do this; the file it had planted is then restored when the copy is next opened.
A JSON file. Commands are argument vectors; nothing is passed through a shell.
{
"language": "python",
"source_root": "../my-project",
"files": ["pkg/ratelimit.py"],
"test_paths": ["tests/"],
"setup": [["uv", "sync", "--quiet"]],
"build": null,
"test": ["uv", "run", "--no-sync", "python", "-m", "pytest", "tests/test_ratelimit.py", "-q", "-x"],
"env": {},
"exclusions": [".venv", "__pycache__"],
"timeout_floor_s": 20,
"timeout_ceiling_s": 300,
"timeout_factor": 5,
"invalid_exit_codes": [2, 3, 4, 5],
"invalid_output_patterns": [],
"error_output_patterns": [],
"runner_evidence_patterns": [" passed", " failed", " error"],
"timeout_exit_codes": [],
"workers": 6,
"serial_only": false
}source_rootis relative to the configuration file.setupruns once in each fresh copy.- A copy under
Wis used again by later runs, with what its builds left in it. Before that, the files that changed or are new in the source root are copied into it and the files that are gone are removed; if anything changed,setupruns again. This is new in release 1.2.0. Releases 1.1.0 and 1.1.1 used the copy as it was, so after a change to a test or to a file that is not mutated they ran against the old files. With those releases, use a fresh work directory after such a change. - The time limit of a step is
timeout_factortimes the slowest unmutated run of that step, at least the floor and at most the ceiling. An argument containing{limit_s}receives that limit in seconds; withtimeout_exit_codesthis lets a command that works inside a container stop itself and be classed as a timeout. - Patterns are plain text fragments, not regular expressions.
- Each copy must build into its own directory. The driver copies the source tree; it cannot see a build
cache that your tools share through the environment. Cargo with a global
CARGO_TARGET_DIRis such a case: two copies then share compiled units, a copy can run tests against another copy's mutant, and classes come out wrong (16 of 133 in one measured run, R75). Set"env": {"CARGO_TARGET_DIR": "target"}so that each copy builds inside itself, or run one copy withserial_only. The driver notices the damage only through the closing run of the unmutated tests, which then fails and voids the run; the receipt names the copy and shows the end of the failing output. - Outcomes are stored under
W/outcomesby the hash of the original file, the mutated text, the configuration and, wheretest_pathsare declared, the files under them; they are reused by later runs unless--no-reuseis given. A changed or added test file therefore makes the mutants run again (from release 1.2.0; a store written by an earlier release no longer matches and is left alone; the receipt records the hash astest_files_sha256). Two limits remain: withouttest_pathsthe tests are not part of the key, and a change to another source file that is neither mutated nor a test is not seen. In both cases use--no-reuseor a fresh work directory.
The configurations used for the five gate scopes are in
docs/experiments/annotation-driver/results/native/.
The shipped packs are native/packs/{rust,python,typescript,java}.json, at most 40 lines each. A pack names
the grammar's node kinds for each site kind and gives the operator templates. admit-pack checks size,
shape, that every node kind exists in the grammar, the template placeholders, and, with a control file,
that recorded annotations get their recorded outcome and that accepted ones yield a real change.
A control file is {"files": [...], "test_paths": [...], "cases": [{"label", "annotations": [...], "expected": [...], "operators": [...]}]}; operators is optional and names the operators a site must
offer. Hand-written control sets are in native/tests/fixtures/driver/controls/.
- Operators for an argument replaced by a value of another form, a dropped list element, a removed chained call, or what a process prints. Seven of the twelve hand-made controls of the earlier specimen gates had no driver mutant (R75); the call-argument operators added on 2026-11-11 reach one of them.
- A fifth language: it needs a grammar compiled into the binary and an admitted pack.
- Faster Rust runs: each mutant rebuilds the crate.