Skip to content

Judging one thing

given(subject) starts an expression about one value. What follows says what to ask about it and what to do with the answer.

given(x) holds the whole object. .seenAs(fn) projects it into the state the model sees. Code predicates, handlers, and the returned Decision still work with the whole object, so you can send { subject, body } and still branch on ticket.status or read ticket.id in a handler.

Without .seenAs, the subject is serialized as-is: strings stay strings, objects become JSON, dates become ISO strings, functions and undefined are dropped.

.when(condition) accepts three kinds of thing, and so do .and(), .or(), and .unless():

  • A string is a proposition the model judges. It becomes a Noul question.
  • A function (subject) => boolean is a code predicate. It runs locally, before anything is sent.
  • A meaning from means(...) is a reusable, named proposition with its boundary drawn. See Meanings.

.and, .or, and .unless build an expression tree. They do not rewrite prose. a.unless(b) is a and not b. The tree is evaluated with three-valued logic: false and uncertain is false, true or uncertain is true, and not uncertain is uncertain. A false code predicate under and therefore settles the whole expression, and no request is sent.

A predicate on its own resolves to a Judgment<boolean>: either { status: "decided", value, evidence } or { status: "uncertain", evidence }.

import { given } from "jevlish";
import { ticket } from "./support.js";
import { blocked, saysResolved } from "./vocabulary.js";
// A predicate without a branch resolves to a Judgment<boolean>.
const judgment = await given(ticket)
.when(blocked)
.unless(saysResolved)
.and((t) => t.status === "open");
if (judgment.status === "decided") {
judgment.value; // true or false
} else {
judgment.evidence; // the requests and answers that failed to settle it
}

.do(handler) runs when the condition is true. .otherwise(handler) runs when it is false. .whenUncertain(handler) runs when the policy would not decide. Each handler receives the whole subject and the judgment that selected it, and may be async.

import { given } from "jevlish";
import { ticket } from "./support.js";
import { needsAttention } from "./vocabulary.js";
const decision = await given(ticket)
.seenAs((t) => ({ subject: t.subject, body: t.body }))
.when(needsAttention)
.do((t) => `escalated ${t.id}`)
.otherwise(() => "left alone")
.whenUncertain((t, judgment) => `review ${t.id}: ${judgment.evidence.judgments.length} judgments`);
decision.branch; // "do" | "otherwise" | "uncertain"
decision.result; // string
decision.judgment; // Judgment<boolean>, with the evidence behind the branch

Awaiting the branch returns a Decision:

  • branch is "do", "otherwise", or "uncertain".
  • result is whatever the handler returned. Its type is the union of the handlers’ return types. Without .otherwise(), undefined joins the union.
  • judgment is the Judgment<boolean> that chose the branch, with its evidence.

A branch without .whenUncertain() cannot run. Awaiting one rejects with a SenseError before judging the predicate, so the mistake sends no request. Its fulfilled type is never, which also exposes the omission in the editor. An abstaining model is not a “no”; the code has to say what happens in that case.

import { given } from "jevlish";
import { ticket } from "./support.js";
import { blocked } from "./vocabulary.js";
const branch = given(ticket).when(blocked).do(() => "escalate").otherwise(() => "leave");
// Awaiting a branch without .whenUncertain() rejects with a SenseError.
// The fulfilled value is typed `never`, so the mistake also shows at compile time.
try {
await branch;
} catch (error) {
error; // SenseError: .whenUncertain() is required before a branch can run.
}

given(x) also starts .measure(scale), .chooseFrom(candidates), and .ask({ ... }). They are covered in Scales, Choosing, and Asking several questions. .withPolicy({...}) overrides acceptance thresholds on given or on any runnable expression it builds; see Uncertainty.