# Definitions

> Declare a rule, a metric or a whole schema once, with a name and a version, and apply it to data. Every field resolves in one request.

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

The fluent form, `semantic(x).score("...")`, is a ramp. Definitions are where it leads: the wording lives in one place, carries a version, shows up in observability, and can be tested on its own.

## `defineRule()` — a reusable `is()`

```ts
// rules.ts
import { defineRule } from "jevascript"

export const safeToAutoReply = defineRule({
  name: "safeToAutoReply",
  version: "2",
  condition: "A templated answer would be adequate and would not annoy this customer.",
  falseWhen: "The customer is angry, is blocked, or is asking something specific to their account.",
})

if (await safeToAutoReply(ticket)) sendTemplate()
```

A rule is a function `(state, options?) => Promise<boolean>` with `name`, `version` and `definition` attached. It accepts the same options as `is()`.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `name` (required) | `string` | — | Identity, surfaced in observability events as `metric.name`. |
| `version` | `string` | — | Bump whenever the wording changes. |
| `condition` (required) | `string` | — | The proposition, written in full. |
| `trueWhen` | `string \| JsonValue` | — | What clearly counts. |
| `falseWhen` | `string \| JsonValue` | — | What clearly does not, even when it looks close. |
| `defaults` | `IsOptions` | — | Options applied to every call: `threshold`, `allowUnknown`, `cache`, `minConfidence`, and so on. |

## `defineMetric()` — a reusable `score()`

```ts
// metrics.ts
import { defineMetric, number } from "jevascript"

export const churnRisk = defineMetric({
  name: "churnRisk",
  version: "2",
  description: "The customer is at risk of leaving for a competitor in the near term.",
  trueWhen: "Cancellation language, repeated unresolved problems, or naming a competitor.",
  falseWhen: "Merely angry. Anger on its own is not churn risk.",
  output: number({ min: 0, max: 100 }),
})

if (await churnRisk(customer) > 80) startRetentionFlow()
```

`description` is sent exactly as written; a metric never reframes. The range comes from `output` and defaults to `0` to `100`. Give `output` a `levels` array for a rubric-backed metric.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `name` (required) | `string` | — | Identity for observability. |
| `version` | `string` | — | Bump whenever the wording changes. |
| `description` (required) | `string` | — | The proposition, in full. Nothing is inferred or reframed. |
| `trueWhen` | `string \| JsonValue` | — | What clearly counts. |
| `falseWhen` | `string \| JsonValue` | — | The near-misses. |
| `output` | `NumberOutput` | `number({ min: 0, max: 100 })` | Range, and optionally rubric levels. |
| `defaults` | `ScoreOptions` | — | Options applied to every call, except `range` and `levels`, which come from `output`. |

### Joining a batch

Both rules and metrics expose `.question()`, so a definition still shares a request with whatever else you ask:

```ts
const t = await semantic(ticket).batch({
  churn: churnRisk.question(),
  safe: safeToAutoReply.question(),
  urgency: score("A human needs to act on this urgently."),
})
```

## `defineSchema()` — many fields, one request

A schema declares the frame, the background and the output shape in one place, then applies them to data. Every field resolves in a single request, however many there are.

```ts
// triage.ts
import { boolean, defineSchema, enumOf, number, object } from "jevascript"

export const triage = defineSchema({
  name: "ticket-triage",
  version: "4",

  instructions: `
    Triage an inbound support ticket for Northwind, a B2B payments API sold to
    engineering teams. "Blocked" means the customer cannot process live
    transactions right now. Sandbox and documentation problems are never blocking.
  `,

  context: {
    escalationPolicy:
      "Page on-call only for live payment failures affecting a paying customer in production.",
  },

  output: object({
    urgency: number({ describe: "A human needs to act on this ticket urgently." }),
    frustration: number({ describe: "The person writing this sounds frustrated or angry." }),
    blocksRevenue: boolean({
      describe: "The customer is currently unable to take money from their own customers.",
      falseWhen: "Sandbox failures, slow dashboards, or questions about future work.",
    }),
    churnSignal: boolean({
      describe: "The customer hints they may leave, or is evaluating competitors.",
      falseWhen: "Frustration alone. Anger is not the same as leaving.",
    }),
    department: enumOf(
      {
        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",
      },
      "Which team should own this ticket",
    ),
  }),
})

const t = await triage(ticket)
//    ^? { urgency: number; frustration: number; blocksRevenue: boolean;
//         churnSignal: boolean; department: "integration" | "payments" | "billing" | "security" }

export type Triage = Awaited<ReturnType<typeof triage>>
```

