jev-tree

September 18, 2026 · View on GitHub

Recursive Jev choice over a taxonomy. TypeSafe’s Jev can only list 255 options in one choice question. Real catalogs are bigger. jev-tree walks a JSON shape and calls Jev once per level (or per partition) so you can select among thousands of leaves.

Same stack as jevlogs: typesafe-ai/jev through Vercel AI Gateway, ai@7.0.105, Node.js 22+, MIT.

npm version CI MIT Node 22+

Guide · API · Explainer · 20s intro · Dataset

There is no marketing site. This repo is the product. The explainer is a 20-second film plus a click-through essay, not a dashboard.

Why

A flat choice with 1,000 SKUs, incident types, or workflow ids does not fit. Truncating to 255 drops the tail (in our bake-off, all of Cape Town). Splitting the catalog into a tree — and auto-bucketing oversized sibling lists — makes each call a short, precise question.

Default 32 options per call. Raise maxFanout up to 255.

Install

npm install jev-tree

Live calls need AI_GATEWAY_API_KEY (Vercel AI Gateway). Not an OpenAI key.

0.1.0 is the first real release (latest). Ignore any placeholder tag. How we publish: docs/release.md.

Shape

type TreeNode = {
  id: string;
  label: string;
  description?: string;
  children?: TreeNode[];  // branch
  value?: unknown;        // returned when this leaf is selected
};

A node with no children (or an empty array) is a leaf. Pass one root, a forest (TreeNode[]), or { "shape": ... } in a JSON file. Ids must be unique. __root is reserved.

Your ids need not be legal Jev choice keys. The library sends o0, o1, … and maps back.

Use

import { createJevTree } from 'jev-tree';

const tree = createJevTree({ maxFanout: 32 });
const result = await tree.select({
  state: 'Replica lag 47m on primary still accepting writes',
  question: 'Which incident type is this?',
  shape: {
    id: 'incidents',
    label: 'incidents',
    children: [
      { id: 'payments', label: 'payments', children: [/* … */] },
      { id: 'data', label: 'data', children: [
        { id: 'replica_lag', label: 'multi-minute replica lag', value: { queue: 'sev1' } },
      ]},
    ],
  },
});

result.ids;         // ['incidents', 'data', 'replica_lag']
result.value;       // { queue: 'sev1' }
result.reason;      // 'model' | 'unavailable'

A flat list of 300+ leaves is fine. Siblings above maxFanout are partitioned into labeled buckets (first … last), Jev picks a bucket, then the walk continues. You never send more than maxFanout criteria in one experimental_evaluate call.

Timeouts and provider failures return reason: 'unavailable' and do not invent a leaf.

CLI

npx jev-tree                         # offline demo, no key
npx jev-tree --live --shape taxonomy.json --state "the record"
npx jev-tree --count --shape taxonomy.json

Options

OptionDefaultMeaning
maxFanout32Options per Jev call; integer 2–255
timeoutMs8000Per-call abort; timeouts do not retry
maxDepth16Stop and mark unavailable
maxCalls64Stop and mark unavailable
instructions(built-in)Prepended to every choice
evaluatorGateway JevInject a fake chooser in tests
validatetrueparseShape before walking

Evidence

320-leaf multi-region incident catalog, 180 labeled tickets, live Jev on Vercel AI Gateway with ZDR. Truncating to 255 gets 0% of the tail. The authored tree recovers it.

Synthetic. Seed 20260917. Not production tickets. Confirm spend on the Gateway dashboard.

What this is not

Not a host for Jev. Not a substitute for a well-authored taxonomy — explicit branches beat a 10,000-item flat list. Auto-partition is the fallback when a node has too many children.

Independent project. Not affiliated with TypeSafe or Vercel. The hosted model is TypeSafe’s Jev via AI Gateway.

License

MIT