> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Adaptive model depth — the tier ladder, upgrade path & promotion guards

> The scorecard → bayesian → logistic_regression → gradient_boosted evolution ladder, the EvolutionConfig readiness thresholds and manual /upgrade endpoint, plus the auto-rollback promotion guard and TS-side preprocessing orchestrator.

## Evolution depth — the model tier ladder

"Model depth" is the algorithm-complexity tier a model sits at. Kaireon defines an evolution ladder so a model can graduate from a simple, data-cheap algorithm to a richer one as it accumulates evidence and demonstrates lift.

### The tier ladder

`ALGORITHM_TIERS` (`platform/src/domain/algorithms.ts`) lists six tiers:

```
scorecard → bayesian → logistic_regression → gradient_boosted → thompson_bandit → epsilon_greedy
```

In practice the **auto-upgrade path covers only the first four** — `scorecard → bayesian → logistic_regression → gradient_boosted`. The `/upgrade` endpoint returns `400 No upgrade available` once a model reaches `gradient_boosted`, and the readiness thresholds below stop there too. `thompson_bandit` and `epsilon_greedy` are model types you create directly, not rungs the upgrade ladder climbs to.

### Readiness thresholds — `EvolutionConfig`

`EvolutionConfigSchema` defines a per-model, nullable config (defaults apply when unset):

| Field         | Default              |
| ------------- | -------------------- |
| `autoEvolve`  | `false`              |
| `currentTier` | `"scorecard"`        |
| `targetTier`  | `"gradient_boosted"` |

The `thresholds` block gates each promotion on BOTH a minimum sample count and a minimum AUC:

| Promotion               | Samples required                       | AUC required                       |
| ----------------------- | -------------------------------------- | ---------------------------------- |
| → `bayesian`            | `bayesianSamples` = **500**            | `bayesianAuc` = **0.65**           |
| → `logistic_regression` | `logisticRegressionSamples` = **5000** | `logisticRegressionAuc` = **0.75** |
| → `gradient_boosted`    | `gradientBoostedSamples` = **20000**   | `gradientBoostedAuc` = **0.80**    |

### Reading readiness — `GET /evolution-history`

`GET /api/v1/algorithm-models/{id}/evolution-history` reports where a model sits on the ladder and how close it is to the next rung:

* `progress.progressPct` — the average of the sample-progress % and AUC-progress % toward the next tier (each capped at 100).
* `progress.readyToEvolve` — `true` once `progressPct >= 100` (both sample and AUC targets met).
* `timeline[]` — the model's version history with the model type at each version.

### Triggering the upgrade — `POST /upgrade` (manual)

Promotion is a **manual** action — `POST /api/v1/algorithm-models/{id}/upgrade` (RBAC admin/editor). It:

1. Creates a **new `draft` model** of the next tier (`key: "{key}-upgraded-{newType}"`), seeded with that tier's default hyperparameters and the current model's `targetField`, `targetSchemaKey`, and `predictors`.
2. With `{"createExperiment": true}` in the body, also creates a **draft champion/challenger `Experiment`** (50/50 split) with the current model as champion and the upgraded model as challenger.
3. Returns `{ upgraded, experiment }` with `201`.

The upgraded model starts as `draft` and inert — you still run it through the [lifecycle](/ai-ml/model-lifecycle) to make it score.

<Warning>
  **`autoEvolve` is advisory only — nothing auto-promotes.** The `autoEvolve` flag persists (schema, `PUT`, and the tier-progression UI toggle) and `evolution-history` reports `readyToEvolve`, but **no background job reads `autoEvolve` to trigger an upgrade**. Crossing a threshold does not create the next-tier model on its own — an operator must call `POST /upgrade`. Automatic tier promotion is roadmap work.
</Warning>

## Auto-rollback guard on champion promotion

A champion promotion that includes a metrics snapshot (AUC and
error rate) now consults the auto-rollback evaluator BEFORE the
transaction that demotes the incumbent.

### Trigger

The guard fires only when:

1. `toStatus === "champion"`, AND
2. `metricsSnapshot.auc` is supplied by the caller, AND
3. The incumbent champion in the same family has a stored
   `metricsSnapshot.auc` to compare against.

If any of those is missing, the guard silently skips — there's no
honest comparison to make.

### Thresholds

| Threshold      | Default           | Source             |
| -------------- | ----------------- | ------------------ |
| `maxAucDrop`   | 5 % relative drop | `evaluateRollback` |
| `maxPsi`       | 0.25 per feature  | `evaluateRollback` |
| `maxErrorRate` | 2 %               | `evaluateRollback` |

Override per call via `rollbackThresholds`. To force a promotion that
breaches a threshold, pass `bypassRollbackGuard: true` — the
returned object surfaces `rollbackGuardBreaches` so the bypass is
auditable.

### Failure mode

When the guard fires and bypass is **not** set, `promoteModel`
throws `ModelRegistryError(...)` with reason `rollback_guard`. The
incumbent champion stays in place; no DB state changes.

## TS-side preprocessing orchestrator

`lib/ml/preprocessing.ts` bundles WOE binning + target encoding into
a single `fitPreprocessing` / `applyPreprocessing` API. Why TS-side
instead of porting into `gbm_trainer.py`:

1. Most Kaireon scoring engines (bayesian, thompson, online,
   epsilon, scorecard) live entirely in TS and never cross the
   Python boundary. A TS-side bridge makes binning + encoding
   available to **all** engines, not just GBM.
2. Keeps W6.2 testable + deterministic without a Python service in CI.

### Honest limit

V1 fits encoders only. Saving / loading them across train + score
boundaries is the caller's responsibility — `AlgorithmModel.modelState`
is the natural home but is not yet auto-populated from this orchestrator.
The Python `gbm_trainer.py` path IS wired, though: `serializeFitForGbmTrainer()`
emits the `{bin_edges, target_encodings}` payload the ml-worker `/train/gbm`
route accepts, so a caller can ship the fit alongside the training data and
keep Python-side training aligned with TS-side scoring.