### `instructions` is not a system prompt

There is no behaviour to steer. The model does not take orders; it scores propositions. What `instructions` does is **frame** every question: it says what the data is and what your words mean in your business. Write it as a description, never as a command.

### `context` is background, not input

`context` is merged into the state on every call, in its own labelled field, so the input can never quietly overwrite the policy it is being judged against. Put the plan table, the escalation policy, the ideal customer profile here. The revenue or trust-and-safety team can edit it without touching code.

### Every `describe` is a proposition

Because that is what gets scored. "How urgent is this?" is a label; "A human needs to act urgently." is a claim. Leave `describe` off a field and the runtime builds the least-bad proposition from the field name, which is rarely what you want.

### Fields

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `name` (required) | `string` | — | Identity for observability. |
| `version` | `string` | — | Bump whenever the wording changes. |
| `instructions` (required) | `string` | — | The frame every field is judged in. |
| `context` | `Record<string, unknown>` | — | Background merged into the state on every call. |
| `output` (required) | `AnyOutput` | — | The shape, built from the output helpers below. |
| `defaults` | `{ timeoutMs?, cache?, samples? }` | — | Applied to every call. |

The evaluator accepts `(data, { provider?, timeoutMs?, cache?, samples? })` and returns the typed object.

## Output helpers

| Helper | Field type | Notes |
|---|---|---|
| `boolean({ describe, trueWhen, falseWhen })` | `boolean` | A truth question, thresholded by `defaults.threshold`. |
| `number({ min, max, describe, levels })` | `number` | Probability mapped onto `[min, max]`, or a rubric if `levels` is given. |
| `enumOf(options, describe)` | literal union | Array or described object, like `choose()`. |
| `object({ ...fields })` | object | Nests. Nested objects are flattened into one request. |
| `numberRange(min, max, describe)` | `number` | Same as `number()` with positional arguments. |

`score(min, max, describe)` is also accepted as an output helper: the top-level `score` export is two functions disambiguated by its first argument. `score("urgency")` is a question for `batch()`; `score(0, 100, "...")` is a number output for a schema. The examples in the library repository use it this way.

## `evaluate()` — the low-level form

`defineSchema` is a thin layer over `evaluate()`, which takes the same pieces as one call:

```ts
import { evaluate, object, boolean, number } from "jevascript"

const result = await evaluate({
  data: ticket,
  question: "Triage an inbound support ticket for Northwind, a B2B payments API.",
  context: { escalationPolicy: "..." },
  output: object({
    urgency: number({ describe: "A human needs to act on this ticket urgently." }),
    blocked: boolean({ describe: "The customer cannot take money right now." }),
  }),
  cache: "10m",
})
```

The task and context travel once, in the state, rather than repeated in every question. On a six-field schema that alone cut tokens by a third.

## Versioning

Bump `version` whenever the wording changes. There is no fine-tuning: the wording, the criteria and the pinned model version together are a single artefact, and evaluations are only comparable within one. The version reaches three places:

- **Observability.** Every event carries `metric: { name, version }`. See [Observability](https://jevascript.org/docs/observability).
- **Cache keys.** A changed question is a different key, so old answers are not reused. See [Caching](https://jevascript.org/docs/caching).
- **Your records.** Store `triage.name` and `triage.version` next to the result, as the [ticket triage](https://jevascript.org/docs/scenarios/ticket-triage) scenario does.

## Instance-bound definitions

A `createSemantic()` instance carries its own `defineRule`, `defineMetric`, `defineSchema` and `evaluate`, bound to that instance's provider and cache:

```ts
export const triage = semantic.defineSchema({ /* ... */ })
export const churnRisk = semantic.defineMetric({ /* ... */ })
```

The top-level `defineRule` and friends bind to the module-level instance; `defineRuleWith(runtime, ...)`, `defineMetricWith`, `defineSchemaWith` and `evaluateWith` take an explicit runtime from `createRuntime()`.
