# Primitives

> Everything semantic(state).is(), .score() and .choose() accept, what they return, and how to write criteria the model answers well.

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

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

```ts
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

```ts
const blocked = await semantic(ticket).is(
  "The customer cannot take money from their own customers right now.",
)
// boolean
```

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

```ts
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 `true`. |
| `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 `"unknown"` for probabilities inside the uncertainty band instead of forcing a boolean. The return type becomes `boolean \| "unknown"`. |
| `uncertaintyBand` | `[number, number]` | `[0.3, 0.7]` | The band `allowUnknown` uses. |
| `detailed` | `boolean` | `false` | Return `{ value, probability, confidence? }` instead of the boolean. |
| `minConfidence` | `number` | — | Measure confidence by re-asking and reject unstable answers. See [Uncertainty](https://jevascript.org/docs/uncertainty). |
| `fallback` | `boolean \| () => boolean \| Promise<boolean>` | — | Used instead of throwing when confidence is below `minConfidence`. |
| `samples` | `number` | — | Rounds for confidence measurement. Implied by `minConfidence`. |
| `cache` | `string \| number` | — | TTL for this answer, e.g. `"10m"`. See [Caching](https://jevascript.org/docs/caching). |
| `timeoutMs` | `number` | `10_000` | Request timeout. |
| `provider` | `SemanticProvider` | — | Use a different provider for this one question. |

### Three ways to read the answer

```ts
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](https://jevascript.org/docs/uncertainty) for why.

## `score()` — how strongly a proposition holds

```ts
const urgency = await semantic(ticket).score("A human needs to act on this urgently.")
// number, 0–100 by default
```

`score()` 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:

```ts
// 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.

```ts
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`:

```ts
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 `minConfidence`. |

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

```ts
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:

```ts
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`:

```ts
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 `{ value, confidence, probabilities }`. `probabilities` has one entry per option and sums to 1. |
| `minConfidence` | `number` | — | Reject choices whose provider-reported confidence is below this, unless a `fallback` is given. |
| `fallback` | `string \| () => string \| Promise<string>` | — | Used when confidence is below `minConfidence`. |

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](https://jevascript.org/docs/definitions) for how to keep this wording in one versioned place instead of scattered across call sites.
