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. Downstream nodes read them as {{answers.<id>.<key>}}. See route. | |
| 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 the value is less thanmarginfrom the bar's nearest edge. For a singleminormaxthat's just|value − bar| < margin. For a window it's whichever ofminandmaxis closer, on either side of it. Exactlymarginaway is outside.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, 0.04 over the 0.60 bar, inside the 0.10 margin, so it took the "unsure" path.
A window has two edges, so it has two sets of close calls. Give the goldilocks gate above a 0.1 margin and everything strictly between 0.3 and 0.7 is unsure, bar one value: Too close to call: p(yes) = 0.55, 0.05 under the 0.60 ceiling of the 0.40–0.60 window, inside the 0.10 margin, so it took the "unsure" path. Dead centre, 0.5 is exactly 0.10 from both edges, which is outside the margin, so it passes. A window narrower than two margins is mostly shrug.
{"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, 0.48 short of the 0.60 bar and clear of its 0.10 unsure margin easily (by 0.38), 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.