# Uncertainty

> Probability versus confidence, why confidence is measured rather than invented, and how bands and fallbacks keep decisions honest.

Section: Concepts · HTML: https://jevascript.org/docs/uncertainty · Markdown: https://jevascript.org/docs/uncertainty.md

A decision model does not return "yes". It returns *P(yes)*. jevascript keeps that honesty all the way to your `if`, and gives you three tools for the cases where a forced boolean would be a lie.

## Probability is not confidence

For a truth-backed answer, the probability **is** the uncertainty. `0.92` means the model is 92% sure; there is no separate "how sure am I about the 92".

It is tempting to derive a confidence from the probability, something like `|p − 0.5| × 2`. That is wrong for scores. A fraud risk of `50` can mean *middling risk*, which is a confident answer, or *no idea*, which is not. The number alone cannot tell you which.

So jevascript does not derive confidence. It **measures** it.

## Measured confidence

Ask for `minConfidence` and the runtime re-asks the question, `samples` times (3 by default), and reports how much the answer moved:

```ts
const risk = await semantic(order).score("This order is fraudulent.", {
  asProposition: true,
  minConfidence: 0.8,
})
```

Confidence is `1 − 2 × stdev` of the sampled probabilities, clamped to `[0, 1]`. Identical answers give `1`; the widest meaningful spread on `[0, 1]` gives `0`. The reported value is the mean across rounds.

Two properties matter in practice:

- **Sampling one question does not multiply the cost of its neighbours.** Round one carries every question in the batch; later rounds carry only the questions that asked for more samples.
- **`detailed: true` leaves `confidence` undefined unless it was measured.** You will never see a number that means nothing.

```ts
const r = await semantic(order).score("...", { detailed: true })
r.confidence   // undefined: nothing was measured

const m = await semantic(order).score("...", { detailed: true, samples: 3 })
m.confidence   // 0.94: measured across three rounds
```

### Provider-reported confidence

`choose()` and rubric-backed `score()` (with `levels`) get a real confidence from the provider: the concentration of the distribution over options or levels. They need no sampling, and `minConfidence` on them uses that value directly.

## When confidence is too low

If measured confidence falls below `minConfidence`, one of two things happens:

```ts
// 1. No fallback: LowConfidenceError, carrying confidence, minConfidence and the value.
await semantic(order).score("...", { minConfidence: 0.9 })

// 2. A fallback value…
await semantic(order).score("...", { minConfidence: 0.9, fallback: 50 })

// 3. …or an async fallback: escalate to something slower and better.
await semantic(order).score("...", {
  minConfidence: 0.9,
  fallback: async () => (await reviewer.assess(order)).score,
})
```

The [fraud review](https://jevascript.org/docs/scenarios/fraud-review) scenario uses the async form to route unstable answers to a costlier path only when they are unstable.

## Bands beat thresholds

Providers are not deterministic. `if (p > 0.5)` can land on either side for the same input on different days, which turns into flip-flopping whenever a record is re-evaluated. The first defence is to stop pretending the middle is decided:

```ts
switch (await semantic(tx).is("This transaction is fraudulent.", { allowUnknown: true })) {
  case true:      return block()
  case false:     return proceed()
  case "unknown": return manualReview()
}
```

With `allowUnknown`, probabilities inside `uncertaintyBand` (`[0.3, 0.7]` by default) are returned as the string `"unknown"` and the return type widens to `boolean | "unknown"`. TypeScript then makes you handle the third branch.

Tune the band per call or per instance:

```ts
await semantic(tx).is("...", { allowUnknown: true, uncertaintyBand: [0.4, 0.6] })
createSemantic({ defaults: { uncertaintyBand: [0.25, 0.75] } })
```

## Caching as a consistency tool

The second defence against flip-flopping is to make repeat evaluations return the same answer:

```ts
await semantic(doc).score("...", { cache: "1h" })
```

The same state and the same question hit the cache for an hour, so a webhook replay or a retry cannot drift across a threshold. See [Caching](https://jevascript.org/docs/caching).

## Reading the raw numbers

`detailed: true` returns the underlying signal on every primitive:

| Primitive | Detailed result |
|---|---|
| `is()` | `{ value: boolean, probability: number, confidence?: number }` |
| `score()` | `{ value: number, probability?: number, level?: number, confidence?: number }` |
| `choose()` | `{ value: T, confidence: number, probabilities: Record<T, number> }` |

Use it for logging, for showing a decision trace to a reviewer, or for thresholds that depend on the probability rather than the mapped score.

## Rules of thumb

- Use `allowUnknown` wherever a wrong boolean costs more than a human glance.
- Use `minConfidence` with an async `fallback` on the few decisions where being wrong is expensive, not everywhere. Each sample is a request.
- Cache anything that can be re-evaluated for the same input.
- Never derive confidence from a probability yourself.
