@namzu/sdk 62.14.0 → 63.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.
Files changed (142) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/connector/mcp/stdio.d.ts.map +1 -1
  3. package/dist/connector/mcp/stdio.js +3 -0
  4. package/dist/connector/mcp/stdio.js.map +1 -1
  5. package/dist/eval/compare.d.ts +54 -0
  6. package/dist/eval/compare.d.ts.map +1 -1
  7. package/dist/eval/compare.js +146 -1
  8. package/dist/eval/compare.js.map +1 -1
  9. package/dist/eval/experiment.d.ts.map +1 -1
  10. package/dist/eval/experiment.js +46 -6
  11. package/dist/eval/experiment.js.map +1 -1
  12. package/dist/eval/index.d.ts +6 -2
  13. package/dist/eval/index.d.ts.map +1 -1
  14. package/dist/eval/index.js +3 -1
  15. package/dist/eval/index.js.map +1 -1
  16. package/dist/eval/report-stats.d.ts +6 -0
  17. package/dist/eval/report-stats.d.ts.map +1 -1
  18. package/dist/eval/report-stats.js +10 -0
  19. package/dist/eval/report-stats.js.map +1 -1
  20. package/dist/eval/run-options.d.ts +25 -0
  21. package/dist/eval/run-options.d.ts.map +1 -1
  22. package/dist/eval/run-options.js +36 -0
  23. package/dist/eval/run-options.js.map +1 -1
  24. package/dist/eval/run-summary.d.ts +8 -0
  25. package/dist/eval/run-summary.d.ts.map +1 -1
  26. package/dist/eval/run-summary.js +13 -3
  27. package/dist/eval/run-summary.js.map +1 -1
  28. package/dist/eval/settings.d.ts +74 -0
  29. package/dist/eval/settings.d.ts.map +1 -0
  30. package/dist/eval/settings.js +161 -0
  31. package/dist/eval/settings.js.map +1 -0
  32. package/dist/eval/split.d.ts +48 -0
  33. package/dist/eval/split.d.ts.map +1 -0
  34. package/dist/eval/split.js +41 -0
  35. package/dist/eval/split.js.map +1 -0
  36. package/dist/eval/types.d.ts +12 -0
  37. package/dist/eval/types.d.ts.map +1 -1
  38. package/dist/execution/local.d.ts.map +1 -1
  39. package/dist/execution/local.js +2 -1
  40. package/dist/execution/local.js.map +1 -1
  41. package/dist/peers/envelope.d.ts +11 -0
  42. package/dist/peers/envelope.d.ts.map +1 -1
  43. package/dist/peers/envelope.js +29 -0
  44. package/dist/peers/envelope.js.map +1 -1
  45. package/dist/peers/index.d.ts +3 -3
  46. package/dist/peers/index.d.ts.map +1 -1
  47. package/dist/peers/index.js +2 -2
  48. package/dist/peers/index.js.map +1 -1
  49. package/dist/peers/protocol.d.ts +26 -0
  50. package/dist/peers/protocol.d.ts.map +1 -1
  51. package/dist/peers/protocol.js +22 -2
  52. package/dist/peers/protocol.js.map +1 -1
  53. package/dist/peers/record.d.ts +2 -0
  54. package/dist/peers/record.d.ts.map +1 -1
  55. package/dist/plugin/hook-context.d.ts +30 -0
  56. package/dist/plugin/hook-context.d.ts.map +1 -0
  57. package/dist/plugin/hook-context.js +51 -0
  58. package/dist/plugin/hook-context.js.map +1 -0
  59. package/dist/plugin/index.d.ts +2 -1
  60. package/dist/plugin/index.d.ts.map +1 -1
  61. package/dist/plugin/index.js +1 -0
  62. package/dist/plugin/index.js.map +1 -1
  63. package/dist/plugin/lifecycle.d.ts +9 -0
  64. package/dist/plugin/lifecycle.d.ts.map +1 -1
  65. package/dist/plugin/lifecycle.js +13 -0
  66. package/dist/plugin/lifecycle.js.map +1 -1
  67. package/dist/plugin/shell-hook.d.ts +87 -12
  68. package/dist/plugin/shell-hook.d.ts.map +1 -1
  69. package/dist/plugin/shell-hook.js +224 -32
  70. package/dist/plugin/shell-hook.js.map +1 -1
  71. package/dist/process/session-child.d.ts +21 -0
  72. package/dist/process/session-child.d.ts.map +1 -0
  73. package/dist/process/session-child.js +32 -0
  74. package/dist/process/session-child.js.map +1 -0
  75. package/dist/public-runtime.d.ts +4 -3
  76. package/dist/public-runtime.d.ts.map +1 -1
  77. package/dist/public-runtime.js +4 -3
  78. package/dist/public-runtime.js.map +1 -1
  79. package/dist/public-types.d.ts +2 -2
  80. package/dist/public-types.d.ts.map +1 -1
  81. package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
  82. package/dist/runtime/query/executor/tool-call-admission.js +2 -3
  83. package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
  84. package/dist/runtime/query/executor.d.ts +2 -0
  85. package/dist/runtime/query/executor.d.ts.map +1 -1
  86. package/dist/runtime/query/executor.js +13 -1
  87. package/dist/runtime/query/executor.js.map +1 -1
  88. package/dist/runtime/query/index.d.ts.map +1 -1
  89. package/dist/runtime/query/index.js +3 -2
  90. package/dist/runtime/query/index.js.map +1 -1
  91. package/dist/runtime/system-events.d.ts +3 -3
  92. package/dist/runtime/system-events.d.ts.map +1 -1
  93. package/dist/runtime/system-events.js +2 -1
  94. package/dist/runtime/system-events.js.map +1 -1
  95. package/dist/sandbox/provider/local.d.ts +1 -0
  96. package/dist/sandbox/provider/local.d.ts.map +1 -1
  97. package/dist/sandbox/provider/local.js +5 -2
  98. package/dist/sandbox/provider/local.js.map +1 -1
  99. package/dist/sandbox/terminal.d.ts.map +1 -1
  100. package/dist/sandbox/terminal.js +2 -1
  101. package/dist/sandbox/terminal.js.map +1 -1
  102. package/dist/tools/command-shell.d.ts.map +1 -1
  103. package/dist/tools/command-shell.js +7 -1
  104. package/dist/tools/command-shell.js.map +1 -1
  105. package/dist/types/message/index.d.ts +1 -1
  106. package/dist/types/message/index.d.ts.map +1 -1
  107. package/dist/types/message/index.js +1 -0
  108. package/dist/types/message/index.js.map +1 -1
  109. package/dist/types/plugin/index.d.ts +37 -10
  110. package/dist/types/plugin/index.d.ts.map +1 -1
  111. package/dist/types/plugin/index.js.map +1 -1
  112. package/package.json +1 -1
  113. package/src/connector/mcp/stdio.ts +3 -0
  114. package/src/eval/compare.ts +245 -2
  115. package/src/eval/experiment.ts +58 -4
  116. package/src/eval/index.ts +32 -1
  117. package/src/eval/report-stats.ts +11 -0
  118. package/src/eval/run-options.ts +51 -0
  119. package/src/eval/run-summary.ts +19 -1
  120. package/src/eval/settings.ts +225 -0
  121. package/src/eval/split.ts +83 -0
  122. package/src/eval/types.ts +12 -0
  123. package/src/execution/local.ts +2 -1
  124. package/src/peers/envelope.ts +36 -0
  125. package/src/peers/index.ts +3 -1
  126. package/src/peers/protocol.ts +24 -2
  127. package/src/plugin/hook-context.ts +58 -0
  128. package/src/plugin/index.ts +9 -0
  129. package/src/plugin/lifecycle.ts +13 -0
  130. package/src/plugin/shell-hook.ts +290 -43
  131. package/src/process/session-child.ts +38 -0
  132. package/src/public-runtime.ts +27 -0
  133. package/src/public-types.ts +2 -0
  134. package/src/runtime/query/executor/tool-call-admission.ts +2 -4
  135. package/src/runtime/query/executor.ts +15 -3
  136. package/src/runtime/query/index.ts +3 -3
  137. package/src/runtime/system-events.ts +2 -1
  138. package/src/sandbox/provider/local.ts +5 -1
  139. package/src/sandbox/terminal.ts +3 -1
  140. package/src/tools/command-shell.ts +7 -1
  141. package/src/types/message/index.ts +1 -0
  142. package/src/types/plugin/index.ts +48 -16
@@ -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 verdict = decide(difference.mean, pairs.length, flips)
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(mean: Interval, paired: number, flips: Flips): EvalComparison['verdict'] {
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.')
@@ -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 = config.cases.length * repeats
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 = config.cases[Math.floor(index / repeats)] as EvalCase<TInput>
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: config.cases.length, repeats, seed },
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)) lines.push(` ${line}`)
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 { currentEvalRunOptions, withEvalRunOptions } from './run-options.js'
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'
@@ -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
+ }
@@ -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
+ }
@@ -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)) lines.push(` ${line}`)
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
  )