Concepts
Primitives
Everything semantic(state).is(), .score() and .choose() accept, what they return, and how to write criteria the model answers well.
The state#
semantic(state) wraps a value so questions can be asked about it. It accepts a string, an array of strings, or any object:
semantic("Every charge returns gateway_timeout.")
semantic(["first message", "reply", "second reply"])
semantic({ plan: "enterprise", subject: "...", body: "..." })Objects are serialised as JSON before they are sent, so field names carry meaning. A field called customerMessage is judged differently from one called internalNote. Keep irrelevant fields out; they measurably degrade accuracy.
The returned context is cheap and stateless. Create one per value; do not cache or share it.
is() — a proposition is true#
const blocked = await semantic(ticket).is(
"The customer cannot take money from their own customers right now.",
)
// booleanThe provider returns P(the proposition is true). is() compares it to a threshold, 0.5 by default, and returns a boolean.
Sharpen the edges#
trueWhen and falseWhen state what clearly counts and what clearly does not. Near-misses are where accuracy is won or lost, and this is where you name them:
await semantic(ticket).is("The customer hints they may leave.", {
trueWhen: "Cancellation language, or naming a competitor they are evaluating.",
falseWhen: "Frustration alone. Anger is not the same as leaving.",
})Options#
| Option | Type | Default | Description |
|---|---|---|---|
| threshold | number | 0.5 | Probability above which the answer reads as |
| trueWhen | string | JsonValue | — | What clearly counts as true. |
| falseWhen | string | JsonValue | — | What clearly does not count, even when it looks close. |
| allowUnknown | boolean | false | Return |
| uncertaintyBand | [number, number] | [0.3, 0.7] | The band |
| detailed | boolean | false | Return |
| minConfidence | number | — | Measure confidence by re-asking and reject unstable answers. See Uncertainty. |
| fallback | boolean | () => boolean | Promise<boolean> | — | Used instead of throwing when confidence is below |
| samples | number | — | Rounds for confidence measurement. Implied by |
| cache | string | number | — | TTL for this answer, e.g. |
| timeoutMs | number | 10_000 | Request timeout. |
| provider | SemanticProvider | — | Use a different provider for this one question. |
Three ways to read the answer#
await semantic(x).is(p) // true | false
await semantic(x).is(p, { allowUnknown: true }) // true | false | "unknown"
await semantic(x).is(p, { detailed: true }) // { value: boolean, probability: number, confidence?: number }confidence is only present when it was measured (samples or minConfidence). It is never derived from the probability. See Uncertainty for why.
score() — how strongly a proposition holds#
const urgency = await semantic(ticket).score("A human needs to act on this urgently.")
// number, 0–100 by defaultscore() is backed by a probability, not a rubric. It asks P(this is true) and maps the answer onto your range, so score(...) of 91 means the model is 91% sure the claim holds.
Framing#
A bare noun phrase is not a claim. score("urgency") is therefore reframed as "This has high urgency." before it is sent, because a decision model answers what it is literally asked. Two ways to control that:
// 1. Write the proposition yourself and turn framing off.
await semantic(ticket).score("A human needs to act on this urgently.", { asProposition: true })
// 2. Replace the frame globally, for instance for another language.
configureSemantic({ defaults: { scoreFrame: (c) => `Bu durumda yüksek ${c} var.` } })A full sentence with asProposition: true is the explicit path and the one the docs use throughout. Definitions (defineMetric) always take it.
Rubric levels#
For a genuine magnitude rather than a probability, describe the rubric. Levels describe situations, not degrees: the model evaluates each level independently, so "worse than the previous level" tells it nothing.
const impact = await semantic(bug).score("impact on the customer", {
levels: [
"No impact; the customer can work normally.",
"Annoying, but a workaround exists.",
"A feature is unusable; the rest of the product works.",
"The whole product is unusable for the customer.",
],
})A level can also be an object with summary and signals:
levels: [
{ summary: "No impact.", signals: ["question", "feature request"] },
{ summary: "Blocked.", signals: ["cannot log in", "payments failing"] },
]Levels are 0-indexed. The provider returns a probability-weighted position from 0 to levels.length - 1, which may fall between levels; score() maps it onto your range. Between 2 and 10 levels are supported by the Jev provider; more or fewer throws UnsupportedByProviderError before any network call.
Options#
Everything is() accepts except threshold, allowUnknown and uncertaintyBand, plus:
| Option | Type | Default | Description |
|---|---|---|---|
| range | [number, number] | [0, 100] | The output range. The probability or rubric position is mapped linearly onto it. |
| levels | (string | LevelSpec)[] | — | 2 to 10 ordered rubric levels. Switches from a probability-backed score to a rubric-backed one. |
| asProposition | boolean | false | Send the criterion exactly as written instead of framing it. |
| fallback | number | () => number | Promise<number> | — | Used when confidence is below |
With detailed: true the result is { value, probability?, level?, confidence? }: probability for probability-backed scores, level and a provider-reported confidence for rubric-backed ones.
choose() — one of a fixed set#
const team = await semantic(ticket).choose(["billing", "integration", "payments", "security"])
// ^? "billing" | "integration" | "payments" | "security"The literal union is inferred at the call site; no as const is needed.
Describe the options#
Pass an object instead of an array and the values become descriptions. This is the single most effective accuracy improvement available on a choice:
const team = await semantic(ticket).choose({
integration: "SDK usage, API errors, webhooks, authentication during integration",
payments: "Declines, settlement, payouts, chargebacks, currency",
billing: "Our own invoices, pricing, plan changes and refunds",
security: "Credential exposure, suspicious access, vulnerability reports",
})Each value can also be a ChoiceOptionSpec with what, not_for and examples:
billing: {
what: "Our own invoices, pricing, plan changes",
not_for: "Refunds the customer owes their own customers",
examples: ["Why was I charged twice?", "Can I move to annual billing?"],
}Options#
| Option | Type | Default | Description |
|---|---|---|---|
| instructions | string | "Select the option that best fits the state." | The frame the choice is made in. |
| detailed | boolean | false | Return |
| minConfidence | number | — | Reject choices whose provider-reported confidence is below this, unless a |
| fallback | string | () => string | Promise<string> | — | Used when confidence is below |
Plus cache, timeoutMs, provider and samples as on is().
choose() gets a real confidence from the provider, the concentration of the distribution, so it needs no sampling. A choice over more options than the provider supports (255 for Jev) throws UnsupportedByProviderError before the network. Fewer than two options is also rejected.
Writing criteria that work#
Everything above is mechanics. Most of the accuracy comes from the words.
Write propositions, not labels. A decision model scores a claim. "How urgent is this?" is a label; "A human needs to act on this urgently." is a claim. Measured on a ticket that plainly is not urgent, the label form returns 0.54, a shrug, where the proposition returns 0.06.
Ask about the text, not the future. "This report describes a security problem" is answerable from the text. "This will become a security incident" is not.
One step per question. A question that needs two inferences ("the customer is angry because of a billing error") scores worse than two questions that need one each. Ask both and combine them in code.
Facts you already have stay in code. Plan, amount, date, distance, role: if it is a property, compare it with ===. Do not ask the model to remember a price table or compare dates; it reads them as text.
Name the near-misses. falseWhen is where you say what looks like a match but is not. It is the cheapest accuracy you will buy.
Keep the state relevant. Every field in the state is context. Fields that have nothing to do with the question dilute the ones that do.
See Definitions for how to keep this wording in one versioned place instead of scattered across call sites.