How Code Evolution Lab evolves candidate code fixes using a genetic algorithm.
This applies to web and API scans only. The CLI and GitHub Action detect issues but do not generate or evolve fixes.
It also applies to a subset of issues. Evolution runs only for issues of type
n_plus_1_query, nested_loops, nested_array_methods or await_in_loop, or
for any issue of high or critical severity. Other issues receive heuristic
solutions only.
What it does
Most linters give you one suggested fix. Code Evolution Lab starts from several candidate fixes derived from your actual code, scores them, and then breeds and mutates them across a few generations, keeping what scores well.
The result is a small set of ranked alternatives rather than a single answer, each with a fitness score you can use to judge how much the tool likes it.
Two things are worth being clear about up front. Candidates are derived from your original code by transformation, not assembled from generic templates — the engine tries to preserve your variable names and structure. And every candidate is parsed and syntax-checked before it can enter the population, so invalid code is discarded rather than ranked.
The loop
- Initial population. Start from the solutions the relevant generator produced for this issue, then fill out the population with mutated variants of them until it reaches the configured size.
- Evaluate fitness. Score every candidate (see below).
- Check convergence. Stop if the population has run out of diversity, or the generation cap is reached.
- Select parents. Tournament selection: sample 3 candidates at random, take the best, repeat. The number of pairs is population size × crossover rate ÷ 2.
- Crossover. Split two parents at a random statement boundary and splice them. Duplicate declarations are auto-fixed; if the child does not parse, the first parent is returned unchanged.
- Mutate. With probability equal to the mutation rate, apply one random mutation operator.
- Select survivors. Keep the top N by fitness (elitism), then fill the rest by roulette wheel — higher fitness means a better chance, but not a guarantee.
After the final generation, the top 5 candidates are returned, then capped to your tier’s solutions-per-issue limit.
If evolution throws at any point, or produces an empty population, the engine falls back to the generator’s heuristic solutions. A scan does not fail because evolution failed.
Convergence
Evolution stops on whichever comes first:
- Generation cap reached.
- Diversity collapse — when the gap between the best and average fitness in the population falls below the convergence threshold. This deliberately measures spread within a generation rather than improvement between generations: once candidates have become too similar to one another, breeding them produces nothing new, so there is no value in continuing.
- Time limit —
EVO_MAX_TIME_MS, 30 seconds per issue.
Mutation operators
Five operators are active. One is tried at random; if it fails, another is tried, until one succeeds or all have failed.
| Operator | What it changes |
|---|---|
| Query parameter | Adds select, take, or include to a database query |
| ORM method | Swaps a method for a related one (findMany ↔ findFirst) |
| Add optimization | Injects caching or batching |
| Cache TTL | Varies a cache time-to-live value |
| Index columns | Varies the columns in an index hint |
A sixth, variable renaming, exists in the source but is disabled — it produced cosmetic changes with no effect on fitness.
Every mutation result is validated before use. A mutation that produces unparseable code is discarded and the candidate passes through unchanged.
Fitness
Each candidate is scored 0-100 across four weighted criteria:
| Criterion | What it looks at |
|---|---|
| Performance | Query reduction, async patterns, remaining issues |
| Complexity | Cyclomatic complexity, nesting depth |
| Maintainability | Code length, comments, naming |
| Compatibility | Framework match, syntax validity |
The weights are configurable through four presets, chosen with the
FITNESS_WEIGHT_PRESET environment variable:
| Preset | Performance | Complexity | Maintainability | Compatibility |
|---|---|---|---|---|
balanced (default) |
0.35 | 0.25 | 0.25 | 0.15 |
performance |
0.55 | 0.15 | 0.15 | 0.15 |
maintainability |
0.20 | 0.20 | 0.45 | 0.15 |
enterprise |
0.25 | 0.20 | 0.35 | 0.20 |
Invalid code scores 0.
Configuration
EVO_ENABLE_ALGORITHM=true # Enable evolution; off, falls back to heuristics
EVO_POPULATION_SIZE=20 # Candidates per generation
EVO_MAX_GENERATIONS=10 # Maximum iterations
EVO_MUTATION_RATE=0.3 # Mutation probability (0-1)
EVO_CROSSOVER_RATE=0.7 # Crossover probability (0-1)
EVO_ELITISM_COUNT=2 # Best candidates preserved each generation
EVO_CONVERGENCE_THRESHOLD=0.01 # Diversity floor before stopping
EVO_TOURNAMENT_SIZE=3 # Candidates sampled per tournament
EVO_MAX_TIME_MS=30000 # Time cap per issue
FITNESS_WEIGHT_PRESET=balanced # Fitness weighting preset
Population size, max generations, and convergence threshold are overridden per analysis by your tier, so the values above do not describe what runs for a given scan. The remaining variables apply globally.
| Parameter | Free | Pro |
|---|---|---|
| Population size | 10 | 15 |
| Max generations | 5 | 10 |
| Convergence threshold | 0.03 | 0.03 |
| Solutions per issue | 3 | 5 |
Progress events
The engine emits a progress event after scoring each generation, which is
what drives the live view during a web scan:
engine.on('progress', (data) => {
console.log(`Generation ${data.generation}/${data.maxGenerations}`);
console.log(`Best fitness: ${data.bestFitness}, average: ${data.avgFitness}`);
});
The payload carries generation, maxGenerations, bestFitness,
avgFitness, the best candidate’s code and fitness, and the fitness and
generation of every member of the population.
See Server-Sent Events for how these reach the browser.
What you get back
Each evolved solution carries its fitness score, its rank, the transformation type it came from, how many generations it went through, how many mutations were applied, an estimated implementation time, and a risk level. Risk is derived from how much of your original code survived — candidates preserving three or more original elements are marked low risk.