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

  1. 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.
  2. Evaluate fitness. Score every candidate (see below).
  3. Check convergence. Stop if the population has run out of diversity, or the generation cap is reached.
  4. Select parents. Tournament selection: sample 3 candidates at random, take the best, repeat. The number of pairs is population size × crossover rate ÷ 2.
  5. 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.
  6. Mutate. With probability equal to the mutation rate, apply one random mutation operator.
  7. 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:

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 (findManyfindFirst)
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.

Next steps