# The calibration lifecycle

The audited life of a qcal_ artifact, from the four one-way states and the per-segment activation gate to the refit triggers and read-time invalidation.

Source: https://tokentriage.com/docs/calibration/lifecycle

---
import Callout from '../../../components/Callout.astro';
import NextSteps from '../../../components/NextSteps.astro';
import CalibrationLifecycleDiagram from '../../../components/CalibrationLifecycleDiagram.astro';

A fit produces a `qcal_` artifact, but fitting is only the first event in its life. The artifact
moves through a small, one-way lifecycle; exactly one of its states can license a claim;
promotion is gated; a background scheduler keeps it fresh; and *every* move, including the ones
that were turned away, is written to an append-only log. This page is the lifecycle end to end.

## The four states

An artifact is content-addressed: its id is `qcal_<compactTS>_<sha8>` where the hash is taken
over the fit's own content, so a refit over the same window lands on the same id and a
*different* fit is always a *new* id. The store keeps one `<id>.json` per artifact plus a
`manifest.json` naming the `active` and `shadow` slots and every `versions[]`. Each artifact
carries a status, and the status moves one way:

```text
candidate → shadow → active → retired
```

| State | What it is | Can it serve a claim? |
|---|---|---|
| `candidate` | A fresh fit, **inert**. Writing one changes nothing else, which is what makes `quality calibrate` safe to run on a schedule. | No |
| `shadow` | Fitted *and* evaluated, but never served — the slot the scheduler populates so a fresh fit can be watched against production. | No |
| `active` | The one status that can license a probability claim. Activating an artifact retires the incumbent atomically. | Yes |
| `retired` | Terminal. A successor is a new content-addressed id, never the same bytes wearing a fresh status. | No |

Forward *skips* are legal — `candidate → active` is the ordinary operator action, and
`candidate → retired` lets you discard superseded evidence — but nothing ever moves backward.
The illegal moves are refused by name, each naming the from-state, the to-state, and the legal
moves that remain:

- **`active → shadow`** — demoting a serving artifact would leave the store with no active
  artifact and no record that one was removed. The right move is to activate its successor,
  which retires it by name.
- **`active → active`** — "already active" is information, not a no-op; silently succeeding would
  let an operator believe a promotion happened and write an audit row saying so.
- **`retired → anything`** — a retired artifact is history.

<CalibrationLifecycleDiagram />

## The activation gate

