jevstrudel.git / worker / src / reference.ts

Strudel's function reference as the MCPs' api_reference serves it: the reference tab's functions (website/src/jev/reference.mjs), which the build writes to /jev/reference.json (website/src/pages/jev/reference.json.js) from jsdoc's doc.json, read like the song catalog (catalog.ts). Static per deploy, so kept for five minutes per isolate.

6import { siteFile } from './catalog';
7import type { Env } from './env';
9export const REFERENCE_PATH = '/jev/reference.json';
10export type Fn = {
11  name: string;
12  synonyms: string[];
13  tags: string[];
14  description: string;
15  params: { name: string; type: string; description: string }[];
16  examples: string[];
17};
18
19const TTL_MS = 5 * 60_000;

Matches a search lists at most, and the characters of a query.

21export const MAX_MATCHES = 25;
22export const MAX_QUERY = 100;
23let kept: { fns: Fn[]; until: number } | null = null;
25export async function reference(env: Env): Promise<Fn[]> {
26  if (kept && Date.now() < kept.until) return kept.fns;
27  const res = await siteFile(env, REFERENCE_PATH);
28  if (!res.ok) throw new Error(`the site's reference answered ${res.status}`);
29  const { functions } = (await res.json()) as { functions?: Fn[] };
30  if (!Array.isArray(functions)) throw new Error("the site's reference is malformed");
31  kept = { fns: functions, until: Date.now() + TTL_MS };
32  return functions;
33}
34export const forgetReference = () => void (kept = null);
35
36const firstSentence = (s: string) => (/^(.+?[.!?])(\s|$)/s.exec(s)?.[1] ?? s).replace(/\s+/g, ' ').slice(0, 160);
37const names = (f: Fn) => [f.name, ...f.synonyms];

One function whole, as text.

40export function describeFn(f: Fn): string {
41  const lines = [`${f.name}(${f.params.map((p) => p.name).join(', ')})`];
42  if (f.synonyms.length) lines.push(`synonyms: ${f.synonyms.join(', ')}`);
43  if (f.tags.length) lines.push(`tags: ${f.tags.join(', ')}`);
44  lines.push('', f.description);
45  if (f.params.length) {
46    lines.push('', 'parameters:');
47    for (const p of f.params) lines.push(`- ${p.name}${p.type ? ` (${p.type})` : ''}${p.description ? `: ${p.description}` : ''}`);
48  }
49  if (f.examples.length) {
50    lines.push('', 'examples:');
51    for (const e of f.examples) lines.push(e);
52  }
53  return lines.join('\n');
54}

api_reference: one function by name, or the functions matching query: any of its words in a name, synonym, tag or the description. Each word found weighs 3 when it is a name, 2 inside a name, 1 in a tag or the description; the heaviest come first, then those matching more words.

60export function answerReference(fns: Fn[], args: { query?: unknown; name?: unknown }): string {
61  if (typeof args.name === 'string' && args.name.trim()) {
62    const name = args.name.trim().slice(0, MAX_QUERY);
63    const f = fns.find((x) => x.name === name) ?? fns.find((x) => x.synonyms.includes(name));
64    if (!f) {
65      const near = fns.filter((x) => names(x).some((n) => n.toLowerCase().includes(name.toLowerCase()))).slice(0, 10);
66      throw new Error(`no function ${name}${near.length ? `; did you mean ${near.map((x) => x.name).join(', ')}?` : '; search with query'}`);
67    }
68    return describeFn(f);
69  }
70  if (typeof args.query !== 'string' || !args.query.trim()) throw new Error('pass query (words to search) or name (one function)');
71  const words = [...new Set(args.query.toLowerCase().slice(0, MAX_QUERY).split(/[\s,]+/).filter(Boolean))];
72  const rank = (f: Fn) => {
73    const own = names(f).map((n) => n.toLowerCase());
74    const all = `${own.join(' ')} ${f.tags.join(' ')} ${f.description}`.toLowerCase();
75    let hits = 0;
76    let weight = 0;
77    for (const w of words) {
78      if (!all.includes(w)) continue;
79      hits += 1;
80      weight += own.includes(w) ? 3 : own.some((n) => n.includes(w)) ? 2 : 1;
81    }
82    return { hits, weight };
83  };
84  const found = fns
85    .map((f) => ({ f, ...rank(f) }))
86    .filter((x) => x.hits > 0)
87    .sort((a, b) => b.weight - a.weight || b.hits - a.hits || a.f.name.localeCompare(b.f.name));
88  if (!found.length) return `nothing in the reference matches "${args.query.slice(0, MAX_QUERY)}"`;
89  const shown = found.slice(0, MAX_MATCHES).map(({ f }) => `${f.name}${f.synonyms.length ? ` (${f.synonyms.join(', ')})` : ''}: ${firstSentence(f.description)}`);
90  const more = found.length > MAX_MATCHES ? `\n… ${found.length - MAX_MATCHES} more; narrow the query` : '';
91  const some = words.length > 1 && found.some((x) => x.hits < words.length) ? ', best first' : '';
92  return `${found.length} of ${fns.length} functions match${some}; api_reference with name for one whole:\n${shown.join('\n')}${more}`;
93}