jev-recipes 0.9.11 → 0.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,12 @@ const stopWords = new Set([
17
17
  'should',
18
18
  'with',
19
19
  ]);
20
+ const EXACT_ID_BONUS = 10_000;
21
+ const FULL_COVERAGE_BONUS = 1_000;
22
+ const PHRASE_BONUS = 50;
23
+ const EXACT_MATCH = 1;
24
+ const SHARED_STEM_MINIMUM_LENGTH = 4;
25
+ const SHARED_STEM_MINIMUM_RATIO = 0.6;
20
26
  export function searchRecipes(recipes, filters) {
21
27
  const query = normalize(filters.query ?? '');
22
28
  const words = query.split(' ').filter(Boolean);
@@ -33,7 +39,19 @@ export function searchRecipes(recipes, filters) {
33
39
  function scoreRecipe(recipe, query, terms) {
34
40
  if (!query)
35
41
  return 0;
36
- const fields = [
42
+ const fields = weightedFields(recipe);
43
+ const matches = terms.map((term) => bestMatch(fields, term));
44
+ if (matches.every((match) => match.quality === 0))
45
+ return -1;
46
+ const coverage = matches.reduce((total, match) => total + match.quality, 0) / terms.length;
47
+ const relevance = matches.reduce((total, match) => total + match.weight, 0);
48
+ return ((normalize(recipe.id) === query ? EXACT_ID_BONUS : 0) +
49
+ (matchesPhrase(recipe, terms) ? PHRASE_BONUS : 0) +
50
+ coverage * FULL_COVERAGE_BONUS +
51
+ relevance);
52
+ }
53
+ function weightedFields(recipe) {
54
+ return [
37
55
  [normalize(recipe.id), 40],
38
56
  [normalize(recipe.title), 25],
39
57
  [normalize(recipe.tags.join(' ')), 20],
@@ -41,16 +59,32 @@ function scoreRecipe(recipe, query, terms) {
41
59
  [normalize(recipe.description), 5],
42
60
  [normalize(recipe.category), 1],
43
61
  ];
44
- let score = normalize(recipe.id) === query ? 1000 : 0;
45
- if (terms.length > 1 && normalize(recipe.useWhen ?? '').includes(terms.join(' ')))
46
- score += 50;
47
- for (const term of terms) {
48
- const matches = fields.filter(([text]) => text.includes(term));
49
- if (!matches.length)
50
- return -1;
51
- score += Math.max(...matches.map(([, weight]) => weight));
52
- }
53
- return score;
62
+ }
63
+ function bestMatch(fields, term) {
64
+ return fields.reduce((best, [text, fieldWeight]) => {
65
+ const quality = fieldMatch(text, term);
66
+ return {
67
+ quality: Math.max(best.quality, quality),
68
+ weight: Math.max(best.weight, fieldWeight * quality),
69
+ };
70
+ }, { quality: 0, weight: 0 });
71
+ }
72
+ function matchesPhrase(recipe, terms) {
73
+ return terms.length > 1 && normalize(recipe.useWhen ?? '').includes(terms.join(' '));
74
+ }
75
+ function fieldMatch(text, term) {
76
+ if (text.includes(term))
77
+ return EXACT_MATCH;
78
+ return text.split(' ').reduce((best, word) => Math.max(best, stemSimilarity(word, term)), 0);
79
+ }
80
+ function stemSimilarity(first, second) {
81
+ const longerLength = Math.max(first.length, second.length);
82
+ let sharedLength = 0;
83
+ while (sharedLength < longerLength && first[sharedLength] === second[sharedLength])
84
+ sharedLength++;
85
+ const sharesStem = sharedLength >= SHARED_STEM_MINIMUM_LENGTH &&
86
+ sharedLength >= Math.ceil(longerLength * SHARED_STEM_MINIMUM_RATIO);
87
+ return sharesStem ? sharedLength / longerLength : 0;
54
88
  }
55
89
  function normalize(text) {
56
90
  return text
@@ -25,8 +25,8 @@ declare const archiveSchema: z.ZodObject<{
25
25
  inputFingerprint: z.ZodString;
26
26
  answerKeyFingerprint: z.ZodString;
27
27
  split: z.ZodEnum<{
28
- "held-out": "held-out";
29
28
  development: "development";
29
+ "held-out": "held-out";
30
30
  }>;
31
31
  requestedModel: z.ZodString;
32
32
  policy: z.ZodObject<{
@@ -54,8 +54,8 @@ declare const archiveSchema: z.ZodObject<{
54
54
  rationale: z.ZodString;
55
55
  family: z.ZodOptional<z.ZodString>;
56
56
  split: z.ZodDefault<z.ZodEnum<{
57
- "held-out": "held-out";
58
57
  development: "development";
58
+ "held-out": "held-out";
59
59
  }>>;
60
60
  provenance: z.ZodDefault<z.ZodObject<{
61
61
  method: z.ZodEnum<{
@@ -113,7 +113,7 @@ export declare function freezePolicy(run: EvaluationRun): EvaluationPolicy;
113
113
  export declare function compare(baseline: EvaluationRun, candidate: EvaluationRun): {
114
114
  recipe: string;
115
115
  cases: number;
116
- split: "held-out" | "development";
116
+ split: "development" | "held-out";
117
117
  baseline: {
118
118
  runId: string;
119
119
  packageVersion: string;
@@ -5,5 +5,6 @@ export declare function comparableDecision(value: unknown, recipe: string): unkn
5
5
  export declare function matchesExpected(actual: unknown, expected: unknown, recipe: string): boolean;
6
6
  export declare function isReady(result: unknown, recipe: string): boolean;
7
7
  export declare function isMetadataPath(schema: Schema, segments: string[]): boolean;
8
+ export declare function gatedTwinFor(schema: Schema, segments: string[]): string | undefined;
8
9
  export declare function schemaAtPath(schema: Schema, segments: string[]): Schema | undefined;
9
10
  export {};
@@ -76,6 +76,19 @@ export function isMetadataPath(schema, segments) {
76
76
  const child = schemaAtPath(schema, [first]);
77
77
  return child !== undefined && isMetadataPath(child, rest);
78
78
  }
79
+ export function gatedTwinFor(schema, segments) {
80
+ const name = segments.at(-1);
81
+ if (name === undefined || !name.length)
82
+ return undefined;
83
+ const twin = `suggested${name[0].toUpperCase()}${name.slice(1)}`;
84
+ const parent = schemaAtPath(schema, segments.slice(0, -1));
85
+ return parent !== undefined && hasProperty(parent, twin) ? twin : undefined;
86
+ }
87
+ function hasProperty(schema, name) {
88
+ if (Array.isArray(schema.anyOf))
89
+ return schema.anyOf.some((branch) => isRecord(branch) && hasProperty(branch, name));
90
+ return isRecord(schema.properties) && Object.hasOwn(schema.properties, name);
91
+ }
79
92
  export function schemaAtPath(schema, segments) {
80
93
  if (!segments.length)
81
94
  return schema;
@@ -3,7 +3,7 @@ import { readFile } from 'node:fs/promises';
3
3
  import { z } from 'zod';
4
4
  import { recipeNameSchema } from '../catalog/schema.js';
5
5
  import { isRecord } from './decisions.js';
6
- import { comparableDecision, isMetadataPath, schemaAtPath, resultSchemaFor } from './comparison.js';
6
+ import { comparableDecision, gatedTwinFor, isMetadataPath, schemaAtPath, resultSchemaFor, } from './comparison.js';
7
7
  import { evaluationCaseSchema } from './schema.js';
8
8
  export async function loadEvaluationRecipe(name) {
9
9
  const id = recipeNameSchema.parse(name);
@@ -45,9 +45,13 @@ export function validateCases(recipe, values) {
45
45
  }
46
46
  recipe.inputSchema.parse(entry.input);
47
47
  for (const [path, expected] of Object.entries(entry.expected)) {
48
- if (isMetadataPath(resultSchema, path.split('.')))
48
+ const segments = path.split('.');
49
+ if (isMetadataPath(resultSchema, segments))
49
50
  throw new Error(`${entry.id}: expected must name an ungated decision, not ${path}.`);
50
- const field = schemaAtPath(resultSchema, path.split('.'));
51
+ const twin = gatedTwinFor(resultSchema, segments);
52
+ if (twin)
53
+ throw new Error(`${entry.id}: ${path} is gated by the review policy; label ${twin} instead.`);
54
+ const field = schemaAtPath(resultSchema, segments);
51
55
  if (!field)
52
56
  throw new Error(`${entry.id}: unknown result field ${path}.`);
53
57
  if (!acceptsValue(field, expected))
@@ -81,7 +81,7 @@ async function evaluateCase(recipe, goldenCase, client, options) {
81
81
  function recordResponses(client, exchanges) {
82
82
  return {
83
83
  async systemOne(request, options) {
84
- const exchange = { request: structuredClone(request), durationMs: 0 };
84
+ const exchange = { request: asArchivedRequest(request), durationMs: 0 };
85
85
  exchanges.push(exchange);
86
86
  const started = performance.now();
87
87
  try {
@@ -106,7 +106,8 @@ export function replayResponses(exchanges) {
106
106
  return {
107
107
  client: {
108
108
  async systemOne(request) {
109
- const index = remaining.findIndex((exchange) => isDeepStrictEqual(exchange.request, request));
109
+ const archived = asArchivedRequest(request);
110
+ const index = remaining.findIndex((exchange) => isDeepStrictEqual(exchange.request, archived));
110
111
  if (index === -1)
111
112
  throw new ReplayMismatch('Replay requested an unrecorded model response. The prompt or model has changed.');
112
113
  const [exchange] = remaining.splice(index, 1);
@@ -121,6 +122,9 @@ export function replayResponses(exchanges) {
121
122
  },
122
123
  };
123
124
  }
125
+ function asArchivedRequest(request) {
126
+ return JSON.parse(JSON.stringify(request));
127
+ }
124
128
  function safeError(error) {
125
129
  let message = error instanceof Error ? error.message : String(error);
126
130
  for (const [name, value] of Object.entries(process.env)) {
@@ -15,8 +15,8 @@ export declare const evaluationCaseSchema: z.ZodObject<{
15
15
  rationale: z.ZodString;
16
16
  family: z.ZodOptional<z.ZodString>;
17
17
  split: z.ZodDefault<z.ZodEnum<{
18
- "held-out": "held-out";
19
18
  development: "development";
19
+ "held-out": "held-out";
20
20
  }>>;
21
21
  provenance: z.ZodDefault<z.ZodObject<{
22
22
  method: z.ZodEnum<{
@@ -54,8 +54,8 @@ export declare const evaluationOptionsSchema: z.ZodObject<{
54
54
  maxRequests: z.ZodDefault<z.ZodNumber>;
55
55
  model: z.ZodOptional<z.ZodString>;
56
56
  split: z.ZodDefault<z.ZodEnum<{
57
- "held-out": "held-out";
58
57
  development: "development";
58
+ "held-out": "held-out";
59
59
  }>>;
60
60
  policy: z.ZodDefault<z.ZodObject<{
61
61
  minConfidence: z.ZodOptional<z.ZodNumber>;
@@ -1,12 +1,12 @@
1
1
  import { choice } from '@typesafe-ai/sdk';
2
2
  import { evaluateWithJev } from '../../src/client.js';
3
3
  import { parseChoiceAnswer } from '../../src/answers.js';
4
- import { parseDecisionState } from '../../src/decisions.js';
4
+ import { asDecisionInstruction, parseDecisionState } from '../../src/decisions.js';
5
5
  import { gameActionInputSchema, gameActionResultSchema } from './schema.js';
6
- const instructions = [
6
+ const instructions = asDecisionInstruction([
7
7
  'Which available action should this player take next, given the supplied game state, player state, rules, and objective?',
8
8
  ...Object.entries(gameActionInputSchema.shape).map(([name, schema]) => `${name}: ${schema.description}`),
9
- ].join('\n');
9
+ ].join('\n'));
10
10
  export async function gameAction(input, options = {}) {
11
11
  const { legalActions, ...state } = gameActionInputSchema.parse(input);
12
12
  if (legalActions.length === 0)
@@ -0,0 +1 @@
1
+ export { createClient } from './client.js';
@@ -0,0 +1 @@
1
+ export { createClient } from './client.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jev-recipes",
3
- "version": "0.9.11",
3
+ "version": "0.9.13",
4
4
  "description": "248 small Jev decisions for JavaScript and TypeScript: route, rerank, gate, grade, compare, and label text for agents, RAG, support, code review, and music. Each returns a typed result with explicit uncertainty. Plug into Vercel AI SDK or LangChain agents, import one recipe, or run the CLI with JSON from any language.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,6 +19,10 @@
19
19
  "types": "./dist/src/index.d.ts",
20
20
  "import": "./dist/src/index.js"
21
21
  },
22
+ "./client": {
23
+ "types": "./dist/src/create-client.d.ts",
24
+ "import": "./dist/src/create-client.js"
25
+ },
22
26
  "./catalog": {
23
27
  "types": "./dist/catalog/index.d.ts",
24
28
  "import": "./dist/catalog/index.js"