@hikae/jev-algorithms
v0.3.0
Published
Runtime-agnostic algorithms built on TypeSafe's Jev structured-evaluation model.
Maintainers
Readme
jev-algorithms
Gallery

Install
npm install @hikae/jev-algorithmsQuick start
import { createTypeSafeClient, sort } from "@hikae/jev-algorithms";
const jev = createTypeSafeClient({ apiKey: process.env.TYPESAFE_API_KEY! });
const inbox = [
{ id: "1", from: "[email protected]", subject: "Contract", unread: true, body: "Please sign by Friday." },
{ id: "2", from: "[email protected]", subject: "Weekly digest", unread: false, body: "This week in..." },
];
const ordered = await sort(jev, inbox, {
task: "for replying first",
stateOf: (mail) => ({
from: mail.from,
subject: mail.subject,
unread: mail.unread,
preview: mail.body.slice(0, 300),
}),
});
// [Contract mail, Weekly digest]The client contract
interface JevClient {
evaluate(input: { state: JevState; questions: JevQuestions }): Promise<JevAnswers>;
}| Adapter | Factory | Notes |
| --- | --- | --- |
| TypeSafe API | createTypeSafeClient({ apiKey, model?, baseUrl?, fetch? }) | Runtime-agnostic default. Keep the key on the server. |
| Workers AI | createWorkersAiClient(binding, model?) | Pass env.AI. Structural, no Cloudflare types required. |
| In memory | createMemoryClient(responder) | Test double; records every calls evaluation. |
Question builders (noul, choice, score) and answer readers
(readNoul, readChoice, readScore) are exported for building your own
algorithms on the same contract.
Algorithms
Every algorithm states its request cost in terms of n items. A request
carries up to 40 questions, so "one request" is rarely one comparison.
Sorting and selection
| Function | What it does | Requests |
| --- | --- | --- |
| sort(client, items, options) | Total order from pairwise "which ranks higher?" answers | O((n/40) log n) |
| sortWith(items, compare) | Same, with an injected comparator (no client) | O((n/40) log n) |
| selectTopK(client, items, k, options) | Top-k, ordered, via quickselect | O(n/40 + (k/40) log k) |
| selectTopKWith(items, k, compare) | Same, with an injected comparator | O(n/40 + (k/40) log k) |
| createPairComparator(client, items, options) | Build the comparator to plug Jev into any sort you own | — |
The sort is randomized quicksort whose recursion runs level by level. Nodes on
a level are disjoint, so a level's comparisons are batched into ⌈n/40⌉
requests at DEFAULT_MAX_PAIRS_PER_REQUEST pairs each. The whole order costs
O((n/40) log n) requests instead of n log n individual comparisons.
selectTopK skips the side that cannot contain the k-th item.
Threshold search
const cutoff = await findFirstTrue(jev, emailsByRecency, {
stateOf: (mail) => ({ ageDays: mail.ageDays }),
instruction: "Is `candidate` older than 30 days?",
});findFirstTrue binary-searches a monotone predicate in O(log n) calls.
Ranking from noisy comparisons
Pairwise answers can cycle (A > B > C > A). elo and bradleyTerry turn a
list of Comparisons into one score per id; rankByElo and
rankByBradleyTerry order them. winner is "first" | "second" | "draw",
named by position so a player called "a" or "b" is never ambiguous.
Clustering
const groups = await clusterByRelation(jev, tickets, {
relation: "duplicate support ticket",
stateOf: (t) => ({ subject: t.subject, body: t.body.slice(0, 300) }),
});Union-find over pairwise equivalence. Every unordered pair is compared once;
threshold controls how confident Jev must be to merge, and maxComparisons
caps sampling for large inputs.
Classification and scoring
classifyChoice(client, state, { instruction, labels, abstainBelow })— Choice with an abstain band, so uncertain cases can be escalated instead of guessed.booleanDecision(client, state, { instruction, threshold })— Noul plus a thresholded verdict.rubricScore(client, state, { instruction, levels })— Score normalized to0..1, with the nearest level label.
Matching
stableMatching(proposers, receivers, proposerPrefs, receiverPrefs) is a pure
Gale-Shapley implementation. buildPreferences(client, choosers, candidates, options)
builds each side's preference order with pairwise comparisons first.
Design notes
- Batch by default. Every algorithm packs up to 40 questions into one
evaluatecall and references items through a chunk-local index so a shared item travels once. - Fail loud, not wrong. If Jev omits an answer, algorithms throw. They
never persist a fabricated order. The one exception is up to the caller:
classifyChoicecan abstain by design. - Persistence is yours. Algorithms return orders and scores; storing them (for example, an integer priority column) is a schema decision, not a library one.
Testing and mutation testing
npm test # build + node:test
npm run typecheck
npm run mutation # build + StrykerTests are judged by mutation coverage, not line coverage: a test only earns its
place if it kills a mutant. Stryker runs with the command test runner against
the built dist and a break threshold of 80.
Two policies keep the signal honest and the suite lean:
StringLiteralmutants are excluded. Prompts and error wording are content, not logic; asserting exact prompt text would make the suite brittle without catching real bugs.- Survivors include equivalent mutations in union-find and pivot selection, as
well as coverage gaps in diagnostics and less common syntax paths. Inspect
reports/mutation.json; a passing threshold does not mean every survivor is equivalent or that Jev's predictions are accurate.
License
MIT
