@aeondave/pi-skill-optimizer 3.0.0

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.
@@ -0,0 +1,692 @@
1
+ /** Pure evaluation helpers for paired baseline/auto skill-catalog corpora. */
2
+
3
+ import { parseSkills, type Skill } from "./skills.ts";
4
+
5
+ export type EvaluationArm = "baseline" | "auto";
6
+ export type SkillExposure = "full" | "intent" | "name-only" | "missing";
7
+
8
+ export interface RequiredGroup {
9
+ /** Alternatives that satisfy the same requirement. */
10
+ anyOf: readonly string[];
11
+ }
12
+
13
+ export interface SkillRenderState {
14
+ key: string;
15
+ blockIndex: number;
16
+ occurrence: number;
17
+ name: string;
18
+ state: SkillExposure;
19
+ loadable: boolean;
20
+ descriptionVerbatim: boolean;
21
+ descriptionExtractedVerbatim: boolean;
22
+ locationVerbatim: boolean;
23
+ original: Skill;
24
+ rendered?: Skill;
25
+ }
26
+
27
+ export interface SkillStateAnalysis {
28
+ states: SkillRenderState[];
29
+ originalNames: string[];
30
+ renderedNames: string[];
31
+ unexpectedNames: string[];
32
+ namesPreserved: boolean;
33
+ orderPreserved: boolean;
34
+ allRetainedLoadable: boolean;
35
+ }
36
+
37
+ export interface CoverageMetric {
38
+ covered: number;
39
+ total: number;
40
+ recall: number | null;
41
+ }
42
+
43
+ export interface RequiredGroupMetrics {
44
+ baseFull: CoverageMetric;
45
+ baseIntent: CoverageMetric;
46
+ baseLoadable: CoverageMetric;
47
+ overlay: CoverageMetric;
48
+ prefetch: CoverageMetric;
49
+ modelSelected: CoverageMetric | null;
50
+ allGroupsBaseFull: boolean | null;
51
+ allGroupsBaseIntent: boolean | null;
52
+ allGroupsBaseLoadable: boolean | null;
53
+ allGroupsOverlay: boolean | null;
54
+ allGroupsPrefetched: boolean | null;
55
+ allGroupsModelSelected: boolean | null;
56
+ }
57
+
58
+ /** Counts produced by an authoritative tokenizer. No estimate is inferred. */
59
+ export interface ExactTokenCounts {
60
+ tokenizer: string;
61
+ before: number;
62
+ after: number;
63
+ }
64
+
65
+ export interface ArmEvaluationInput {
66
+ arm: EvaluationArm;
67
+ /** Full catalog before auto rendering. */
68
+ originalText: string;
69
+ /** Query-independent prompt-cache base for this arm. */
70
+ baseText: string;
71
+ /** Query-specific eager prefetch surface; empty for baseline. */
72
+ overlayText: string;
73
+ requiredGroups: readonly RequiredGroup[];
74
+ /** Names selected by the prefetch planner, independently of overlay parsing. */
75
+ prefetchedSkillNames: readonly string[];
76
+ /** Downstream model skill choices, when independently observed. */
77
+ modelSelectedSkillNames?: readonly string[];
78
+ originalSerializedText?: string;
79
+ renderedSerializedText?: string;
80
+ /** True iff base rendering preserved the caller's original input identity. */
81
+ baseIdentityPreserved: boolean;
82
+ /** Base produced by applying stable rendering a second time. */
83
+ reoptimizedBaseText: string;
84
+ /** True iff the second base pass was an identity no-op. */
85
+ reoptimizedBaseIdentity: boolean;
86
+ exactTokenCounts?: ExactTokenCounts;
87
+ }
88
+
89
+ export interface SafetyCheck {
90
+ name: string;
91
+ passed: boolean;
92
+ detail?: string;
93
+ }
94
+
95
+ export interface SafetyReport {
96
+ passed: boolean;
97
+ checks: SafetyCheck[];
98
+ }
99
+
100
+ export interface ArmEvaluation {
101
+ arm: EvaluationArm;
102
+ analysis: SkillStateAnalysis;
103
+ coverage: RequiredGroupMetrics;
104
+ overlaySkillNames: string[];
105
+ prefetchedSkillNames: string[];
106
+ baseFullCount: number;
107
+ baseIntentCount: number;
108
+ baseNameOnlyCount: number;
109
+ baseMissingCount: number;
110
+ overlayCount: number;
111
+ prefetchCount: number;
112
+ bytesBefore: number;
113
+ baseBytes: number;
114
+ overlayBytes: number;
115
+ bytesAfter: number;
116
+ bytesSaved: number;
117
+ exactTokenCounts?: ExactTokenCounts & { saved: number };
118
+ safety: SafetyReport;
119
+ baseText: string;
120
+ overlayText: string;
121
+ }
122
+
123
+ export interface EvaluationCaseInput {
124
+ id: string;
125
+ /** Stable project/catalog key used for base-cache grouping. */
126
+ catalogKey?: string;
127
+ originalText: string;
128
+ requiredGroups: readonly RequiredGroup[];
129
+ originalSerializedText?: string;
130
+ arms: readonly Omit<ArmEvaluationInput, "originalText" | "requiredGroups" | "originalSerializedText">[];
131
+ }
132
+
133
+ export interface EvaluationCaseResult {
134
+ id: string;
135
+ catalogKey: string;
136
+ arms: Record<EvaluationArm, ArmEvaluation>;
137
+ }
138
+
139
+ export interface Distribution {
140
+ count: number;
141
+ mean: number | null;
142
+ median: number | null;
143
+ p95: number | null;
144
+ min: number | null;
145
+ max: number | null;
146
+ }
147
+
148
+ export interface RecallAggregate {
149
+ examples: number;
150
+ coveredGroups: number;
151
+ totalGroups: number;
152
+ macroRecall: number | null;
153
+ microRecall: number | null;
154
+ allGroupsRate: number | null;
155
+ }
156
+
157
+ export interface ExactTokenAggregate {
158
+ samples: number;
159
+ before: Distribution;
160
+ after: Distribution;
161
+ saved: Distribution;
162
+ }
163
+
164
+ export interface ArmAggregate {
165
+ arm: EvaluationArm;
166
+ samples: number;
167
+ bytesBefore: Distribution;
168
+ baseBytes: Distribution;
169
+ overlayBytes: Distribution;
170
+ bytesAfter: Distribution;
171
+ bytesSaved: Distribution;
172
+ baseFullCount: Distribution;
173
+ overlayCount: Distribution;
174
+ prefetchCount: Distribution;
175
+ exactTokensByTokenizer: Record<string, ExactTokenAggregate>;
176
+ baseFullRecall: RecallAggregate;
177
+ baseIntentRecall: RecallAggregate;
178
+ baseLoadableRecall: RecallAggregate;
179
+ overlayRecall: RecallAggregate;
180
+ prefetchRecall: RecallAggregate;
181
+ modelSelectedRecall: RecallAggregate;
182
+ }
183
+
184
+ export interface PairedArmAggregate {
185
+ from: "baseline";
186
+ to: "auto";
187
+ samples: number;
188
+ /** Positive means auto retained fewer complete eager-request UTF-8 bytes. */
189
+ bytesSavedByAuto: Distribution;
190
+ /** Positive means auto retained fewer query-independent base bytes. */
191
+ baseBytesSavedByAuto: Distribution;
192
+ /** Query-specific bytes added by auto's eager overlay. */
193
+ overlayBytesAddedByAuto: Distribution;
194
+ exactTokensSavedByAuto: Record<string, Distribution>;
195
+ }
196
+
197
+ export interface BaseCacheStability {
198
+ /** Null means fewer than two cases shared a catalog. */
199
+ passed: boolean | null;
200
+ comparedCatalogs: number;
201
+ unstableCatalogKeys: string[];
202
+ }
203
+
204
+ export interface EvaluationAggregate {
205
+ arms: Record<EvaluationArm, ArmAggregate>;
206
+ pair: PairedArmAggregate;
207
+ baseCacheStability: BaseCacheStability;
208
+ hardSafetyPassed: boolean;
209
+ safetyFailures: Array<{ caseId: string; arm: EvaluationArm; check: string; detail?: string }>;
210
+ }
211
+
212
+ interface ParsedBlock {
213
+ inner: string;
214
+ skills: Skill[];
215
+ roots: Set<string>;
216
+ resolverBacked: boolean;
217
+ }
218
+
219
+ const CATALOG_RE = /<available_skills>([\s\S]*?)<\/available_skills>/g;
220
+ const PATH_NOTE_RE = /<skill_path_note>([\s\S]*?)<\/skill_path_note>/g;
221
+
222
+ /** Independent benchmark oracle; intentionally separate from the runtime reducer. */
223
+ const EVALUATION_EVIDENCE_PATTERNS: readonly RegExp[] = [
224
+ /\b(?:error|failed|failure|fatal|panic|exception|traceback|warning|warn|denied|timed?\s*out)\b/i,
225
+ /^\s*(?:at\s+\S|Caused by:|File\s+["'][^"']+["'],\s+line\s+\d+)/i,
226
+ /(?:^|\s)(?:FAIL|FAILED|not ok)(?:\s|$|:)/i,
227
+ /\b(?:assert(?:ion)?|expected|actual|received)\b/i,
228
+ /\b(?:exit(?:ed)?\s+(?:code|status)|non-zero)\b/i,
229
+ /(?:^|\s)(?:[A-Za-z]:[\\/]|\/|\.\.\/[\w.-])?[^\s:()[\]{}]+\.[A-Za-z0-9]{1,12}:\d+(?::\d+)?\b/,
230
+ /^\s*(?:constraint|requirement|required|must(?:\s+not)?|do\s+not|never|shall(?:\s+not)?)\b/i,
231
+ /\b(?:[45]\d{2}|E\d{2,}|ENOENT|EACCES|EPERM|ECONNREFUSED|ETIMEDOUT)\b/,
232
+ ];
233
+
234
+ export function collectIndependentEvidenceLines(text: string): string[] {
235
+ return text.split(/\r?\n/).filter((line) => EVALUATION_EVIDENCE_PATTERNS.some((pattern) => pattern.test(line)));
236
+ }
237
+
238
+ /** Invalid JSON and unknown labels are errors, never empty selections. */
239
+ export function parseStrictAllowedSelection(text: string, field: string, allowed: ReadonlySet<string>): string[] {
240
+ let source = text.trim();
241
+ const fenced = source.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
242
+ if (fenced) source = fenced[1].trim();
243
+ let value: unknown;
244
+ try {
245
+ value = JSON.parse(source);
246
+ } catch (error) {
247
+ throw new Error(`judge response is not strict JSON: ${error instanceof Error ? error.message : String(error)}`);
248
+ }
249
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("judge response must be a JSON object");
250
+ const entries = (value as Record<string, unknown>)[field];
251
+ if (!Array.isArray(entries)) throw new Error(`judge response field ${field} must be an array`);
252
+ const selected: string[] = [];
253
+ const seen = new Set<string>();
254
+ for (const entry of entries) {
255
+ if (typeof entry !== "string") throw new Error(`judge response field ${field} must contain only strings`);
256
+ if (!allowed.has(entry)) throw new Error(`judge response contains unknown ${field} value: ${entry}`);
257
+ if (!seen.has(entry)) {
258
+ seen.add(entry);
259
+ selected.push(entry);
260
+ }
261
+ }
262
+ return selected;
263
+ }
264
+
265
+ export function descriptionIsExtractedVerbatim(original: string, rendered: string): boolean {
266
+ if (!rendered.trim()) return true;
267
+ const sourceWords = Array.from(original.matchAll(/[\p{L}\p{N}]+/gu), (match) => match[0]);
268
+ const renderedWords = Array.from(rendered.matchAll(/[\p{L}\p{N}]+/gu), (match) => {
269
+ const end = (match.index ?? 0) + match[0].length;
270
+ return { value: match[0], truncated: /^[^\p{L}\p{N}]*…/u.test(rendered.slice(end)) };
271
+ });
272
+ if (renderedWords.length === 0) return original.trim().length > 0 && /^…+$/u.test(rendered.trim());
273
+ let sourceIndex = 0;
274
+ for (const word of renderedWords) {
275
+ while (sourceIndex < sourceWords.length) {
276
+ const sourceWord = sourceWords[sourceIndex];
277
+ if (sourceWord === word.value || (word.truncated && sourceWord.startsWith(word.value))) break;
278
+ sourceIndex += 1;
279
+ }
280
+ if (sourceIndex === sourceWords.length) return false;
281
+ sourceIndex += 1;
282
+ }
283
+ return true;
284
+ }
285
+
286
+ function pathRoots(inner: string): Set<string> {
287
+ const roots = new Set<string>();
288
+ for (const note of inner.matchAll(PATH_NOTE_RE)) {
289
+ const match = note[1].match(/\(roots:\s*([\s\S]*?)\)\.\s*Read/i);
290
+ if (!match) continue;
291
+ for (const root of match[1].split("|").map((value) => value.trim()).filter(Boolean)) roots.add(root);
292
+ }
293
+ return roots;
294
+ }
295
+
296
+ function parseCatalogBlocks(text: string): ParsedBlock[] {
297
+ return Array.from(text.matchAll(CATALOG_RE), (match) => ({
298
+ inner: match[1],
299
+ skills: parseSkills(match[1]),
300
+ roots: pathRoots(match[1]),
301
+ resolverBacked: match[1].includes("<!--skill-optimizer:auto:v2-->"),
302
+ }));
303
+ }
304
+
305
+ function parseOverlaySkills(text: string): Skill[] {
306
+ if (!text.trim()) return [];
307
+ const blocks = parseCatalogBlocks(text);
308
+ return blocks.length > 0 ? blocks.flatMap((block) => block.skills) : parseSkills(text);
309
+ }
310
+
311
+ function derivableRoot(location: string, name: string): string | null {
312
+ const match = location.match(/^(.*)[\\/]([^\\/]+)[\\/]SKILL\.md$/i);
313
+ return match && match[2] === name ? match[1] : null;
314
+ }
315
+
316
+ function multisetEqual(left: readonly string[], right: readonly string[]): boolean {
317
+ if (left.length !== right.length) return false;
318
+ const counts = new Map<string, number>();
319
+ for (const value of left) counts.set(value, (counts.get(value) ?? 0) + 1);
320
+ for (const value of right) {
321
+ const count = counts.get(value) ?? 0;
322
+ if (count === 0) return false;
323
+ if (count === 1) counts.delete(value);
324
+ else counts.set(value, count - 1);
325
+ }
326
+ return counts.size === 0;
327
+ }
328
+
329
+ /** Derive every original skill's exposure and loadability in the stable base. */
330
+ export function deriveSkillStates(originalText: string, baseText: string): SkillStateAnalysis {
331
+ const originalBlocks = parseCatalogBlocks(originalText);
332
+ const renderedBlocks = parseCatalogBlocks(baseText);
333
+ const states: SkillRenderState[] = [];
334
+ const unexpectedNames: string[] = [];
335
+ const originalNames = originalBlocks.flatMap((block) => block.skills.map((skill) => skill.name));
336
+ const renderedNames = renderedBlocks.flatMap((block) => block.skills.map((skill) => skill.name));
337
+
338
+ for (let blockIndex = 0; blockIndex < originalBlocks.length; blockIndex++) {
339
+ const originalBlock = originalBlocks[blockIndex];
340
+ const renderedBlock = renderedBlocks[blockIndex];
341
+ const queues = new Map<string, Array<{ skill: Skill; index: number }>>();
342
+ for (const [index, skill] of (renderedBlock?.skills ?? []).entries()) {
343
+ const queue = queues.get(skill.name) ?? [];
344
+ queue.push({ skill, index });
345
+ queues.set(skill.name, queue);
346
+ }
347
+ const usedRendered = new Set<number>();
348
+ const occurrences = new Map<string, number>();
349
+ for (const original of originalBlock.skills) {
350
+ const occurrence = occurrences.get(original.name) ?? 0;
351
+ occurrences.set(original.name, occurrence + 1);
352
+ const matched = queues.get(original.name)?.shift();
353
+ if (matched) usedRendered.add(matched.index);
354
+ const rendered = matched?.skill;
355
+ const descriptionVerbatim = rendered !== undefined && rendered.description === original.description;
356
+ const descriptionExtractedVerbatim = rendered !== undefined && descriptionIsExtractedVerbatim(original.description, rendered.description);
357
+ const locationVerbatim = rendered !== undefined && rendered.location === original.location;
358
+ let state: SkillExposure;
359
+ if (!rendered) state = "missing";
360
+ else if (descriptionVerbatim && locationVerbatim) state = "full";
361
+ else if (!rendered.description) state = "name-only";
362
+ else state = "intent";
363
+
364
+ const root = derivableRoot(original.location, original.name);
365
+ const hasExplicitLocation = !!rendered?.location;
366
+ const explicitLocation = hasExplicitLocation && rendered.location === original.location;
367
+ const locationFromNote = !hasExplicitLocation && root !== null && (renderedBlock?.roots.has(root) ?? false);
368
+ const locationFromResolver = !!rendered && (renderedBlock?.resolverBacked ?? false);
369
+ const loadable = !!rendered && (explicitLocation || locationFromNote || locationFromResolver);
370
+ states.push({
371
+ key: `${blockIndex}:${original.name}:${occurrence}`,
372
+ blockIndex,
373
+ occurrence,
374
+ name: original.name,
375
+ state,
376
+ loadable,
377
+ descriptionVerbatim,
378
+ descriptionExtractedVerbatim,
379
+ locationVerbatim,
380
+ original,
381
+ ...(rendered ? { rendered } : {}),
382
+ });
383
+ }
384
+ for (const [index, skill] of (renderedBlock?.skills ?? []).entries()) {
385
+ if (!usedRendered.has(index)) unexpectedNames.push(skill.name);
386
+ }
387
+ }
388
+ for (let i = originalBlocks.length; i < renderedBlocks.length; i++) {
389
+ unexpectedNames.push(...renderedBlocks[i].skills.map((skill) => skill.name));
390
+ }
391
+
392
+ return {
393
+ states,
394
+ originalNames,
395
+ renderedNames,
396
+ unexpectedNames,
397
+ namesPreserved: multisetEqual(originalNames, renderedNames),
398
+ orderPreserved: originalNames.length === renderedNames.length && originalNames.every((name, i) => renderedNames[i] === name),
399
+ allRetainedLoadable: states.filter((state) => state.state !== "missing").every((state) => state.loadable),
400
+ };
401
+ }
402
+
403
+ function coverageMetric(groups: readonly RequiredGroup[], covered: (name: string) => boolean): CoverageMetric {
404
+ let count = 0;
405
+ for (const group of groups) if (group.anyOf.some(covered)) count += 1;
406
+ return { covered: count, total: groups.length, recall: groups.length === 0 ? null : count / groups.length };
407
+ }
408
+
409
+ export function evaluateRequiredGroups(
410
+ states: readonly SkillRenderState[],
411
+ groups: readonly RequiredGroup[],
412
+ overlaySkillNames: readonly string[],
413
+ prefetchedSkillNames: readonly string[],
414
+ modelSelectedSkillNames?: readonly string[],
415
+ ): RequiredGroupMetrics {
416
+ const byName = new Map<string, SkillRenderState[]>();
417
+ for (const state of states) {
418
+ const entries = byName.get(state.name) ?? [];
419
+ entries.push(state);
420
+ byName.set(state.name, entries);
421
+ }
422
+ const has = (name: string, predicate: (state: SkillRenderState) => boolean): boolean => (byName.get(name) ?? []).some(predicate);
423
+ const overlaySet = new Set(overlaySkillNames);
424
+ const prefetchSet = new Set(prefetchedSkillNames);
425
+ const modelSet = modelSelectedSkillNames === undefined ? undefined : new Set(modelSelectedSkillNames);
426
+ const baseFull = coverageMetric(groups, (name) => has(name, (state) => state.state === "full"));
427
+ const baseIntent = coverageMetric(groups, (name) => has(name, (state) => state.state === "full" || state.state === "intent"));
428
+ const baseLoadable = coverageMetric(groups, (name) => has(name, (state) => state.loadable));
429
+ const overlay = coverageMetric(groups, (name) => overlaySet.has(name));
430
+ const prefetch = coverageMetric(groups, (name) => prefetchSet.has(name));
431
+ const modelSelected = modelSet === undefined ? null : coverageMetric(groups, (name) => modelSet.has(name));
432
+ const all = (metric: CoverageMetric): boolean | null => metric.total === 0 ? null : metric.covered === metric.total;
433
+ return {
434
+ baseFull,
435
+ baseIntent,
436
+ baseLoadable,
437
+ overlay,
438
+ prefetch,
439
+ modelSelected,
440
+ allGroupsBaseFull: all(baseFull),
441
+ allGroupsBaseIntent: all(baseIntent),
442
+ allGroupsBaseLoadable: all(baseLoadable),
443
+ allGroupsOverlay: all(overlay),
444
+ allGroupsPrefetched: all(prefetch),
445
+ allGroupsModelSelected: modelSelected ? all(modelSelected) : null,
446
+ };
447
+ }
448
+
449
+ function utf8Bytes(text: string): number {
450
+ return new TextEncoder().encode(text).byteLength;
451
+ }
452
+
453
+ function validateExactTokens(value: ExactTokenCounts | undefined): (ExactTokenCounts & { saved: number }) | undefined {
454
+ if (!value) return undefined;
455
+ if (!value.tokenizer.trim()) throw new Error("exactTokenCounts.tokenizer must be non-empty");
456
+ if (![value.before, value.after].every((count) => Number.isInteger(count) && count >= 0)) {
457
+ throw new Error("exactTokenCounts must contain non-negative integer counts");
458
+ }
459
+ return { ...value, saved: value.before - value.after };
460
+ }
461
+
462
+ function combinedText(baseText: string, overlayText: string): string {
463
+ return overlayText.trim() ? `${baseText}\n\n${overlayText}` : baseText;
464
+ }
465
+
466
+ function overlayIsVerbatim(originalText: string, overlayText: string, overlaySkills: readonly Skill[]): boolean {
467
+ const originals = parseCatalogBlocks(originalText).flatMap((block) => block.skills);
468
+ const resolverBacked = overlayText.includes("<skill_prefetch");
469
+ return overlaySkills.every((rendered) => originals.some((original) =>
470
+ original.name === rendered.name
471
+ && original.description === rendered.description
472
+ && (original.location === rendered.location || (resolverBacked && rendered.location === ""))));
473
+ }
474
+
475
+ /** Evaluate one artifact while keeping stable-base and eager-overlay metrics separate. */
476
+ export function evaluateArm(input: ArmEvaluationInput): ArmEvaluation {
477
+ if ((input.originalSerializedText === undefined) !== (input.renderedSerializedText === undefined)) {
478
+ throw new Error("originalSerializedText and renderedSerializedText must be provided together");
479
+ }
480
+ const analysis = deriveSkillStates(input.originalText, input.baseText);
481
+ const overlaySkills = parseOverlaySkills(input.overlayText);
482
+ const overlaySkillNames = overlaySkills.map((skill) => skill.name);
483
+ const prefetchedSkillNames = [...input.prefetchedSkillNames];
484
+ const coverage = evaluateRequiredGroups(
485
+ analysis.states,
486
+ input.requiredGroups,
487
+ overlaySkillNames,
488
+ prefetchedSkillNames,
489
+ input.modelSelectedSkillNames,
490
+ );
491
+ const stateCount = (state: SkillExposure): number => analysis.states.filter((entry) => entry.state === state).length;
492
+ const descriptionsExtractive = analysis.states
493
+ .filter((state) => state.state !== "missing")
494
+ .every((state) => state.descriptionExtractedVerbatim);
495
+ const overlayVerbatim = overlayIsVerbatim(input.originalText, input.overlayText, overlaySkills);
496
+ const overlayPrefetchIntegrity = multisetEqual(overlaySkillNames, prefetchedSkillNames);
497
+ const checks: SafetyCheck[] = [
498
+ { name: "base-name-preservation", passed: analysis.namesPreserved, detail: analysis.namesPreserved ? undefined : "base names differ from the original multiset" },
499
+ { name: "base-order-preservation", passed: analysis.orderPreserved, detail: analysis.orderPreserved ? undefined : "base skill order changed" },
500
+ { name: "base-description-extractive", passed: descriptionsExtractive, detail: descriptionsExtractive ? undefined : "base description contains non-extractive text" },
501
+ { name: "base-loadability", passed: analysis.namesPreserved && analysis.allRetainedLoadable, detail: analysis.namesPreserved && analysis.allRetainedLoadable ? undefined : "a base skill is missing or not loadable" },
502
+ { name: "overlay-verbatim", passed: overlayVerbatim, detail: overlayVerbatim ? undefined : "overlay contains an unknown or rewritten skill" },
503
+ { name: "overlay-prefetch-integrity", passed: overlayPrefetchIntegrity, detail: overlayPrefetchIntegrity ? undefined : "overlay names differ from the prefetch plan" },
504
+ {
505
+ name: "base-idempotence",
506
+ passed: input.reoptimizedBaseText === input.baseText && input.reoptimizedBaseIdentity,
507
+ detail: input.reoptimizedBaseText === input.baseText && input.reoptimizedBaseIdentity ? undefined : "second stable-base pass changed text or identity",
508
+ },
509
+ ];
510
+ if (input.arm === "baseline") {
511
+ const passed = input.baseText === input.originalText
512
+ && input.baseIdentityPreserved
513
+ && input.overlayText.trim().length === 0
514
+ && prefetchedSkillNames.length === 0;
515
+ checks.push({ name: "baseline-identity", passed, detail: passed ? undefined : "baseline changed the catalog or added prefetch content" });
516
+ }
517
+ const beforeText = input.originalSerializedText ?? input.originalText;
518
+ const afterText = input.renderedSerializedText ?? combinedText(input.baseText, input.overlayText);
519
+ const bytesBefore = utf8Bytes(beforeText);
520
+ const baseBytes = utf8Bytes(input.baseText);
521
+ const overlayBytes = utf8Bytes(input.overlayText);
522
+ const bytesAfter = utf8Bytes(afterText);
523
+ return {
524
+ arm: input.arm,
525
+ analysis,
526
+ coverage,
527
+ overlaySkillNames,
528
+ prefetchedSkillNames,
529
+ baseFullCount: stateCount("full"),
530
+ baseIntentCount: stateCount("intent"),
531
+ baseNameOnlyCount: stateCount("name-only"),
532
+ baseMissingCount: stateCount("missing"),
533
+ overlayCount: overlaySkillNames.length,
534
+ prefetchCount: prefetchedSkillNames.length,
535
+ bytesBefore,
536
+ baseBytes,
537
+ overlayBytes,
538
+ bytesAfter,
539
+ bytesSaved: bytesBefore - bytesAfter,
540
+ ...(input.exactTokenCounts ? { exactTokenCounts: validateExactTokens(input.exactTokenCounts) } : {}),
541
+ safety: { passed: checks.every((check) => check.passed), checks },
542
+ baseText: input.baseText,
543
+ overlayText: input.overlayText,
544
+ };
545
+ }
546
+
547
+ /** Evaluate exactly one baseline artifact and one auto artifact. */
548
+ export function evaluateCase(input: EvaluationCaseInput): EvaluationCaseResult {
549
+ const arms = new Map(input.arms.map((arm) => [arm.arm, arm]));
550
+ for (const arm of ["baseline", "auto"] as const) {
551
+ if (!arms.has(arm)) throw new Error(`evaluation case ${input.id} is missing arm ${arm}`);
552
+ }
553
+ const evaluate = (arm: EvaluationArm): ArmEvaluation => evaluateArm({
554
+ ...arms.get(arm)!,
555
+ arm,
556
+ originalText: input.originalText,
557
+ requiredGroups: input.requiredGroups,
558
+ ...(input.originalSerializedText === undefined ? {} : { originalSerializedText: input.originalSerializedText }),
559
+ });
560
+ return {
561
+ id: input.id,
562
+ catalogKey: input.catalogKey ?? input.originalText,
563
+ arms: { baseline: evaluate("baseline"), auto: evaluate("auto") },
564
+ };
565
+ }
566
+
567
+ /** Deterministic nearest-rank distributions used by corpus reports. */
568
+ export function distribution(values: readonly number[]): Distribution {
569
+ if (values.length === 0) return { count: 0, mean: null, median: null, p95: null, min: null, max: null };
570
+ const sorted = [...values].sort((a, b) => a - b);
571
+ const mean = sorted.reduce((sum, value) => sum + value, 0) / sorted.length;
572
+ const middle = Math.floor(sorted.length / 2);
573
+ const median = sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
574
+ const p95 = sorted[Math.max(0, Math.ceil(sorted.length * 0.95) - 1)];
575
+ return { count: sorted.length, mean, median, p95, min: sorted[0], max: sorted[sorted.length - 1] };
576
+ }
577
+
578
+ function aggregateRecall(
579
+ evaluations: readonly ArmEvaluation[],
580
+ metric: (coverage: RequiredGroupMetrics) => CoverageMetric | null,
581
+ ): RecallAggregate {
582
+ const values = evaluations.map((evaluation) => metric(evaluation.coverage)).filter((value): value is CoverageMetric => value !== null && value.total > 0);
583
+ const coveredGroups = values.reduce((sum, value) => sum + value.covered, 0);
584
+ const totalGroups = values.reduce((sum, value) => sum + value.total, 0);
585
+ return {
586
+ examples: values.length,
587
+ coveredGroups,
588
+ totalGroups,
589
+ macroRecall: values.length === 0 ? null : values.reduce((sum, value) => sum + (value.recall ?? 0), 0) / values.length,
590
+ microRecall: totalGroups === 0 ? null : coveredGroups / totalGroups,
591
+ allGroupsRate: values.length === 0 ? null : values.filter((value) => value.covered === value.total).length / values.length,
592
+ };
593
+ }
594
+
595
+ function aggregateArm(arm: EvaluationArm, evaluations: readonly ArmEvaluation[]): ArmAggregate {
596
+ const tokenGroups = new Map<string, Array<ExactTokenCounts & { saved: number }>>();
597
+ for (const evaluation of evaluations) {
598
+ const tokens = evaluation.exactTokenCounts;
599
+ if (!tokens) continue;
600
+ const group = tokenGroups.get(tokens.tokenizer) ?? [];
601
+ group.push(tokens);
602
+ tokenGroups.set(tokens.tokenizer, group);
603
+ }
604
+ const exactTokensByTokenizer: Record<string, ExactTokenAggregate> = {};
605
+ for (const [tokenizer, values] of tokenGroups) {
606
+ exactTokensByTokenizer[tokenizer] = {
607
+ samples: values.length,
608
+ before: distribution(values.map((value) => value.before)),
609
+ after: distribution(values.map((value) => value.after)),
610
+ saved: distribution(values.map((value) => value.saved)),
611
+ };
612
+ }
613
+ return {
614
+ arm,
615
+ samples: evaluations.length,
616
+ bytesBefore: distribution(evaluations.map((value) => value.bytesBefore)),
617
+ baseBytes: distribution(evaluations.map((value) => value.baseBytes)),
618
+ overlayBytes: distribution(evaluations.map((value) => value.overlayBytes)),
619
+ bytesAfter: distribution(evaluations.map((value) => value.bytesAfter)),
620
+ bytesSaved: distribution(evaluations.map((value) => value.bytesSaved)),
621
+ baseFullCount: distribution(evaluations.map((value) => value.baseFullCount)),
622
+ overlayCount: distribution(evaluations.map((value) => value.overlayCount)),
623
+ prefetchCount: distribution(evaluations.map((value) => value.prefetchCount)),
624
+ exactTokensByTokenizer,
625
+ baseFullRecall: aggregateRecall(evaluations, (coverage) => coverage.baseFull),
626
+ baseIntentRecall: aggregateRecall(evaluations, (coverage) => coverage.baseIntent),
627
+ baseLoadableRecall: aggregateRecall(evaluations, (coverage) => coverage.baseLoadable),
628
+ overlayRecall: aggregateRecall(evaluations, (coverage) => coverage.overlay),
629
+ prefetchRecall: aggregateRecall(evaluations, (coverage) => coverage.prefetch),
630
+ modelSelectedRecall: aggregateRecall(evaluations, (coverage) => coverage.modelSelected),
631
+ };
632
+ }
633
+
634
+ function aggregatePair(cases: readonly EvaluationCaseResult[]): PairedArmAggregate {
635
+ const tokenDeltas = new Map<string, number[]>();
636
+ for (const entry of cases) {
637
+ const baseline = entry.arms.baseline.exactTokenCounts;
638
+ const auto = entry.arms.auto.exactTokenCounts;
639
+ if (!baseline || !auto || baseline.tokenizer !== auto.tokenizer) continue;
640
+ const values = tokenDeltas.get(baseline.tokenizer) ?? [];
641
+ values.push(baseline.after - auto.after);
642
+ tokenDeltas.set(baseline.tokenizer, values);
643
+ }
644
+ return {
645
+ from: "baseline",
646
+ to: "auto",
647
+ samples: cases.length,
648
+ bytesSavedByAuto: distribution(cases.map((entry) => entry.arms.baseline.bytesAfter - entry.arms.auto.bytesAfter)),
649
+ baseBytesSavedByAuto: distribution(cases.map((entry) => entry.arms.baseline.baseBytes - entry.arms.auto.baseBytes)),
650
+ overlayBytesAddedByAuto: distribution(cases.map((entry) => entry.arms.auto.overlayBytes - entry.arms.baseline.overlayBytes)),
651
+ exactTokensSavedByAuto: Object.fromEntries([...tokenDeltas].map(([tokenizer, values]) => [tokenizer, distribution(values)])),
652
+ };
653
+ }
654
+
655
+ /** Aggregate paired cases without mixing catalogs or tokenizer families. */
656
+ export function aggregateEvaluationCases(cases: readonly EvaluationCaseResult[]): EvaluationAggregate {
657
+ const autoBasesByCatalog = new Map<string, Set<string>>();
658
+ const catalogCounts = new Map<string, number>();
659
+ for (const entry of cases) {
660
+ const outputs = autoBasesByCatalog.get(entry.catalogKey) ?? new Set<string>();
661
+ outputs.add(entry.arms.auto.baseText);
662
+ autoBasesByCatalog.set(entry.catalogKey, outputs);
663
+ catalogCounts.set(entry.catalogKey, (catalogCounts.get(entry.catalogKey) ?? 0) + 1);
664
+ }
665
+ const comparedCatalogs = [...catalogCounts.values()].filter((count) => count > 1).length;
666
+ const unstableCatalogKeys = [...autoBasesByCatalog]
667
+ .filter(([key, outputs]) => (catalogCounts.get(key) ?? 0) > 1 && outputs.size > 1)
668
+ .map(([key]) => key);
669
+ const safetyFailures: EvaluationAggregate["safetyFailures"] = [];
670
+ for (const entry of cases) {
671
+ for (const arm of ["baseline", "auto"] as const) {
672
+ for (const check of entry.arms[arm].safety.checks) {
673
+ if (!check.passed) safetyFailures.push({ caseId: entry.id, arm, check: check.name, ...(check.detail ? { detail: check.detail } : {}) });
674
+ }
675
+ }
676
+ }
677
+ const baseCacheStability: BaseCacheStability = {
678
+ passed: comparedCatalogs === 0 ? null : unstableCatalogKeys.length === 0,
679
+ comparedCatalogs,
680
+ unstableCatalogKeys,
681
+ };
682
+ return {
683
+ arms: {
684
+ baseline: aggregateArm("baseline", cases.map((entry) => entry.arms.baseline)),
685
+ auto: aggregateArm("auto", cases.map((entry) => entry.arms.auto)),
686
+ },
687
+ pair: aggregatePair(cases),
688
+ baseCacheStability,
689
+ hardSafetyPassed: safetyFailures.length === 0 && baseCacheStability.passed !== false,
690
+ safetyFailures,
691
+ };
692
+ }