jevascript
esc

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

    Get started

    Concepts

    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.

    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()#

    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().

    FieldTypeDefaultDescription
    name*stringIdentity, surfaced in observability events as metric.name.
    versionstringBump whenever the wording changes.
    condition*stringThe proposition, written in full.
    trueWhenstring | JsonValueWhat clearly counts.
    falseWhenstring | JsonValueWhat clearly does not, even when it looks close.
    defaultsIsOptionsOptions applied to every call: threshold, allowUnknown, cache, minConfidence, and so on.

    defineMetric() — a reusable score()#

    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.

    FieldTypeDefaultDescription
    name*stringIdentity for observability.
    versionstringBump whenever the wording changes.
    description*stringThe proposition, in full. Nothing is inferred or reframed.
    trueWhenstring | JsonValueWhat clearly counts.
    falseWhenstring | JsonValueThe near-misses.
    outputNumberOutputnumber({ min: 0, max: 100 })Range, and optionally rubric levels.
    defaultsScoreOptionsOptions 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:

    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.

    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#

    FieldTypeDefaultDescription
    name*stringIdentity for observability.
    versionstringBump whenever the wording changes.
    instructions*stringThe frame every field is judged in.
    contextRecord<string, unknown>Background merged into the state on every call.
    output*AnyOutputThe 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#

    HelperField typeNotes
    boolean({ describe, trueWhen, falseWhen })booleanA truth question, thresholded by defaults.threshold.
    number({ min, max, describe, levels })numberProbability mapped onto [min, max], or a rubric if levels is given.
    enumOf(options, describe)literal unionArray or described object, like choose().
    object({ ...fields })objectNests. Nested objects are flattened into one request.
    numberRange(min, max, describe)numberSame 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:

    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.
    • Cache keys. A changed question is a different key, so old answers are not reused. See Caching.
    • Your records. Store triage.name and triage.version next to the result, as the 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:

    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().