A small, statically checked language where decisions are the branches. You write
ordinary logic — functions, loops, lists, strings — and wherever a program needs
judgment you declare a decision and match on it. The checker guarantees every
answer and "not sure" is handled before the program runs.
verdict check program.vd # every error, with line:col and a caret
verdict run program.vd # check, then run
verdict build program.vd # train the builtin tier for each decision → .verdict/
Hello world
decision greeting: choice "Is the user saying hello or goodbye?" {
hello: "a greeting"
goodbye: "a farewell"
examples { "hi there" => hello "bye" => goodbye … }
tiers builtin
}
fn reply(text: text) -> text {
match greeting(text) {
hello => "Hello, world!"
goodbye => "Goodbye, world!"
uncertain => "Hmm, not sure what you mean."
}
}
for t in ["hello world", "bye world", "banana"] {
print("{t} → {reply(t)}")
}
Decisions
decision NAME: choice "question" { label: "description" … } // 2–255 labels
decision NAME: noul "question" { yes: "…" no: "…" } // yes/no; descriptions optional
decision NAME: score "question" { low: "…" … high: "…" } // 2–10 ordered levels, low → high
Inside the body, in any order:
| Entry | Meaning |
|---|---|
label: "description" |
an answer; the description is what Jet reads |
examples { "text" => label … } |
training data for the builtin tier; labels are checked |
examples from "file.csv" |
text,label CSV (Nodd's format), path relative to the program |
tiers builtin -> nodd "dir" -> jet |
escalation chain; jet "http://…" for a specific server |
threshold 0.85 |
confidence a tier needs to answer instead of escalating (default 0.7) |
Default tiers: builtin -> jet when there are examples, otherwise jet.
Jet's URL defaults to $JET_URL (else http://localhost:8000), key from $JET_API_KEY.
A decision is called like a function and returns an outcome:
let o = moderate(comment)
o.label o.confidence o.certain o.source o.score o.latency_ms
prob(o, "spam")
print(o) // spam (97% via builtin)
match
match moderate(comment) {
ok => "publish"
spam => "quarantine"
toxic @ 0.9 => "hide" // guard: only if p(toxic) ≥ 0.9
toxic => "review" // otherwise the next toxic arm
uncertain => "review" // required
}
Rules (all checked before running):
- every label has an arm;
uncertainis required; - arms for unknown labels are errors;
- several arms per label are allowed only when all but the last are guarded;
the first arm whose guard passes runs; if none passes →
uncertain; - an outcome with
certain = false(no tier was confident) always goes touncertain.
An arm's value is its last expression; match is an expression.
if only takes booleans — if moderate(x) { … } is an error that points you to match.
Comparing a label to a literal (o.label == "spma") is checked against the labels.
Deciding several things at once
let r = decide ticket { topic, human: needs_human, urgency }
match r.human { yes => … no => … uncertain => … }
decide runs all the decisions on one input. At each tier, everything still
unresolved goes together — Jet gets one request for all its questions.
The rest of the language
let x = 1 // declare
x = x + 1 // assign (must be declared)
if x > 1 and not done { … } else if … { … } else { … }
for item in list { … }
while cond { … }
fn name(a: text, b: num) -> text { … } // last expression is the return value; `return` works too
"interpolated {x + 1} text" // \{ for a literal brace
[1, 2, 3] xs[0] xs[-1] r.field
Types: text, num, bool, list, any, and a decision's name for its outcome
(fn queue(t: topic) -> text { match t { … } }).
Built-ins: print, len, lower, upper, trim, contains, split, join, str,
num, round, range, push, read_lines, read_csv, prob.
How it runs
.vd source → lexer → parser → checker → interpreter. Decisions compile to the
same runtime decisions the TypeScript API uses, so escalation, batching, tracing and the
builtin / Nodd / Jet tiers are shared. verdict build, eval, label and questions
accept .vd files as well as TS modules.