Concepts
Uncertainty
Probability versus confidence, why confidence is measured rather than invented, and how bands and fallbacks keep decisions honest.
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:
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: trueleavesconfidenceundefined unless it was measured. You will never see a number that means nothing.
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 roundsProvider-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:
// 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 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:
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:
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:
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.
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
allowUnknownwherever a wrong boolean costs more than a human glance. - Use
minConfidencewith an asyncfallbackon 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.