jevascript
esc

    Type to search · ↑↓ to move · ↵ to open

    Get started

    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: true leaves confidence undefined 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 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:

    // 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:

    PrimitiveDetailed 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.