@namzu/sdk 62.14.0 → 62.15.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.
- package/CHANGELOG.md +13 -0
- package/dist/connector/mcp/stdio.d.ts.map +1 -1
- package/dist/connector/mcp/stdio.js +3 -0
- package/dist/connector/mcp/stdio.js.map +1 -1
- package/dist/eval/compare.d.ts +54 -0
- package/dist/eval/compare.d.ts.map +1 -1
- package/dist/eval/compare.js +146 -1
- package/dist/eval/compare.js.map +1 -1
- package/dist/eval/experiment.d.ts.map +1 -1
- package/dist/eval/experiment.js +46 -6
- package/dist/eval/experiment.js.map +1 -1
- package/dist/eval/index.d.ts +6 -2
- package/dist/eval/index.d.ts.map +1 -1
- package/dist/eval/index.js +3 -1
- package/dist/eval/index.js.map +1 -1
- package/dist/eval/report-stats.d.ts +6 -0
- package/dist/eval/report-stats.d.ts.map +1 -1
- package/dist/eval/report-stats.js +10 -0
- package/dist/eval/report-stats.js.map +1 -1
- package/dist/eval/run-options.d.ts +25 -0
- package/dist/eval/run-options.d.ts.map +1 -1
- package/dist/eval/run-options.js +36 -0
- package/dist/eval/run-options.js.map +1 -1
- package/dist/eval/run-summary.d.ts +8 -0
- package/dist/eval/run-summary.d.ts.map +1 -1
- package/dist/eval/run-summary.js +13 -3
- package/dist/eval/run-summary.js.map +1 -1
- package/dist/eval/settings.d.ts +74 -0
- package/dist/eval/settings.d.ts.map +1 -0
- package/dist/eval/settings.js +161 -0
- package/dist/eval/settings.js.map +1 -0
- package/dist/eval/split.d.ts +48 -0
- package/dist/eval/split.d.ts.map +1 -0
- package/dist/eval/split.js +41 -0
- package/dist/eval/split.js.map +1 -0
- package/dist/eval/types.d.ts +12 -0
- package/dist/eval/types.d.ts.map +1 -1
- package/dist/execution/local.d.ts.map +1 -1
- package/dist/execution/local.js +2 -1
- package/dist/execution/local.js.map +1 -1
- package/dist/peers/envelope.d.ts +11 -0
- package/dist/peers/envelope.d.ts.map +1 -1
- package/dist/peers/envelope.js +29 -0
- package/dist/peers/envelope.js.map +1 -1
- package/dist/peers/index.d.ts +3 -3
- package/dist/peers/index.d.ts.map +1 -1
- package/dist/peers/index.js +2 -2
- package/dist/peers/index.js.map +1 -1
- package/dist/peers/protocol.d.ts +26 -0
- package/dist/peers/protocol.d.ts.map +1 -1
- package/dist/peers/protocol.js +22 -2
- package/dist/peers/protocol.js.map +1 -1
- package/dist/peers/record.d.ts +2 -0
- package/dist/peers/record.d.ts.map +1 -1
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +2 -1
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/process/session-child.d.ts +21 -0
- package/dist/process/session-child.d.ts.map +1 -0
- package/dist/process/session-child.js +32 -0
- package/dist/process/session-child.js.map +1 -0
- package/dist/public-runtime.d.ts +3 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +3 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +1 -1
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/system-events.d.ts +3 -3
- package/dist/runtime/system-events.d.ts.map +1 -1
- package/dist/runtime/system-events.js +2 -1
- package/dist/runtime/system-events.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts +1 -0
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +5 -2
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/sandbox/terminal.d.ts.map +1 -1
- package/dist/sandbox/terminal.js +2 -1
- package/dist/sandbox/terminal.js.map +1 -1
- package/dist/tools/command-shell.d.ts.map +1 -1
- package/dist/tools/command-shell.js +7 -1
- package/dist/tools/command-shell.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +1 -0
- package/dist/types/message/index.js.map +1 -1
- package/package.json +1 -1
- package/src/connector/mcp/stdio.ts +3 -0
- package/src/eval/compare.ts +245 -2
- package/src/eval/experiment.ts +58 -4
- package/src/eval/index.ts +32 -1
- package/src/eval/report-stats.ts +11 -0
- package/src/eval/run-options.ts +51 -0
- package/src/eval/run-summary.ts +19 -1
- package/src/eval/settings.ts +225 -0
- package/src/eval/split.ts +83 -0
- package/src/eval/types.ts +12 -0
- package/src/execution/local.ts +2 -1
- package/src/peers/envelope.ts +36 -0
- package/src/peers/index.ts +3 -1
- package/src/peers/protocol.ts +24 -2
- package/src/plugin/shell-hook.ts +2 -1
- package/src/process/session-child.ts +38 -0
- package/src/public-runtime.ts +21 -0
- package/src/public-types.ts +1 -0
- package/src/runtime/system-events.ts +2 -1
- package/src/sandbox/provider/local.ts +5 -1
- package/src/sandbox/terminal.ts +3 -1
- package/src/tools/command-shell.ts +7 -1
- package/src/types/message/index.ts +1 -0
package/src/eval/compare.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
} from './bootstrap.js'
|
|
11
11
|
import { UNKNOWN } from './disclosure.js'
|
|
12
12
|
import { type CaseRuns, duplicateRun, groupRuns, passedEveryRun, tallyOf } from './report-stats.js'
|
|
13
|
+
import { type SettingDifference, diffSettings } from './settings.js'
|
|
14
|
+
import { SPLIT_FILTERS, type SplitFilter } from './split.js'
|
|
13
15
|
import type { CaseResult } from './types.js'
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -31,6 +33,41 @@ export interface LoadedSuite {
|
|
|
31
33
|
readonly repeats: number
|
|
32
34
|
/** Whatever the file said about how the suite ran; absent in an older file. */
|
|
33
35
|
readonly facts: Readonly<Record<string, unknown>> | undefined
|
|
36
|
+
/** The settings snapshot the suite ran under; absent in a report written before there were any. */
|
|
37
|
+
readonly settings: LoadedSettings | undefined
|
|
38
|
+
/** Which split the suite ran; absent in a report written before there were any. */
|
|
39
|
+
readonly split: SplitFilter | undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LoadedSettings {
|
|
43
|
+
readonly name: string
|
|
44
|
+
readonly digest: string
|
|
45
|
+
readonly values: Readonly<Record<string, unknown>>
|
|
46
|
+
/** False when the suite never asked for the settings. Absent is read as unknown. */
|
|
47
|
+
readonly read: boolean | undefined
|
|
48
|
+
/** Paths the suite asked for, `*` for the whole object; undefined when the report does not say. */
|
|
49
|
+
readonly readKeys: readonly string[] | undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadSettings(value: unknown): LoadedSettings | undefined {
|
|
53
|
+
if (!isRecord(value)) return undefined
|
|
54
|
+
if (typeof value.name !== 'string' || typeof value.digest !== 'string') return undefined
|
|
55
|
+
return {
|
|
56
|
+
name: value.name,
|
|
57
|
+
digest: value.digest,
|
|
58
|
+
values: isRecord(value.values) ? value.values : {},
|
|
59
|
+
read: typeof value.read === 'boolean' ? value.read : undefined,
|
|
60
|
+
readKeys: Array.isArray(value.readKeys)
|
|
61
|
+
? value.readKeys.filter((k): k is string => typeof k === 'string')
|
|
62
|
+
: undefined,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function loadSplit(value: unknown): SplitFilter | undefined {
|
|
67
|
+
if (!isRecord(value)) return undefined
|
|
68
|
+
return SPLIT_FILTERS.includes(value.requested as SplitFilter)
|
|
69
|
+
? (value.requested as SplitFilter)
|
|
70
|
+
: undefined
|
|
34
71
|
}
|
|
35
72
|
|
|
36
73
|
export interface LoadedReportFile {
|
|
@@ -81,6 +118,8 @@ export function loadEvalReportFile(json: unknown, label: string): LoadedReportFi
|
|
|
81
118
|
cases: groupRuns(runs),
|
|
82
119
|
repeats: typeof report.repeats === 'number' ? report.repeats : 1,
|
|
83
120
|
facts: isRecord(report.disclosure) ? report.disclosure : undefined,
|
|
121
|
+
settings: loadSettings(report.settings),
|
|
122
|
+
split: loadSplit(report.taskSplit),
|
|
84
123
|
})
|
|
85
124
|
}
|
|
86
125
|
return { label, suites }
|
|
@@ -277,10 +316,53 @@ export interface EvalComparison {
|
|
|
277
316
|
readonly sentence: string
|
|
278
317
|
/** Why, in plain words. */
|
|
279
318
|
readonly detail: string
|
|
319
|
+
/**
|
|
320
|
+
* Why no winner is named although the numbers might say one. Empty when
|
|
321
|
+
* the comparison is on the held-out split, between different settings, over
|
|
322
|
+
* the same tasks: the one case in which it speaks.
|
|
323
|
+
*/
|
|
324
|
+
readonly withheld: readonly WithheldReason[]
|
|
280
325
|
}
|
|
281
326
|
readonly disclosure: DisclosureComparison
|
|
327
|
+
/** The settings each report ran under, and what differs between the two. */
|
|
328
|
+
readonly settings: SettingsComparison
|
|
329
|
+
/** The split each report ran. Null when a report does not say (an older one). */
|
|
330
|
+
readonly split: { readonly a: SideSplit; readonly b: SideSplit }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** `mixed` when the suites of one report ran different splits. */
|
|
334
|
+
export type SideSplit = SplitFilter | 'mixed' | null
|
|
335
|
+
|
|
336
|
+
export interface SnapshotRef {
|
|
337
|
+
readonly name: string
|
|
338
|
+
readonly digest: string
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export interface SettingsComparison {
|
|
342
|
+
/** Null when the report has no snapshot (it was written before reports carried one). */
|
|
343
|
+
readonly a: SnapshotRef | null
|
|
344
|
+
readonly b: SnapshotRef | null
|
|
345
|
+
/** Null when either side has no snapshot. */
|
|
346
|
+
readonly identical: boolean | null
|
|
347
|
+
/** Key by key; empty when the snapshots are identical or either is missing. */
|
|
348
|
+
readonly differences: readonly SettingDifference[]
|
|
349
|
+
/** Suites that ran under settings but never read them, so the settings cannot have moved them. */
|
|
350
|
+
readonly notRead: { readonly a: readonly string[]; readonly b: readonly string[] }
|
|
351
|
+
/** Settings that differ between the two snapshots and that no shared suite on either side read. */
|
|
352
|
+
readonly unreadKeys: readonly string[]
|
|
353
|
+
/** Whether at least one differing setting was read by a shared suite. Null when there is no difference to read. */
|
|
354
|
+
readonly anyDifferenceRead: boolean | null
|
|
282
355
|
}
|
|
283
356
|
|
|
357
|
+
/** Why a comparison does not name a winner, besides the numbers not supporting one. */
|
|
358
|
+
export type WithheldReason =
|
|
359
|
+
| 'tune-split'
|
|
360
|
+
| 'identical-settings'
|
|
361
|
+
| 'no-snapshot'
|
|
362
|
+
| 'tasks-differ'
|
|
363
|
+
| 'not-held-out'
|
|
364
|
+
| 'settings-not-read'
|
|
365
|
+
|
|
284
366
|
function sorted(refs: CaseRef[]): CaseRef[] {
|
|
285
367
|
return refs.sort((x, y) =>
|
|
286
368
|
x.suite === y.suite ? (x.case < y.case ? -1 : 1) : x.suite < y.suite ? -1 : 1,
|
|
@@ -358,7 +440,13 @@ export function compareEvalReports(
|
|
|
358
440
|
pValue: exactSignTestP(onlyBPasses, onlyAPasses),
|
|
359
441
|
test: 'exact two-sided sign test on the cases that flipped',
|
|
360
442
|
}
|
|
361
|
-
const
|
|
443
|
+
const settings = compareSettings(a, b, sharedSuiteIds(a, b))
|
|
444
|
+
const split = { a: splitOf(a), b: splitOf(b) }
|
|
445
|
+
const withheld = withheldReasons(settings, split, onlyInA.length + onlyInB.length)
|
|
446
|
+
const verdict =
|
|
447
|
+
withheld.length > 0
|
|
448
|
+
? withhold(withheld, settings, onlyInA.length, onlyInB.length, difference.mean, pairs.length)
|
|
449
|
+
: { ...decide(difference.mean, pairs.length, flips), withheld: [] as WithheldReason[] }
|
|
362
450
|
const sharedSuites = a.suites.map((s) => s.id).filter((id) => b.suites.some((s) => s.id === id))
|
|
363
451
|
|
|
364
452
|
return {
|
|
@@ -375,6 +463,130 @@ export function compareEvalReports(
|
|
|
375
463
|
flips,
|
|
376
464
|
verdict,
|
|
377
465
|
disclosure: compareDisclosure(a, b, sharedSuites),
|
|
466
|
+
settings,
|
|
467
|
+
split,
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function sharedSuiteIds(a: LoadedReportFile, b: LoadedReportFile): string[] {
|
|
472
|
+
return a.suites.map((s) => s.id).filter((id) => b.suites.some((s) => s.id === id))
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function snapshotOf(file: LoadedReportFile): LoadedSettings | undefined {
|
|
476
|
+
return file.suites.find((s) => s.settings !== undefined)?.settings
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function splitOf(file: LoadedReportFile): SideSplit {
|
|
480
|
+
const requested = new Set(file.suites.map((s) => s.split))
|
|
481
|
+
if (requested.size === 0 || requested.has(undefined)) return null
|
|
482
|
+
return requested.size === 1 ? ([...requested][0] as SplitFilter) : 'mixed'
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function compareSettings(
|
|
486
|
+
a: LoadedReportFile,
|
|
487
|
+
b: LoadedReportFile,
|
|
488
|
+
shared: readonly string[],
|
|
489
|
+
): SettingsComparison {
|
|
490
|
+
const left = snapshotOf(a)
|
|
491
|
+
const right = snapshotOf(b)
|
|
492
|
+
const ref = (x: LoadedSettings | undefined): SnapshotRef | null =>
|
|
493
|
+
x === undefined ? null : { name: x.name, digest: x.digest }
|
|
494
|
+
const ignored = (file: LoadedReportFile) =>
|
|
495
|
+
file.suites
|
|
496
|
+
.filter(
|
|
497
|
+
(s) =>
|
|
498
|
+
shared.includes(s.id) &&
|
|
499
|
+
s.settings?.read === false &&
|
|
500
|
+
Object.keys(s.settings.values).length > 0,
|
|
501
|
+
)
|
|
502
|
+
.map((s) => s.id)
|
|
503
|
+
const identical = left && right ? left.digest === right.digest : null
|
|
504
|
+
const differences = left && right && !identical ? diffSettings(left.values, right.values) : []
|
|
505
|
+
const readers = [a, b].flatMap((file) =>
|
|
506
|
+
file.suites.filter((s) => shared.includes(s.id) && s.settings !== undefined),
|
|
507
|
+
)
|
|
508
|
+
const wasRead = (key: string): boolean =>
|
|
509
|
+
readers.some((s) => {
|
|
510
|
+
const st = s.settings as LoadedSettings
|
|
511
|
+
if (st.read === false) return false
|
|
512
|
+
// A report that does not say which keys it read is taken to have read all.
|
|
513
|
+
if (st.readKeys === undefined) return true
|
|
514
|
+
return st.readKeys.some(
|
|
515
|
+
(k) => k === '*' || k === key || key.startsWith(`${k}.`) || k.startsWith(`${key}.`),
|
|
516
|
+
)
|
|
517
|
+
})
|
|
518
|
+
const unreadKeys = differences.map((d) => d.key).filter((k) => !wasRead(k))
|
|
519
|
+
return {
|
|
520
|
+
a: ref(left),
|
|
521
|
+
b: ref(right),
|
|
522
|
+
identical,
|
|
523
|
+
differences,
|
|
524
|
+
notRead: { a: ignored(a), b: ignored(b) },
|
|
525
|
+
unreadKeys,
|
|
526
|
+
anyDifferenceRead: differences.length === 0 ? null : unreadKeys.length < differences.length,
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function withheldReasons(
|
|
531
|
+
settings: SettingsComparison,
|
|
532
|
+
split: { a: SideSplit; b: SideSplit },
|
|
533
|
+
taskMismatches: number,
|
|
534
|
+
): WithheldReason[] {
|
|
535
|
+
const out: WithheldReason[] = []
|
|
536
|
+
if (settings.a === null || settings.b === null) out.push('no-snapshot')
|
|
537
|
+
if (taskMismatches > 0) out.push('tasks-differ')
|
|
538
|
+
// The most informative reason first: identical settings say the whole
|
|
539
|
+
// difference is noise, whatever split they ran on.
|
|
540
|
+
if (settings.identical === true) out.push('identical-settings')
|
|
541
|
+
// Settings no shared suite read cannot have moved a number: whatever the
|
|
542
|
+
// difference between the runs, it is not the settings.
|
|
543
|
+
if (settings.identical === false && settings.anyDifferenceRead === false) {
|
|
544
|
+
out.push('settings-not-read')
|
|
545
|
+
}
|
|
546
|
+
const tune = split.a === 'tune' || split.b === 'tune'
|
|
547
|
+
if (tune) out.push('tune-split')
|
|
548
|
+
else if (
|
|
549
|
+
settings.a !== null &&
|
|
550
|
+
settings.b !== null &&
|
|
551
|
+
(split.a !== 'held-out' || split.b !== 'held-out')
|
|
552
|
+
) {
|
|
553
|
+
out.push('not-held-out')
|
|
554
|
+
}
|
|
555
|
+
return out
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function short(digest: string): string {
|
|
559
|
+
return digest.slice(0, 12)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function withhold(
|
|
563
|
+
reasons: readonly WithheldReason[],
|
|
564
|
+
settings: SettingsComparison,
|
|
565
|
+
onlyA: number,
|
|
566
|
+
onlyB: number,
|
|
567
|
+
mean: Interval,
|
|
568
|
+
paired: number,
|
|
569
|
+
): EvalComparison['verdict'] {
|
|
570
|
+
const text: Record<WithheldReason, string> = {
|
|
571
|
+
'tune-split':
|
|
572
|
+
'these are the tasks the settings were chosen on; run the held-out split to judge',
|
|
573
|
+
'identical-settings': `the two reports ran under identical settings (${settings.a?.name ?? 'unnamed'}, ${short(settings.a?.digest ?? '')}), so any difference between them is run-to-run noise`,
|
|
574
|
+
'no-snapshot':
|
|
575
|
+
'a report has no settings snapshot (it was written before reports named their settings), so what the two runs changed is not known',
|
|
576
|
+
'tasks-differ': `the two reports ran different tasks (${onlyA} only in A, ${onlyB} only in B), so their numbers are not about the same tasks`,
|
|
577
|
+
'settings-not-read':
|
|
578
|
+
'no suite the two reports share read any of the settings that differ between them, so the settings cannot have changed these numbers',
|
|
579
|
+
'not-held-out':
|
|
580
|
+
'a report ran on all tasks or a mix of splits, which include the tasks the settings were chosen on; run the held-out split to judge',
|
|
581
|
+
}
|
|
582
|
+
const numbers = mean.computed
|
|
583
|
+
? `the paired difference in mean score over ${paired} paired cases is ${signed(mean.estimate)}, 95% interval [${mean.low.toFixed(3)}, ${mean.high.toFixed(3)}]`
|
|
584
|
+
: `no interval: ${mean.reason}`
|
|
585
|
+
return {
|
|
586
|
+
claim: 'none',
|
|
587
|
+
sentence: `no winner is named: ${text[reasons[0] as WithheldReason]}`,
|
|
588
|
+
detail: `${[...reasons.slice(1).map((r) => text[r]), numbers].join('; ')}.`,
|
|
589
|
+
withheld: reasons,
|
|
378
590
|
}
|
|
379
591
|
}
|
|
380
592
|
|
|
@@ -386,7 +598,11 @@ function atPrintedPrecision(value: number): number {
|
|
|
386
598
|
return Math.round(value * 1000) / 1000 || 0
|
|
387
599
|
}
|
|
388
600
|
|
|
389
|
-
function decide(
|
|
601
|
+
function decide(
|
|
602
|
+
mean: Interval,
|
|
603
|
+
paired: number,
|
|
604
|
+
flips: Flips,
|
|
605
|
+
): Omit<EvalComparison['verdict'], 'withheld'> {
|
|
390
606
|
const none = 'no difference can be claimed at this size'
|
|
391
607
|
if (!mean.computed) {
|
|
392
608
|
return { claim: 'none', sentence: none, detail: `no interval: ${mean.reason}.` }
|
|
@@ -453,6 +669,33 @@ export function formatEvalComparison(c: EvalComparison): string {
|
|
|
453
669
|
` (${c.verdict.detail})`,
|
|
454
670
|
'',
|
|
455
671
|
]
|
|
672
|
+
const ref = (x: SnapshotRef | null) =>
|
|
673
|
+
x === null ? 'none recorded' : `"${x.name}" (sha256 ${short(x.digest)})`
|
|
674
|
+
lines.push(
|
|
675
|
+
`Settings: A ${ref(c.settings.a)}, B ${ref(c.settings.b)}. Split: A ${c.split.a ?? 'not recorded'}, B ${c.split.b ?? 'not recorded'}.`,
|
|
676
|
+
)
|
|
677
|
+
if (c.settings.identical === true) {
|
|
678
|
+
lines.push('The two runs ran under identical settings.')
|
|
679
|
+
} else if (c.settings.differences.length > 0) {
|
|
680
|
+
lines.push('What differs in the settings:')
|
|
681
|
+
const show = (v: unknown, has: boolean) => (has ? JSON.stringify(v) : 'not stated')
|
|
682
|
+
for (const x of c.settings.differences) {
|
|
683
|
+
lines.push(` ${x.key}: A ${show(x.a, 'a' in x)}, B ${show(x.b, 'b' in x)}`)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
for (const side of ['a', 'b'] as const) {
|
|
687
|
+
if (c.settings.notRead[side].length > 0) {
|
|
688
|
+
lines.push(
|
|
689
|
+
`Suites that ran under settings but did not read them (${side.toUpperCase()}): ${c.settings.notRead[side].join(', ')}`,
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (c.settings.unreadKeys.length > 0) {
|
|
694
|
+
lines.push(
|
|
695
|
+
`Settings that differ and that no shared suite read: ${c.settings.unreadKeys.join(', ')}`,
|
|
696
|
+
)
|
|
697
|
+
}
|
|
698
|
+
lines.push('')
|
|
456
699
|
const d = c.disclosure
|
|
457
700
|
if (d.differences.length === 0) {
|
|
458
701
|
lines.push('What the two reports say about how they were produced: no stated difference.')
|
package/src/eval/experiment.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EvalUsageError, validateSeed } from './bootstrap.js'
|
|
2
2
|
import { type SuiteFacts, buildDisclosure, describeDisclosure } from './disclosure.js'
|
|
3
3
|
import {
|
|
4
|
+
clarifyDeclared,
|
|
4
5
|
describeIntervals,
|
|
5
6
|
describeSplits,
|
|
6
7
|
duplicateMessage,
|
|
@@ -9,6 +10,15 @@ import {
|
|
|
9
10
|
statsOver,
|
|
10
11
|
} from './report-stats.js'
|
|
11
12
|
import { currentEvalRunOptions } from './run-options.js'
|
|
13
|
+
import { defaultSettingsSnapshot } from './settings.js'
|
|
14
|
+
import {
|
|
15
|
+
HELD_OUT_FRACTION,
|
|
16
|
+
TASK_SPLIT_SEED,
|
|
17
|
+
assignSplit,
|
|
18
|
+
taskId,
|
|
19
|
+
validateSplitFilter,
|
|
20
|
+
} from './split.js'
|
|
21
|
+
import type { ReportTaskSplit } from './split.js'
|
|
12
22
|
import type {
|
|
13
23
|
CaseResult,
|
|
14
24
|
CaseStatus,
|
|
@@ -106,8 +116,28 @@ export async function runExperiment<TInput>(
|
|
|
106
116
|
const alike = duplicateName(config.cases.map((c) => c.name))
|
|
107
117
|
if (alike !== undefined) throw new Error(duplicateMessage(config.name, alike))
|
|
108
118
|
|
|
119
|
+
// Which tasks run: every one, or the half assigned to one split. The
|
|
120
|
+
// assignment is a function of the task's identity, so it does not move.
|
|
121
|
+
const requested = validateSplitFilter(options.split)
|
|
122
|
+
const assigned = config.cases.map((c) => ({
|
|
123
|
+
evalCase: c,
|
|
124
|
+
...assignSplit(config.name, c.name, c.split),
|
|
125
|
+
}))
|
|
126
|
+
const chosen = assigned.filter((t) => requested === 'all' || t.split === requested)
|
|
127
|
+
const cases = chosen.map((t) => t.evalCase)
|
|
128
|
+
const taskSplit: ReportTaskSplit = {
|
|
129
|
+
requested,
|
|
130
|
+
seed: TASK_SPLIT_SEED,
|
|
131
|
+
heldOutFraction: HELD_OUT_FRACTION,
|
|
132
|
+
tasks: chosen.map((t) => ({
|
|
133
|
+
id: taskId(config.name, t.evalCase.name),
|
|
134
|
+
split: t.split,
|
|
135
|
+
declared: t.declared,
|
|
136
|
+
})),
|
|
137
|
+
}
|
|
138
|
+
|
|
109
139
|
// Case-major, so a case's repeats sit together in the report.
|
|
110
|
-
const total =
|
|
140
|
+
const total = cases.length * repeats
|
|
111
141
|
const results: CaseResult[] = new Array(total)
|
|
112
142
|
let cursor = 0
|
|
113
143
|
|
|
@@ -115,7 +145,7 @@ export async function runExperiment<TInput>(
|
|
|
115
145
|
for (;;) {
|
|
116
146
|
const index = cursor++
|
|
117
147
|
if (index >= total) return
|
|
118
|
-
const evalCase =
|
|
148
|
+
const evalCase = cases[Math.floor(index / repeats)] as EvalCase<TInput>
|
|
119
149
|
|
|
120
150
|
const result = await evaluateCase(config, evalCase, threshold, timeoutMs)
|
|
121
151
|
result.repeat = (index % repeats) + 1
|
|
@@ -159,10 +189,16 @@ export async function runExperiment<TInput>(
|
|
|
159
189
|
splits: stats.splits,
|
|
160
190
|
disclosure: buildDisclosure(
|
|
161
191
|
config.disclosure,
|
|
162
|
-
{ cases:
|
|
192
|
+
{ cases: cases.length, repeats, seed },
|
|
163
193
|
options,
|
|
164
194
|
new Date().toISOString(),
|
|
165
195
|
),
|
|
196
|
+
settings: {
|
|
197
|
+
...(options.settings ?? defaultSettingsSnapshot()),
|
|
198
|
+
read: options.settingsUse?.read ?? false,
|
|
199
|
+
readKeys: [...(options.settingsUse?.keys ?? [])].sort(),
|
|
200
|
+
},
|
|
201
|
+
taskSplit,
|
|
166
202
|
}
|
|
167
203
|
}
|
|
168
204
|
|
|
@@ -468,9 +504,27 @@ export function formatReport(report: ExperimentReport): string {
|
|
|
468
504
|
}
|
|
469
505
|
}
|
|
470
506
|
if (report.splits) {
|
|
471
|
-
for (const line of describeSplits(report.splits))
|
|
507
|
+
for (const line of describeSplits(report.splits)) {
|
|
508
|
+
// The task split is the held-out one: the line about cases a suite
|
|
509
|
+
// declared as developed against would contradict the run's own header.
|
|
510
|
+
if (report.taskSplit?.requested === 'held-out' && line.startsWith('no held-out cases'))
|
|
511
|
+
continue
|
|
512
|
+
lines.push(` ${report.taskSplit ? clarifyDeclared(line) : line}`)
|
|
513
|
+
}
|
|
472
514
|
}
|
|
473
515
|
if (report.disclosure) lines.push(` ${describeDisclosure(report.disclosure)}`)
|
|
516
|
+
if (report.settings) {
|
|
517
|
+
const r = report.settings
|
|
518
|
+
lines.push(
|
|
519
|
+
` settings: "${r.name}", sha256 ${r.digest.slice(0, 12)}${r.read || Object.keys(r.values).length === 0 ? '' : ' (this suite does not read them)'}`,
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
if (report.taskSplit) {
|
|
523
|
+
const t = report.taskSplit
|
|
524
|
+
lines.push(
|
|
525
|
+
` split: ${t.requested}, ${t.tasks.length} task${t.tasks.length === 1 ? '' : 's'} ran`,
|
|
526
|
+
)
|
|
527
|
+
}
|
|
474
528
|
lines.push('')
|
|
475
529
|
|
|
476
530
|
// Surfaced above the failures, because an inconclusive case means the
|
package/src/eval/index.ts
CHANGED
|
@@ -52,7 +52,33 @@ export type {
|
|
|
52
52
|
SuiteFacts,
|
|
53
53
|
} from './disclosure.js'
|
|
54
54
|
export { sdkVersion } from './disclosure.js'
|
|
55
|
-
export {
|
|
55
|
+
export {
|
|
56
|
+
currentEvalRunOptions,
|
|
57
|
+
currentEvalSettings,
|
|
58
|
+
evalSetting,
|
|
59
|
+
withEvalRunOptions,
|
|
60
|
+
} from './run-options.js'
|
|
61
|
+
export {
|
|
62
|
+
DEFAULT_SETTINGS_NAME,
|
|
63
|
+
SETTINGS_DIGEST_ALGORITHM,
|
|
64
|
+
buildSettingsSnapshot,
|
|
65
|
+
canonicalJson,
|
|
66
|
+
defaultSettingsSnapshot,
|
|
67
|
+
diffSettings,
|
|
68
|
+
findSecretIn,
|
|
69
|
+
validateSettingsName,
|
|
70
|
+
} from './settings.js'
|
|
71
|
+
export type { ReportSettings, SettingDifference, SettingsSnapshot } from './settings.js'
|
|
72
|
+
export {
|
|
73
|
+
HELD_OUT_FRACTION,
|
|
74
|
+
SPLIT_FILTERS,
|
|
75
|
+
TASK_SPLIT_SEED,
|
|
76
|
+
assignSplit,
|
|
77
|
+
hashedSplit,
|
|
78
|
+
taskId,
|
|
79
|
+
validateSplitFilter,
|
|
80
|
+
} from './split.js'
|
|
81
|
+
export type { ReportTaskSplit, SplitFilter, TaskSplit } from './split.js'
|
|
56
82
|
export type { EvalRunOptions } from './run-options.js'
|
|
57
83
|
export { formatRunSummary, summarizeRun } from './run-summary.js'
|
|
58
84
|
export type { RunDisclosure, RunSummary } from './run-summary.js'
|
|
@@ -67,7 +93,12 @@ export type {
|
|
|
67
93
|
EvalComparison,
|
|
68
94
|
Flips,
|
|
69
95
|
LoadedReportFile,
|
|
96
|
+
LoadedSettings,
|
|
70
97
|
LoadedSuite,
|
|
98
|
+
SettingsComparison,
|
|
99
|
+
SideSplit,
|
|
100
|
+
SnapshotRef,
|
|
101
|
+
WithheldReason,
|
|
71
102
|
SideNumbers,
|
|
72
103
|
} from './compare.js'
|
|
73
104
|
export { describeUncertainty, uncertaintyOf } from './uncertainty.js'
|
package/src/eval/report-stats.ts
CHANGED
|
@@ -255,3 +255,14 @@ export function describeSplits(splits: RunStats['splits']): string[] {
|
|
|
255
255
|
}
|
|
256
256
|
return lines
|
|
257
257
|
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The line about cases a suite declared as held-out is the suite's own
|
|
261
|
+
* statement. Beside the run's `split:` line it reads as a contradiction, since
|
|
262
|
+
* the hashed split puts some of the same tasks in held-out; say which is which.
|
|
263
|
+
*/
|
|
264
|
+
export function clarifyDeclared(line: string): string {
|
|
265
|
+
return line.startsWith('no held-out cases')
|
|
266
|
+
? `${line} (declared by the suite; the split above is the hashed one)`
|
|
267
|
+
: line
|
|
268
|
+
}
|
package/src/eval/run-options.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
import type { SettingsSnapshot } from './settings.js'
|
|
3
|
+
import type { SplitFilter } from './split.js'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Settings an operator gives a whole eval run, handed to every
|
|
@@ -24,6 +26,25 @@ export interface EvalRunOptions {
|
|
|
24
26
|
readonly commit?: string
|
|
25
27
|
/** Version of the suite being run, when known. */
|
|
26
28
|
readonly suiteVersion?: string
|
|
29
|
+
/**
|
|
30
|
+
* The settings the run was given. Written into every report. A suite shapes
|
|
31
|
+
* itself by them through {@link currentEvalSettings}; one that does not is
|
|
32
|
+
* reported as not having read them.
|
|
33
|
+
*/
|
|
34
|
+
readonly settings?: SettingsSnapshot
|
|
35
|
+
/** Which tasks run. Absent means all of them. */
|
|
36
|
+
readonly split?: SplitFilter
|
|
37
|
+
/** Set by {@link currentEvalSettings} when a suite asks for the settings. Owned by the runner. */
|
|
38
|
+
readonly settingsUse?: { read: boolean; keys?: Set<string> }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Stands in the list of read keys for a suite that asked for the whole settings object. */
|
|
42
|
+
export const READ_ALL = '*'
|
|
43
|
+
|
|
44
|
+
function markRead(use: { read: boolean; keys?: Set<string> }, key: string): void {
|
|
45
|
+
const keys = use.keys ?? new Set<string>()
|
|
46
|
+
keys.add(key)
|
|
47
|
+
use.keys = keys
|
|
27
48
|
}
|
|
28
49
|
|
|
29
50
|
const STORE_KEY = Symbol.for('namzu.eval.runOptions')
|
|
@@ -47,3 +68,33 @@ export function withEvalRunOptions<T>(options: EvalRunOptions, work: () => T): T
|
|
|
47
68
|
export function currentEvalRunOptions(): EvalRunOptions {
|
|
48
69
|
return store().getStore() ?? {}
|
|
49
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The values of the settings the run was given, or an empty object when it was
|
|
74
|
+
* given none. Asking marks the suite as one that read them, which its report
|
|
75
|
+
* says: a setting nobody reads cannot have changed what a suite measured.
|
|
76
|
+
*/
|
|
77
|
+
export function currentEvalSettings(): Readonly<Record<string, unknown>> {
|
|
78
|
+
const options = currentEvalRunOptions()
|
|
79
|
+
if (options.settingsUse) {
|
|
80
|
+
options.settingsUse.read = true
|
|
81
|
+
// The whole object was asked for: every key counts as read.
|
|
82
|
+
markRead(options.settingsUse, READ_ALL)
|
|
83
|
+
}
|
|
84
|
+
return options.settings?.values ?? {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** One setting by its dotted path, for example `limits.maxIterations`; undefined when not stated. */
|
|
88
|
+
export function evalSetting(path: string): unknown {
|
|
89
|
+
const options = currentEvalRunOptions()
|
|
90
|
+
let at: unknown = options.settings?.values ?? {}
|
|
91
|
+
if (options.settingsUse) {
|
|
92
|
+
options.settingsUse.read = true
|
|
93
|
+
markRead(options.settingsUse, path)
|
|
94
|
+
}
|
|
95
|
+
for (const part of path.split('.')) {
|
|
96
|
+
if (typeof at !== 'object' || at === null || Array.isArray(at)) return undefined
|
|
97
|
+
at = (at as Record<string, unknown>)[part]
|
|
98
|
+
}
|
|
99
|
+
return at
|
|
100
|
+
}
|
package/src/eval/run-summary.ts
CHANGED
|
@@ -2,11 +2,14 @@ import { UNKNOWN } from './disclosure.js'
|
|
|
2
2
|
import type { RunMode } from './disclosure.js'
|
|
3
3
|
import {
|
|
4
4
|
type RunStats,
|
|
5
|
+
clarifyDeclared,
|
|
5
6
|
describeIntervals,
|
|
6
7
|
describeSplits,
|
|
7
8
|
groupRuns,
|
|
8
9
|
statsOver,
|
|
9
10
|
} from './report-stats.js'
|
|
11
|
+
import type { SettingsSnapshot } from './settings.js'
|
|
12
|
+
import type { SplitFilter } from './split.js'
|
|
10
13
|
import type { ExperimentReport } from './types.js'
|
|
11
14
|
|
|
12
15
|
/** What produced a whole run, across every suite in it. */
|
|
@@ -23,6 +26,10 @@ export interface RunDisclosure {
|
|
|
23
26
|
readonly runs: number
|
|
24
27
|
/** `mixed` when suites that used a model differ in kind; a suite with no model at all does not count. */
|
|
25
28
|
readonly mode: RunMode | 'mixed'
|
|
29
|
+
/** The settings the run was given. Absent from a run written before reports named them. */
|
|
30
|
+
readonly settings?: SettingsSnapshot
|
|
31
|
+
/** Which tasks ran. Absent from a run written before there were splits. */
|
|
32
|
+
readonly split?: SplitFilter
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
/** The numbers for every case of every suite taken together. */
|
|
@@ -40,6 +47,8 @@ export function summarizeRun(
|
|
|
40
47
|
commit: string
|
|
41
48
|
generatedAt: string
|
|
42
49
|
sdkVersion: string
|
|
50
|
+
settings?: SettingsSnapshot
|
|
51
|
+
split?: SplitFilter
|
|
43
52
|
},
|
|
44
53
|
): RunSummary {
|
|
45
54
|
// A case is named by its suite and its id: two suites may reuse a name.
|
|
@@ -74,6 +83,8 @@ export function summarizeRun(
|
|
|
74
83
|
cases: stats.cases,
|
|
75
84
|
runs: stats.runs,
|
|
76
85
|
mode,
|
|
86
|
+
...(context.settings ? { settings: context.settings } : {}),
|
|
87
|
+
...(context.split ? { split: context.split } : {}),
|
|
77
88
|
},
|
|
78
89
|
}
|
|
79
90
|
}
|
|
@@ -96,7 +107,14 @@ export function formatRunSummary(summary: RunSummary): string {
|
|
|
96
107
|
` reliability across repeats: ${r.passedEveryRepeat} of ${r.cases} cases passed in every one of ${d.repeats} repeats${r.fraction === null ? '' : ` (${(r.fraction * 100).toFixed(1)}%)`}`,
|
|
97
108
|
)
|
|
98
109
|
}
|
|
99
|
-
for (const line of describeSplits(summary.splits))
|
|
110
|
+
for (const line of describeSplits(summary.splits)) {
|
|
111
|
+
if (d.split === 'held-out' && line.startsWith('no held-out cases')) continue
|
|
112
|
+
lines.push(` ${d.split ? clarifyDeclared(line) : line}`)
|
|
113
|
+
}
|
|
114
|
+
if (d.settings) {
|
|
115
|
+
lines.push(` settings: "${d.settings.name}", sha256 ${d.settings.digest.slice(0, 12)}`)
|
|
116
|
+
}
|
|
117
|
+
if (d.split) lines.push(` split: ${d.split}`)
|
|
100
118
|
lines.push(
|
|
101
119
|
` produced by: namzu eval ${d.harnessVersion}, sdk ${d.sdkVersion}, commit ${d.commit}, ${d.generatedAt}, ${d.mode}`,
|
|
102
120
|
)
|