Style scorer: the code
These are the files that do most of the work. The comments explain why each part works the way it does. If anything is unclear, or you would do it differently, tell me. I will also send you the full code for free, with nothing to sign up for. Ask for the full code
rules/not-x-but-y.ts
One rule, whole. Each rule returns the exact characters that break it, so the page can mark them in place. 35 lines
import { type Rule, matchAll } from "./types.ts";
const SUBJ = String.raw`(?:it|this|that|he|she|they|we|you|i)`;
const BE = String.raw`(?:['’]s|['’]re|['’]m| is| was| are| were| am)`;
const NEG_BE = String.raw`(?:isn['’]t|wasn['’]t|aren['’]t|weren['’]t|is not|was not|are not|were not)`;
const GAP = String.raw`[^.;!?\n]{1,80}?`;
const TURN = String.raw`\s*(?:,|;|:|\u2014|\.)\s*`;
const PATTERNS = [
// "It's not the price, it's the wait."
new RegExp(String.raw`\b${SUBJ}${BE}\s+not\s+(?:just\s+|only\s+|merely\s+|about\s+)?${GAP}${TURN}${SUBJ}${BE}\b`, "gi"),
// "The problem isn't speed. It's trust."
new RegExp(String.raw`\b${NEG_BE}\s+${GAP}${TURN}${SUBJ}${BE}\b`, "gi"),
// "not just a tool, but a partner"
new RegExp(String.raw`\bnot\s+(?:just|only|merely|simply)\s+${GAP},?\s+but\b`, "gi"),
];
export const notXButY: Rule = {
id: "not-x-but-y",
name: "Not X, it's Y",
styleRule: "The \"not X, it's Y\" reveal.",
how: "A negated clause turned straight into a positive one: \"it's not X, it's Y\", \"X isn't A. It's B.\", or \"not just X, but Y\".",
check(text) {
const all = PATTERNS.flatMap((p) => matchAll("not-x-but-y", text, p));
all.sort((a, b) => a.start - b.start || b.end - a.end);
// Two patterns can match the same words; keep the first of any overlapping pair.
return all.filter((f, i) => i === 0 || f.start >= all[i - 1].end);
},
fires: [
"It's not the price, it's the wait.",
"The problem isn't speed. It's trust.",
"This is not just a tool, but a partner.",
],
quiet: ["It's not raining.", "He's not here, so we left without him.", "She did not just leave; she ran."],
};
eval/stats.ts
Kappa and the intervals. Every interval on the results page comes from this file. 71 lines
// Every interval the results page shows comes from here.
/** Wilson score interval for k successes in n trials, at 95%. Sensible at 0% and 100%. */
export function wilson(k: number, n: number, z = 1.96): [number, number] {
if (n === 0) return [0, 1];
const p = k / n;
const denom = 1 + (z * z) / n;
const centre = (p + (z * z) / (2 * n)) / denom;
const half = (z * Math.sqrt((p * (1 - p)) / n + (z * z) / (4 * n * n))) / denom;
return [Math.max(0, centre - half), Math.min(1, centre + half)];
}
/** Cohen's kappa for two raters over the same items. Null when chance agreement is total. */
export function cohenKappa<T>(a: T[], b: T[]): number | null {
if (a.length !== b.length) throw new Error("kappa: raters must label the same items");
const n = a.length;
if (n === 0) return null;
const cats = [...new Set([...a, ...b])];
let agree = 0;
for (let i = 0; i < n; i++) if (a[i] === b[i]) agree += 1;
const po = agree / n;
let pe = 0;
for (const c of cats) pe += (a.filter((x) => x === c).length / n) * (b.filter((x) => x === c).length / n);
if (pe === 1) return null;
return (po - pe) / (1 - pe);
}
/** Small seeded generator, so a bootstrap interval is the same on every run. */
export function mulberry32(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (s + 0x6d2b79f5) >>> 0;
let t = s;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
/**
* Percentile bootstrap over clusters (for example, one paragraph and all its pairs), so pairs
* from the same paragraph are never treated as independent.
*/
export function bootstrap<C>(clusters: C[], stat: (sample: C[]) => number | null, opts: { resamples?: number; seed?: number } = {}): [number, number] | null {
const resamples = opts.resamples ?? 2000;
const rand = mulberry32(opts.seed ?? 1);
const values: number[] = [];
for (let r = 0; r < resamples; r++) {
const sample = Array.from({ length: clusters.length }, () => clusters[Math.floor(rand() * clusters.length)]);
const v = stat(sample);
if (v !== null && Number.isFinite(v)) values.push(v);
}
if (values.length < resamples / 2) return null;
values.sort((x, y) => x - y);
const at = (q: number) => values[Math.min(values.length - 1, Math.max(0, Math.round(q * (values.length - 1))))];
return [at(0.025), at(0.975)];
}
/** Seeded sample without replacement, in the original order. */
export function sample<T>(items: T[], k: number, seed: number): T[] {
const rand = mulberry32(seed);
const idx = items.map((_, i) => i);
for (let i = idx.length - 1; i > 0; i--) {
const j = Math.floor(rand() * (i + 1));
[idx[i], idx[j]] = [idx[j], idx[i]];
}
return idx.slice(0, Math.min(k, items.length)).sort((a, b) => a - b).map((i) => items[i]);
}
export const round = (x: number, places = 4) => Math.round(x * 10 ** places) / 10 ** places;
export const roundPair = (p: [number, number] | null, places = 4): [number, number] | null => (p ? [round(p[0], places), round(p[1], places)] : null);
judge/budget.ts
The spend guard for the Claude judge. A run cannot start without a budget, and each call is charged its worst case first. 85 lines
// The spend guard. Before each call it charges the worst case (one token per input byte plus the
// full reply ceiling); a timeout is also charged the worst case, since it may be billed.
import { callCost, settingsFor } from "./prices.ts";
import { type Adapter, type Completion, type CompletionRequest, NetworkError } from "./types.ts";
/** Raising this is meant to be a deliberate edit. */
export const MAX_BUDGET_USD = 10;
/** One call per text, plus 20% for retries. */
export const CALLS_PER_TEXT = 1.2;
const REQUEST_OVERHEAD_TOKENS = 1000;
export class BudgetExhausted extends Error {
constructor(message: string) {
super(message);
this.name = "BudgetExhausted";
}
}
export class Budget {
readonly limitUsd: number;
spentUsd = 0;
calls = 0;
constructor(limitUsd: number) {
if (!(limitUsd >= 0) || !Number.isFinite(limitUsd)) throw new Error(`Bad budget ${limitUsd}`);
this.limitUsd = limitUsd;
}
canAfford(usd: number): boolean {
return this.spentUsd + usd <= this.limitUsd;
}
}
export function worstCaseCost(req: CompletionRequest): number {
const bytes = Buffer.byteLength(req.system, "utf8") + Buffer.byteLength(req.user, "utf8");
return callCost(req.model, REQUEST_OVERHEAD_TOKENS + bytes, req.maxTokens);
}
export interface BudgetedAdapter extends Adapter {
readonly budget: Budget;
}
export function budgeted(inner: Adapter, budget: Budget): BudgetedAdapter {
return {
provider: inner.provider,
budget,
async complete(req: CompletionRequest): Promise<Completion> {
settingsFor(req.model);
const worst = worstCaseCost(req);
if (!budget.canAfford(worst)) {
throw new BudgetExhausted(`next call could cost up to $${worst.toFixed(4)}; spent $${budget.spentUsd.toFixed(4)} of $${budget.limitUsd.toFixed(2)}`);
}
budget.calls += 1;
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new NetworkError(`timed out after ${req.timeoutMs} ms`, true)), req.timeoutMs);
});
try {
const out = await Promise.race([inner.complete(req), timeout]);
budget.spentUsd += callCost(req.model, out.inputTokens, out.outputTokens);
return out;
} catch (err) {
if (err instanceof NetworkError && err.mayHaveBilled) budget.spentUsd += worst;
throw err;
} finally {
clearTimeout(timer);
}
},
};
}
export function estimateCost(model: string, texts: number): number {
const t = settingsFor(model).typical;
return texts * CALLS_PER_TEXT * callCost(model, t.inputTokens, t.outputTokens);
}
/** Why a paid run may not start, or null. */
export function refusalBeforeStart(budgetUsd: number | undefined, estimateUsd: number): string | null {
if (budgetUsd === undefined) return "a judge run calls a paid model, so it needs --budget <USD>. The default budget is zero.";
if (!(budgetUsd > 0) || !Number.isFinite(budgetUsd)) return `--budget must be a positive number of US dollars (got ${budgetUsd})`;
if (budgetUsd > MAX_BUDGET_USD) return `--budget ${budgetUsd} is over the $${MAX_BUDGET_USD} ceiling in judge/budget.ts. Raise MAX_BUDGET_USD there only if you mean to.`;
if (estimateUsd > budgetUsd) return `the estimated cost $${estimateUsd.toFixed(4)} is over the budget $${budgetUsd.toFixed(4)}. Fewer texts, or a bigger budget.`;
return null;
}