Running and planning
An expression is a value until you run it. Building one sends nothing; await or .run() sends the requests; .plan() describes them without sending.
import { from } from "jevlish";import { tickets } from "./support.js";import { disruption, reportsProblem } from "./vocabulary.js";
const query = from(tickets) .seenAs((t) => ({ subject: t.subject, body: t.body })) .where((t) => t.status === "open") .and(reportsProblem) .rankedBy(disruption) .take(5);
// Nothing has been sent yet. The query is a value.const plan = query.plan();
plan.subjectCount; // 3plan.decidedByCode; // 1: the closed ticket never reaches the modelplan.questionCount; // one Noul and one Score for each of the other twoplan.requestCount; // 2plan.requests[0]?.state; // exactly what leaves the process for the first subjectplan.notes; // ["filter: ...", 'ranked by "...", highest first', "take 5"]
// Now spend.const result = await query;result.scored;A Plan has:
subjectCount: how many subjects the expression covers (one forgiven, the list length forfrom).decidedByCode: how many of them code predicates settled with no request.questionCountandrequestCount: what would be asked, and how it batches.requests: each request’sstate(exactly what the model would see) andquestions.notes: a description of the filter, ranking, and limit on a query.
Every runnable expression has .plan(): predicates, branches, queries, measurements, choices, and ask. On a branch, plan() does not run any handler.
Running
Section titled “Running”await expression and await expression.run() do the same thing. Each call runs the expression again: a second request, or a cache hit if a cache is configured and nothing changed. Expressions are not memoized.
import { given } from "jevlish";import type { Ticket } from "./support.js";import { blocked } from "./vocabulary.js";
// Build expressions in plain functions so callers can still .plan() them.export const isBlocked = (t: Ticket) => given(t).seenAs((x) => ({ body: x.body })).when(blocked);
isBlocked({ id: "T-1", status: "open", subject: "s", body: "b" }).plan(); // fine
// An async function awaits the expression for you: the caller gets a// Promise<Judgment<boolean>> and can no longer plan it.export const isBlockedNow = async (t: Ticket) => given(t).when(blocked);
// .run() and await do the same thing; each call runs the expression again.const expression = isBlocked({ id: "T-1", status: "open", subject: "s", body: "b" });const first = await expression.run();const second = await expression; // a second request (or a cache hit)first.status === second.status;Keep expressions in plain functions
Section titled “Keep expressions in plain functions”An async function awaits its return value. A function that returns given(t).when(blocked) hands the caller an expression they can .plan(), .run(), or .withPolicy() further. An async function that returns the same thing hands them a Promise<Judgment<boolean>> that is already in flight. If callers need to inspect before spending, build expressions in plain functions and let the caller decide when to await.
Most application code should let TypeScript infer expression types. Libraries that need to name a builder type can import the advanced classes from jevlish/types; they are kept out of the root entry point so the common API stays focused on verbs, question builders, and results.
Errors
Section titled “Errors”A SenseError means the expression itself is malformed or incomplete: an empty proposition, .and() before .where(), a branch awaited without .whenUncertain(), a scale with one level. Transport failures from the underlying SDK propagate as they are. A thrown error is a failed request, never evidence about the proposition; it does not become uncertain.