fuzzy-jev
Ask Jev typed questions about a piece of text and get calibrated probabilities back, rather than prose. A Choice between named options, a Score on an ordered scale, or a Noul — the probability that something is true.
Jev is TypeSafe's model. This client reaches it through
OpenRouter, which serves it on the decisions endpoint
(https://openrouter.ai/api/alpha/decisions), so the key you need is an OpenRouter one;
--url points the same questions at another endpoint.
Every answer is already a degree from 0 to 1, so the answers can be used directly as fuzzy truth
values. A rules file combines them with AND, OR, NOT and hedges into
decisions, or into a crisp amount through fuzzy outputs.
--svg draws the whole rule base. docs/rules.md is the
full guide to rules.toml: every table, operator and formula, the output formats, the errors, and
an example worked by hand.
A command for your terminal, a Rust library that also compiles for wasm32-unknown-unknown, and
an Agent Skill that teaches a coding agent when to ask Jev instead of judging
by eye — jev add skill writes it into your project. The crate and the command are both called
jev.
is_urgent 0.95 yes
department billing confidence 0.84 (billing 0.89, technical 0.11, sales 0.00)
frustration 1.04 of 2, nearest "Frustrated" confidence 0.94
427 tokens in, 73 out, $0.000018, typesafe/jev-1.13-20260917
Every question sees the same state and is answered on its own, so ask all of them in one call: an extra question costs a few tokens and no extra round trip.
Install
From crates.io. The package is fuzzy-jev, since jev on crates.io is another project; the
command it installs is jev. Needs a Rust toolchain.
It lands in ~/.cargo/bin, which rustup puts on your PATH, so jev works in any folder.
cargo install fuzzy-jev --locked --force updates it; cargo uninstall fuzzy-jev takes it off
your PATH again.
A built binary. Each release carries a
.tar.gz per platform — Linux and macOS, x86-64 and Arm — with a .sha256 beside it:
v0.1.0, from before this repository was renamed, has no fuzzy rules or drawing.
With cargo, from this repository. For what is on main before it is released, with no
clone: cargo fetches the source and builds it.
A few variants:
# a particular release
# a branch, to try something before it is merged
# over an older copy, when cargo says one is already installed
--locked builds with the dependency versions in Cargo.lock, which is what CI tested; leave it
out to let cargo pick newer ones. To run it from a clone instead:
Then set OPENROUTER_API_KEY from an OpenRouter key. --dry-run
prints the request instead of sending it, and needs no key.
Asking
A question on the command line is ID=INSTRUCTIONS, then |-separated criteria:
| Flag | Criteria |
|---|---|
--noul |
none, or WHAT YES MEANS|WHAT NO MEANS |
--choice |
two or more options, each NAME or NAME:DESCRIPTION |
--score |
two to ten levels, lowest first |
Each flag can be repeated, and the answers print in the order the questions were written. Text
with a | in it, or structured criteria, goes in a JSON file of ids to questions passed with
--questions (-q); file questions come first, then the flags'.
| Option | |
|---|---|
STATE |
The state, as text. Without it, it's read from --state-file (-f, - for standard input), or from standard input when that isn't a terminal. |
--state-json |
Parse the state as JSON: cat ticket.json | jev --state-json -q questions.json |
--model (-m) |
Another model; the default is TypeSafe's typesafe/jev-1.13. |
--url |
Another endpoint. With one, OPENROUTER_API_KEY may be unset, for an endpoint that adds the key. |
--text |
One line per question. The default. |
--table |
A table: question, type, answer, confidence, and every option's probability. |
--json |
The reply as the endpoint sent it, for jq and scripts. |
--rules (-r) |
Fuzzy rules over the answers, from a TOML file; prints their outcome instead of the answers. |
--graph |
With -r: draw the rules in the terminal. |
--svg PATH |
With -r: draw the rules as an SVG image. Without a state, either one draws the structure alone, with no call. |
--dry-run |
Print the request instead of sending it. No key needed. |
A structured state
--state-json sends the state as JSON rather than as text, so nested fields, numbers and lists
reach the model as what they are. A support ticket, ticket.json:
The three question types, in one questions.json. A noul's criteria are keyed true and false,
which is what the endpoint calls them — the --noul flag spells the same thing
id=INSTRUCTIONS|WHAT YES MEANS|WHAT NO MEANS:
|
team billing confidence 0.99 (billing 0.99, success 0.01, support 0.00)
urgency 2.48 of 3, nearest "Today" confidence 0.52
churn_risk 0.86 yes
571 tokens in, 72 out, $0.000024, typesafe/jev-1.13-20260917
The numbers move a little from call to call, so a threshold is worth setting with a margin rather than at the value one run happened to give.
With --json, each answer is named after its type — .noul, .choice (with .confidence and
.probabilities), .score — which is what a script gates on:
|
Fuzzy rules
The complete reference is
docs/rules.md. This section is the short tour.
A decision is often several answers combined: "a raincoat when it rains, unless it's hot". A rules file says that directly. It names the answers it needs as terms, combines them with fuzzy logic, and gives each outcome a score. The rules are your knowledge; Jev only supplies how much each condition holds.
The questions, examples/rules/weather.json:
The rules, examples/rules/wear.toml:
[] # optional
= "min" # min | product | lukasiewicz
= "max" # max | probsum | bounded
[] # optional
= 0.5
[]
= "temp.Cold" # a Score level, by its exact text
= "temp.Mild"
= "temp.Hot"
= "humidity.Humid"
= "raining" # a Noul, by its id
# stormy = "sky.storm" (a Choice option, from a "sky" choice: clear, cloudy, storm)
[[]]
= "cold"
= "coat"
[[]]
= "mild AND NOT raining"
= "light jacket"
[[]]
= "raining AND NOT hot"
= "raincoat"
[[]]
= "raining"
= "umbrella"
[[]]
= "hot AND humid"
= "t-shirt"
[[]]
= "humid OR hot"
= "breathable fabric"
= 1.0 # optional, 0 to 1; the rule's score is multiplied by it
coat 0.03
light jacket 0.05
raincoat 0.95 yes
umbrella 0.95 yes
t-shirt 0.02
breathable fabric 1.00 yes
threshold 0.50
369 tokens in, 47 out, $0.000015, typesafe/jev-1.13-20260917
- Terms are lowercase names, and every word in a rule is a term or an operator. A term points
at a Noul by its id, at a Score level by the question id, a
., and the level's exact text, or at a Choice option by its name. - Operators are uppercase:
AND,OR,NOT, parentheses, and the hedgesVERY(x²),SOMEWHAT(√x),EXTREMELY(x³) andINDEED(pushed toward 0 or 1). Hedges andNOTbind tightest, thenAND, thenOR. [logic]picks the AND and the OR for the whole file.minandmax, the defaults, are safe when answers are related, as answers about one state usually are.productandprobsumtreat conditions as independent, so doubts compound and reasons reinforce.- Rules with the same
thenare joined by the file's OR, and an item is a yes at or over the threshold. - Checked before the call. Every term is resolved against the questions first, so a typo costs
nothing, and
--dry-runcatches it too. The error names what does exist:[terms] hot: `temp` has no level `hot`; its levels are `temp.Cold`, `temp.Mild`, `temp.Hot`. An answer or a probability missing from the reply is an error, never a silent zero. --tableadds the rules behind each score, and--jsonprints{"reply": …, "outcome": …}, so a script keeps every answer.
Outputs: a crisp amount
When the answer is an amount ("how much to water?") rather than a yes, a rule can conclude in an output: a crisp axis with named fuzzy sets. Each rule clips its set at its score, the clipped shapes are merged with the file's OR, and the value is the centre of the merged shape. This is Mamdani inference with centroid defuzzification.
examples/rules/irrigation.toml, with
examples/rules/rain.json asking how much it rained:
[]
= "rainfall.Scarce"
= "rainfall.Regular"
= "rainfall.Large"
[]
= [0, 100]
= [0, 0, 20, 40] # a trapezoid: rises a→b, flat b→c, falls c→d
= [30, 50, 70] # a triangle: a, peak, c
= [60, 80, 100, 100] # a shoulder, held up to the end of the range
[[]]
= "scarce"
= "irrigation IS gallon"
[[]]
= "regular"
= "irrigation IS liter"
[[]]
= "large"
= "irrigation IS drops"
irrigation 51.02 (drops 0.00, liter 0.98, gallon 0.02)
then = "OUTPUT IS SET" concludes in an output when the file declares that output; any other
then is an item, and one file can have both. When no rule for an output scores above zero, its
value is - (null in JSON) rather than a made-up number.
For everything together (a Choice, hedges, parentheses, a weight, probsum, a threshold, and items
alongside an output), see the support-triage example,
examples/rules/triage.toml, which
docs/rules.md works through by hand.
Drawing the rules
--svg PATH draws the rules as an image, and --graph draws them in the terminal. Without a
state (no argument, no -f, nothing on standard input) or with --dry-run, either one draws the
structure alone and makes no call, which is a free way to check a rules file. With a state, every
part carries its number.
That is the drawing at the top of this page. The image is laid out the way a fuzzy rule base is usually drawn, with one row per rule:
- Premises: a column per question. A Score's levels are drawn as a fuzzy partition, with the term's own level in bold, shaded up to the degree Jev gave it. The red arrow is the expected level, the reading the curves turn into degrees. Choices and Nouls are drawn as bars.
- Rules: the
ifas a tree of its operators, each with what it came to. - Conclusions: an output's sets, with the rule's own set clipped at its score, or an item's bar against the threshold.
- Final: each output's merged shape with an arrow at its centroid, and every item's score.
With an output, the conclusions are its sets clipped at each rule's score, and the final column is their merged shape with an arrow at its centroid. The irrigation rules:
In the terminal, --graph prints each rule as a tree, and each term with every level's
probability (the term's own in brackets):
R3 raining AND NOT hot ⇒ raincoat
AND (min) 0.95
├─ raining 0.95 ← raining: no 0.05 · [yes 0.95]
└─ NOT (1 − x) 0.98
└─ hot 0.02 ← temp: Cold 0.04 · Mild 0.94 · [Hot 0.02]
⇒ raincoat 0.95 ███████████████████
An output is drawn as a plot of its merged shape, with ↑ at its centre:
irrigation = 51.02
1.0 ┤░░░░░░░░░░░░░ ▃▆▆▃ ░░░░░░░░░░░░░
│░░░░░░░░░░░░░░░░ ▁▄██████▄▁ ░░░░░░░░░░░░░░░░
│░░░░░░░░░░░░░░░░░░ ▂▅██████████▅▂ ░░░░░░░░░░░░░░░░░░
│░░░░░░░░░░░░░░░░░░░░░▃▇██████████████▇▃░░░░░░░░░░░░░░░░░░░░░
│░░░░░░░░░░░░░░░░░░▁▄████████████████████▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
0.0 └──────────────────────────────┬─────────────────────────────
0 ↑ 51.02 100
drops liter gallon
The Agent Skill
A coding agent asked to "sort these tickets" or "which of these need a human?" will usually read
them itself and write a paragraph of opinion. The skill teaches it to reach for jev instead: one
call, a probability per item, and a number it can threshold on.
created .agents/skills/jev/SKILL.md
created .agents/skills/jev/references/patterns.md
The jev skill is in .agents/skills/jev. Agents that read .agents will find it.
jev add skill |
writes it to .agents/skills/jev, the convention most agents read |
jev add skill --claude |
writes it to .claude/skills/jev instead |
jev add skill --tool |
writes the skill for an agent whose jev is a tool it calls with JSON, rather than this command |
jev add skill --force |
replaces files that are there and differ |
The two files are compiled into the binary, so an installed jev carries its own skill and needs
no source tree to hand it over. Nothing is overwritten without --force: a file that is already
what would be written is left alone, so running it twice says unchanged rather than churning
your diff.
What it teaches is when the tool fits — classifying, routing, triaging, rating, flagging, and
anything where a confidence number beats an opinion — how to write the three question types, the
patterns for putting many items through one call, and how to design fuzzy rules over the answers. Read
skills/jev/SKILL.md and
skills/jev/references/patterns.md before installing it, as
you would any instruction you're adding to a project.
As a library
[]
= { = "fuzzy-jev", = "0.2", = false }
let client = new;
let reply = client
.decide
.await?;
default-features = false leaves out the CLI's dependencies; the library then builds for
wasm32-unknown-unknown too, where requests go through the host's fetch. The command feature
adds question specs (jev::spec), the command's own printing (jev::print) and the rules engine
(jev::rules) without the command, for another program that wants to offer jev the way the
terminal does:
let rules = parse?;
let outcome = rules.evaluate?; // items and outputs, with their scores
let svg = rules.graph_svg?; // or graph_text, for a terminal
Development
CI runs cargo fmt --check, clippy for the host and for wasm32-unknown-unknown, and the tests.
Pushing a tag such as v0.2.0 builds the four binaries and puts them on a Release; the same build
can be started by hand from the Actions tab, which leaves them as artifacts.
Extracted from wasm-agent, where this began as
crates/jev and where dx's shell offers the same command to an agent. This repository was called
jev-cli until the fuzzy rules arrived; GitHub redirects the old URLs.
License
MIT — see LICENSE. The bundled Agent Skill says the same in its own frontmatter, so a
project that runs jev add skill carries it with the files.