gate()
Continue only when a number clears a bar. Give close calls their own path, or halt.
Basics#
gate asks one question, turns the answer into a single number, and checks it against a bar. Clear it and the input flows on to then. Miss it and the input goes to otherwise, or, if you didn't give one, the run stops.
import { gate, noul, emit } from "jevchain";
const urgent = gate("is-urgent", {
ask: noul("Is the user blocked right now?"),
pass: { min: 0.7 },
then: pageOnCall,
otherwise: emit("file a ticket"),
});| name | type | default | what it does |
|---|---|---|---|
| ask | Question | A choice, score or noul. | |
| pass | { min?, max?, label? } | The bar. Set min, max or both (inclusive). label is required for a choice and not allowed otherwise. | |
| then | JevNode | Runs when the bar is cleared. | |
| otherwise | JevNode | default halt | Runs when it isn't. Omit it to halt the run instead. |
| unsure | { margin?, minConfidence?, then } | A third path for close calls. Checked before pass/fail. | |
| alsoAsk | Questions | Extra questions in the same call, recorded in the trace. | |
| state / model | … | As in ask. |
Like a route, a gate passes its input through unchanged. Its output type is the union of whichever paths you gave it.
Thresholds per question type#
What gets compared to the bar depends on the question:
| name | type | what it does |
|---|---|---|
| noul | metric: noul | p(yes), 0–1. |
| score | metric: score | The probability-weighted level, where 0 is the first level. A 5-level rubric gives 0–4, so bars aren't limited to 0–1. |
| choice | metric: probability | p(pass.label). The label is typed against the question's labels. |
// noul: measures p(yes)
gate("safe", { ask: noul("Is anyone in danger?"), pass: { max: 0.5 }, then: carryOn });
// score: measures the probability-weighted level (0 = first level)
gate("needs-a-meeting", {
ask: score("How much does this need people live?", ["slack", "email", "doc", "call", "meeting"]),
pass: { min: 2.5 },
then: keepIt,
});
// choice: measures p(label). The label is required, and typed.
gate("is-billing", {
ask: choice("What is this?", ["billing", "bug", "vibes"]),
pass: { label: "billing", min: 0.8 },
then: toBilling,
});
// min and max together: a window
gate("goldilocks", { ask: noul("Is the porridge hot?"), pass: { min: 0.4, max: 0.6 }, then: eat });Every gate decision gets a one-sentence summary in the trace, templated from the numbers:
- Passed: p(yes) = 0.83, clearing the 0.60 bar comfortably (by 0.23).
- Blocked: the score came in at 1.40, short of the 2.50 bar easily (by 1.10), so took “otherwise”.
- Passed: p(yes) = 0.04, under the 0.50 ceiling easily (by 0.46).
The unsure band#
A value of 0.61 against a 0.60 bar isn't a pass. It's a shrug with a decimal point. unsuregives close calls their own path, and it's checked before pass/fail. There are two triggers, and either one is enough:
margin: unsure when|value − bar| < margin, where the bar ismin, ormaxif there's no min.minConfidence: unsure when the answer's confidence is below it. For choice and score answers that's Jev'sconfidence. For a noul it's|p − 0.5| × 2, the distance from a coin flip (see confidenceOf).
gate("dress-code", {
ask: noul("Is this outfit appropriate for a fancy rooftop bar?"),
pass: { min: 0.6 },
then: welcome,
otherwise: turnAway,
unsure: { margin: 0.1, then: getManager }, // 0.5 < p(yes) < 0.7
});
gate("vibe-check", {
ask: choice("Tone?", ["friendly", "hostile", "neutral"]),
pass: { label: "friendly", min: 0.5 },
then: reply,
unsure: { minConfidence: 0.3, then: askAHuman }, // flat distributions go to a human
});The trace says so plainly: Too close to call: p(yes) = 0.64, right next to the 0.60 bar, so it took the “unsure” path.
{"title":"Weekly sync","description":"Going around the room with status updates.","attendees":14,"minutes":60}
Halting#
Leave out otherwise and a gate becomes a guard: miss the bar and the run stops right there. Halting isn't an error. Nothing threw; the chain decided to stop. run resolves with status "halted", no output, and a trace.halted saying where and why.
const result = await jev.run(bouncer, "Running shorts and one AirPod.");
result.status; // "halted"
result.output; // undefined
result.trace.halted; // {
// path: "$", nodeId: "dress-code",
// summary: "Blocked: p(yes) = 0.12, short of the 0.60 bar easily (by 0.48), so the run stopped here."
// }In graphs and traces#
graphOfdraws ahaltvertex hanging off the gate, so the stop is visible before anything runs.- The gate's decision has
taken: "halt", and its span, plus any enclosing ones, end with statushalted. Siblings still running in aparallelare closed too. explainTraceends with Halted at dress-code. followed by the gate's summary.
otherwise: the gym fit halts the run. The smart-ish outfit tends to land in the unsure band and get the manager.const bouncer = gate("dress-code", {
title: "Rooftop bar dress code",
ask: noul("Is this outfit appropriate for a fancy rooftop bar?", {
true: "smart, deliberate, dressed for the occasion",
false: "gym clothes, pyjamas, or a costume",
}),
pass: { min: 0.6 },
then: emit("Welcome in. The view is on the left.", { id: "welcome" }),
// Close calls get a human, not a coin flip.
unsure: { margin: 0.1, then: emit("Wait here. The manager is coming.", { id: "get-manager" }) },
// No `otherwise`: a failed gate halts the run. Nothing after it executes.
});Navy linen suit, white shirt, loafers, no socks.