Activation is the one transition that is gated. The rule is **per segment**: an artifact may be
activated if *at least one* measured segment passed its own [B.5
gate](/docs/calibration/how-calibration-works#the-b5-gate). Segments that failed serve nothing —
but they do not block the ones that passed.

The strict reading — "every measured segment must pass" — is only ever a *disclosure*, and the
reason is arithmetic. Requiring `N` independent segments to each clear a per-segment
false-refusal rate β makes the artifact-level refusal rate `1 − (1 − β)^N`: about 14% over three
segments and 99.4% over a hundred. A real fleet is many segments by construction, so a strict
gate would refuse a *perfectly calibrated* deployment essentially always. Nothing is weakened by
the per-segment reading, because a segment that failed its own gate licenses nothing whether or
not the artifact around it is active — the per-segment gate is where the safety actually lives.

So only two things are refused at activation: an artifact that measured **nothing** (a fit over
an all-synthetic or all-thin window), and one whose **every** measured segment failed. Both
refusals carry the full reliability report.

<Callout type="warning">
Activation repoints the manifest and is audited immediately — but it does **not** move live
routing on its own. The proxy reads the active artifact at config-load time, so a newly
activated calibration takes effect on the next config reload. Both the management API response
and the studio disclose this.
</Callout>

## The refit scheduler

Calibrations go stale as traffic shifts, so a background loop — strictly off the request path,
paced by a ticker that checks roughly every five minutes — decides *when* to refit. Five events
can trigger a refit, and each one travels into the log as a **named** trigger from a closed
vocabulary:

| Trigger | Fires when |
|---|---|
| `drift_finding` | A registered drift detector reports that a segment's population moved. |
| `policy_epoch_change` | The policy fingerprint moved — the artifact was fit under an epoch no longer in force. |
| `staleness` | The artifact passed the freshness horizon (30 days). |
| `operator_request` | A human asked, with a reason that becomes the log entry's detail. |
| `scheduled_interval` | The configured `refit_interval` elapsed. (Omit it, or set `0s`, and there is no periodic refit — the other four triggers are unaffected.) |

Each trigger fires as an **edge**, not a level: once per `(artifact, epoch)` rather than once per
tick. A policy-epoch change is a persistent condition; firing on the level would write one log
entry per tick forever, and a log that grows without limit is an outage. A refit that failed is
retried by the scheduled interval, which is the mechanism that already means "try again later."

The scheduler's default promotion mode is **shadow**. An automatic refit lands its fresh fit in
the shadow slot, fitted and evaluated and serving nothing, and *stops*. Promoting itself into the
serving slot would be a routing change nobody approved. An operator promotes from shadow to
active with `tokentriage quality activate`, and *that* activation is audited.

## The recalibration log

Every lifecycle event appends one entry to `recalibration.jsonl` in the store (a `fit`,
`activate`, `shadow`, `retire` or `invalidate`), and so does **every one of those that was turned
away**, each classified `applied` or `refused`. A turned-away move is an *entry*, not an absence: a scheduler that
logged its successes and swallowed its refusals would produce a log in which "the epoch moved and
we refitted" and "the epoch moved and the refit failed for eleven days" are the same document,
one of them with fewer lines.

The log is strictly append-only (no entry is rewritten in place) and every entry carries a
monotonic `Seq` seeded from disk, so a restart is invisible and a gap at the head of a read is
a disclosure that older entries rotated away. Each entry also *requires* a detail, since a
refused activation with no text is nothing anybody can act on, so the trail records not only
*what* happened but *why*.

A second kind of artifact lands in the same store under the same discipline. A judge validation
run writes a `jval_` artifact beside the `qcal_` ones, whether the run promoted the subject or
declined it, and a declined run is as durable as a promoted one. What is inside a `jval_` record,
and the ordered walk that produces it, are
[How a judge reaches a measured basis](/docs/judges/agreement).

## Invalidation happens at read time

When a drift finding, the staleness horizon, or a policy-epoch change invalidates a calibration,
the artifact's bytes are **never** rewritten. They can't be: the id is a content hash of the fit,
so editing an entry and re-saving under the same id is exactly the corruption the store refuses.

Instead the invalidation is applied **per segment, at read time**. The affected segment's entry
is flipped to `insufficient`, `drift_invalidated` for a detector finding,
`calibration_stale` for a stale or epoch-mismatched fit — carrying its own `need`, `have`, and
hint, the same emitted-not-omitted shape the other refusals take. The durable record of the
decision is the recalibration log; the flip is computed when the artifact is read. So the system
says "I cannot prove *that* any more" about the exact thing you asked about, without ever
pretending the original measurement did not happen.

## Two calibrations, one chassis

One naming caveat carries real weight. This page is about **`tokentriage quality calibrate`**,
*probability* calibration, stored under `<state_dir>/calibrations/quality/`, producing `qcal_`
artifacts. It is **not** the pre-existing **`tokentriage calibrate`**, which is *routing-threshold*
calibration — it decides what share of traffic goes to the frontier tier, stores its artifacts
under `~/.tokentriage/calibrations`, and is documented in
[Routing](/docs/build/routing#calibrate-from-your-own-logs).

<Callout type="note">
The two share this lifecycle *chassis* — the same candidate → shadow → active → retired states,
the same atomic manifest, the same "a fit writes a candidate and nothing changes until you
activate it" contract — and nothing else. They are different **kinds** of artifact on one
lifecycle, never one command with a mode switch, and they live in two separate stores so a loader
does not have to guess which kind a file holds.
</Callout>

You can watch the whole lifecycle in the **Calibration studio** — its lifecycle board orders
cards candidate → shadow → active → retired, and gated activation shows the reliability diff as
its confirm surface. See [Dashboard](/docs/build/dashboard).

<NextSteps items={[
  { title: 'Calibrate quality from your logs', href: '/docs/calibration/calibrate-from-logs', description: 'Fit a candidate, read its reliability report, and run quality activate.' },
  { title: 'Keep calibrations fresh: drift & refit', href: '/docs/calibration/drift-and-refit', description: 'Configure the detectors and the refit interval, and read the drift timeline.' },
  { title: 'Calibration & evals vocabularies', href: '/docs/reference/quality-vocabularies', description: 'The closed enums: states, triggers, refusal tokens, and insufficiency reasons.' }
]} />
