@namzu/sdk 62.12.0 → 62.13.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 (174) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/connector/index.d.ts +1 -1
  3. package/dist/connector/index.d.ts.map +1 -1
  4. package/dist/connector/index.js +1 -1
  5. package/dist/connector/index.js.map +1 -1
  6. package/dist/connector/mcp/client.d.ts +96 -0
  7. package/dist/connector/mcp/client.d.ts.map +1 -1
  8. package/dist/connector/mcp/client.js +488 -13
  9. package/dist/connector/mcp/client.js.map +1 -1
  10. package/dist/connector/mcp/index.d.ts +1 -0
  11. package/dist/connector/mcp/index.d.ts.map +1 -1
  12. package/dist/connector/mcp/index.js +1 -0
  13. package/dist/connector/mcp/index.js.map +1 -1
  14. package/dist/connector/mcp/tasks.d.ts +40 -0
  15. package/dist/connector/mcp/tasks.d.ts.map +1 -0
  16. package/dist/connector/mcp/tasks.js +55 -0
  17. package/dist/connector/mcp/tasks.js.map +1 -0
  18. package/dist/constants/mcp/index.d.ts +1 -1
  19. package/dist/constants/mcp/index.d.ts.map +1 -1
  20. package/dist/constants/mcp/index.js +5 -0
  21. package/dist/constants/mcp/index.js.map +1 -1
  22. package/dist/eval/bootstrap.d.ts +122 -0
  23. package/dist/eval/bootstrap.d.ts.map +1 -0
  24. package/dist/eval/bootstrap.js +227 -0
  25. package/dist/eval/bootstrap.js.map +1 -0
  26. package/dist/eval/compare.d.ts +109 -0
  27. package/dist/eval/compare.d.ts.map +1 -0
  28. package/dist/eval/compare.js +324 -0
  29. package/dist/eval/compare.js.map +1 -0
  30. package/dist/eval/disclosure.d.ts +70 -0
  31. package/dist/eval/disclosure.d.ts.map +1 -0
  32. package/dist/eval/disclosure.js +64 -0
  33. package/dist/eval/disclosure.js.map +1 -0
  34. package/dist/eval/experiment.d.ts +23 -0
  35. package/dist/eval/experiment.d.ts.map +1 -1
  36. package/dist/eval/experiment.js +87 -15
  37. package/dist/eval/experiment.js.map +1 -1
  38. package/dist/eval/from-turn.d.ts.map +1 -1
  39. package/dist/eval/from-turn.js +5 -0
  40. package/dist/eval/from-turn.js.map +1 -1
  41. package/dist/eval/index.d.ts +13 -2
  42. package/dist/eval/index.d.ts.map +1 -1
  43. package/dist/eval/index.js +6 -1
  44. package/dist/eval/index.js.map +1 -1
  45. package/dist/eval/report-stats.d.ts +77 -0
  46. package/dist/eval/report-stats.d.ts.map +1 -0
  47. package/dist/eval/report-stats.js +184 -0
  48. package/dist/eval/report-stats.js.map +1 -0
  49. package/dist/eval/run-options.d.ts +30 -0
  50. package/dist/eval/run-options.d.ts.map +1 -0
  51. package/dist/eval/run-options.js +20 -0
  52. package/dist/eval/run-options.js.map +1 -0
  53. package/dist/eval/run-summary.d.ts +36 -0
  54. package/dist/eval/run-summary.d.ts.map +1 -0
  55. package/dist/eval/run-summary.js +57 -0
  56. package/dist/eval/run-summary.js.map +1 -0
  57. package/dist/eval/scorers.d.ts +32 -0
  58. package/dist/eval/scorers.d.ts.map +1 -1
  59. package/dist/eval/scorers.js +77 -0
  60. package/dist/eval/scorers.js.map +1 -1
  61. package/dist/eval/types.d.ts +74 -0
  62. package/dist/eval/types.d.ts.map +1 -1
  63. package/dist/manager/session/turn-recorder.d.ts +3 -0
  64. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  65. package/dist/manager/session/turn-recorder.js +18 -2
  66. package/dist/manager/session/turn-recorder.js.map +1 -1
  67. package/dist/public-runtime.d.ts +5 -2
  68. package/dist/public-runtime.d.ts.map +1 -1
  69. package/dist/public-runtime.js +7 -2
  70. package/dist/public-runtime.js.map +1 -1
  71. package/dist/public-types.d.ts +2 -0
  72. package/dist/public-types.d.ts.map +1 -1
  73. package/dist/registry/command/kernel-commands.d.ts +3 -0
  74. package/dist/registry/command/kernel-commands.d.ts.map +1 -1
  75. package/dist/registry/command/kernel-commands.js +76 -5
  76. package/dist/registry/command/kernel-commands.js.map +1 -1
  77. package/dist/runtime/query/index.d.ts.map +1 -1
  78. package/dist/runtime/query/index.js +15 -0
  79. package/dist/runtime/query/index.js.map +1 -1
  80. package/dist/runtime/query/iteration/index.d.ts +18 -1
  81. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  82. package/dist/runtime/query/iteration/index.js +80 -1
  83. package/dist/runtime/query/iteration/index.js.map +1 -1
  84. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  85. package/dist/runtime/query/iteration/phases/tool-review.js +18 -1
  86. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  87. package/dist/runtime/query/turn/answer-claims.d.ts +52 -0
  88. package/dist/runtime/query/turn/answer-claims.d.ts.map +1 -0
  89. package/dist/runtime/query/turn/answer-claims.js +348 -0
  90. package/dist/runtime/query/turn/answer-claims.js.map +1 -0
  91. package/dist/runtime/query/turn/outcome-ledger.d.ts +7 -0
  92. package/dist/runtime/query/turn/outcome-ledger.d.ts.map +1 -1
  93. package/dist/runtime/query/turn/outcome-ledger.js +13 -0
  94. package/dist/runtime/query/turn/outcome-ledger.js.map +1 -1
  95. package/dist/store/goal/index.d.ts +3 -1
  96. package/dist/store/goal/index.d.ts.map +1 -1
  97. package/dist/store/goal/index.js +6 -0
  98. package/dist/store/goal/index.js.map +1 -1
  99. package/dist/telemetry/runtime-accessors.d.ts +14 -0
  100. package/dist/telemetry/runtime-accessors.d.ts.map +1 -1
  101. package/dist/telemetry/runtime-accessors.js +32 -0
  102. package/dist/telemetry/runtime-accessors.js.map +1 -1
  103. package/dist/tools/goal/confirmation.d.ts +154 -0
  104. package/dist/tools/goal/confirmation.d.ts.map +1 -0
  105. package/dist/tools/goal/confirmation.js +290 -0
  106. package/dist/tools/goal/confirmation.js.map +1 -0
  107. package/dist/tools/goal/index.d.ts +4 -1
  108. package/dist/tools/goal/index.d.ts.map +1 -1
  109. package/dist/tools/goal/index.js +27 -2
  110. package/dist/tools/goal/index.js.map +1 -1
  111. package/dist/tools/hand-back/index.d.ts +48 -0
  112. package/dist/tools/hand-back/index.d.ts.map +1 -0
  113. package/dist/tools/hand-back/index.js +129 -0
  114. package/dist/tools/hand-back/index.js.map +1 -0
  115. package/dist/tools/schedules/schedule-tool.d.ts.map +1 -1
  116. package/dist/tools/schedules/schedule-tool.js +15 -1
  117. package/dist/tools/schedules/schedule-tool.js.map +1 -1
  118. package/dist/tools/schedules/types.d.ts +10 -0
  119. package/dist/tools/schedules/types.d.ts.map +1 -1
  120. package/dist/types/connector/mcp.d.ts +18 -0
  121. package/dist/types/connector/mcp.d.ts.map +1 -1
  122. package/dist/types/goal/index.d.ts +12 -0
  123. package/dist/types/goal/index.d.ts.map +1 -1
  124. package/dist/types/session/config.d.ts +8 -0
  125. package/dist/types/session/config.d.ts.map +1 -1
  126. package/dist/types/session/hand-back.d.ts +29 -0
  127. package/dist/types/session/hand-back.d.ts.map +1 -0
  128. package/dist/types/session/hand-back.js +11 -0
  129. package/dist/types/session/hand-back.js.map +1 -0
  130. package/dist/types/session/outcome.d.ts +37 -0
  131. package/dist/types/session/outcome.d.ts.map +1 -1
  132. package/dist/types/session/outcome.js.map +1 -1
  133. package/dist/types/session/turn.d.ts +19 -1
  134. package/dist/types/session/turn.d.ts.map +1 -1
  135. package/dist/types/session/turn.js.map +1 -1
  136. package/package.json +1 -1
  137. package/src/connector/index.ts +1 -0
  138. package/src/connector/mcp/client.ts +531 -14
  139. package/src/connector/mcp/index.ts +1 -0
  140. package/src/connector/mcp/tasks.ts +81 -0
  141. package/src/constants/mcp/index.ts +6 -1
  142. package/src/eval/bootstrap.ts +314 -0
  143. package/src/eval/compare.ts +473 -0
  144. package/src/eval/disclosure.ts +136 -0
  145. package/src/eval/experiment.ts +127 -15
  146. package/src/eval/from-turn.ts +5 -0
  147. package/src/eval/index.ts +51 -0
  148. package/src/eval/report-stats.ts +257 -0
  149. package/src/eval/run-options.ts +49 -0
  150. package/src/eval/run-summary.ts +104 -0
  151. package/src/eval/scorers.ts +83 -0
  152. package/src/eval/types.ts +76 -0
  153. package/src/manager/session/turn-recorder.ts +23 -2
  154. package/src/public-runtime.ts +53 -0
  155. package/src/public-types.ts +9 -0
  156. package/src/registry/command/kernel-commands.ts +94 -6
  157. package/src/runtime/query/index.ts +18 -0
  158. package/src/runtime/query/iteration/index.ts +85 -1
  159. package/src/runtime/query/iteration/phases/tool-review.ts +24 -1
  160. package/src/runtime/query/turn/answer-claims.ts +413 -0
  161. package/src/runtime/query/turn/outcome-ledger.ts +12 -0
  162. package/src/store/goal/index.ts +11 -0
  163. package/src/telemetry/runtime-accessors.ts +29 -0
  164. package/src/tools/goal/confirmation.ts +375 -0
  165. package/src/tools/goal/index.ts +37 -1
  166. package/src/tools/hand-back/index.ts +150 -0
  167. package/src/tools/schedules/schedule-tool.ts +19 -1
  168. package/src/tools/schedules/types.ts +10 -0
  169. package/src/types/connector/mcp.ts +18 -0
  170. package/src/types/goal/index.ts +13 -0
  171. package/src/types/session/config.ts +8 -0
  172. package/src/types/session/hand-back.ts +31 -0
  173. package/src/types/session/outcome.ts +42 -0
  174. package/src/types/session/turn.ts +19 -1
@@ -34,6 +34,7 @@ export type {
34
34
  } from './era.js'
35
35
  export { buildEnvelope, decodeResult, encodeMcpHeaderValue } from './envelope.js'
36
36
  export type { McpEnvelope, McpEnvelopeInput, MCPDecodedResult } from './envelope.js'
37
+ export { MCP_TASKS_EXTENSION } from './tasks.js'
37
38
 
38
39
  // The repo's first refusal path: a tool definition namzu declines to expose.
39
40
  // Exported so a host can ask the same question of a schema it holds — and
@@ -0,0 +1,81 @@
1
+ import type { MCPInputRequest } from '../../types/connector/index.js'
2
+
3
+ /** The extension's identifier, in both the client's and the server's `extensions`. */
4
+ export const MCP_TASKS_EXTENSION = 'io.modelcontextprotocol/tasks'
5
+
6
+ /** Poll no faster than this whatever the server suggests, and no slower than the cap. */
7
+ export const MCP_TASK_MIN_POLL_MS = 100
8
+ export const MCP_TASK_MAX_POLL_MS = 30_000
9
+ /** The interval used when the server names none. */
10
+ export const MCP_TASK_DEFAULT_POLL_MS = 1_000
11
+
12
+ export type MCPTaskStatus = 'working' | 'input_required' | 'completed' | 'failed' | 'cancelled'
13
+
14
+ /** One reading of a task: what `CreateTaskResult` and a `tasks/get` result both carry. */
15
+ export interface MCPTaskState {
16
+ readonly taskId: string
17
+ readonly status: MCPTaskStatus
18
+ readonly statusMessage?: string
19
+ readonly lastUpdatedAt?: string
20
+ readonly pollIntervalMs?: number
21
+ /** `input_required`: what the server wants, as `{ method }` entries. */
22
+ readonly inputRequests?: readonly MCPInputRequest[]
23
+ /** `completed`: the result the original request would have returned. */
24
+ readonly result?: unknown
25
+ /** `failed`: the JSON-RPC error. */
26
+ readonly error?: { readonly code?: unknown; readonly message?: unknown; readonly data?: unknown }
27
+ }
28
+
29
+ const STATUSES = new Set<string>(['working', 'input_required', 'completed', 'failed', 'cancelled'])
30
+
31
+ /** Is this result a task handle rather than a result? */
32
+ export function isCreateTaskResult(raw: unknown): boolean {
33
+ return (
34
+ typeof raw === 'object' &&
35
+ raw !== null &&
36
+ (raw as { resultType?: unknown }).resultType === 'task'
37
+ )
38
+ }
39
+
40
+ /**
41
+ * Read a `CreateTaskResult` or a `tasks/get` result, or refuse it.
42
+ *
43
+ * Returns `undefined` for anything that is not a task with a non-empty
44
+ * `taskId` and a status the extension defines, so a malformed answer ends the
45
+ * wait with a clear error instead of polling a handle nobody can name.
46
+ */
47
+ export function decodeTaskState(raw: unknown): MCPTaskState | undefined {
48
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return undefined
49
+ const r = raw as Record<string, unknown>
50
+ if (typeof r.taskId !== 'string' || r.taskId.length === 0) return undefined
51
+ if (typeof r.status !== 'string' || !STATUSES.has(r.status)) return undefined
52
+ const inputRequests =
53
+ typeof r.inputRequests === 'object' && r.inputRequests !== null
54
+ ? Object.values(r.inputRequests as Record<string, unknown>).filter(
55
+ (item): item is MCPInputRequest =>
56
+ typeof item === 'object' &&
57
+ item !== null &&
58
+ typeof (item as { method?: unknown }).method === 'string',
59
+ )
60
+ : undefined
61
+ return {
62
+ taskId: r.taskId,
63
+ status: r.status as MCPTaskStatus,
64
+ ...(typeof r.statusMessage === 'string' ? { statusMessage: r.statusMessage } : {}),
65
+ ...(typeof r.lastUpdatedAt === 'string' ? { lastUpdatedAt: r.lastUpdatedAt } : {}),
66
+ ...(typeof r.pollIntervalMs === 'number' && Number.isFinite(r.pollIntervalMs)
67
+ ? { pollIntervalMs: r.pollIntervalMs }
68
+ : {}),
69
+ ...(inputRequests ? { inputRequests } : {}),
70
+ ...('result' in r ? { result: r.result } : {}),
71
+ ...(typeof r.error === 'object' && r.error !== null
72
+ ? { error: r.error as MCPTaskState['error'] }
73
+ : {}),
74
+ }
75
+ }
76
+
77
+ /** How long to wait before the next `tasks/get`, from what the server last suggested. */
78
+ export function taskPollDelayMs(suggested: number | undefined): number {
79
+ const wanted = suggested ?? MCP_TASK_DEFAULT_POLL_MS
80
+ return Math.min(Math.max(wanted, MCP_TASK_MIN_POLL_MS), MCP_TASK_MAX_POLL_MS)
81
+ }
@@ -177,8 +177,13 @@ export const MCP_PARAM_HEADER_PREFIX = 'Mcp-Param-'
177
177
  * WHICH tool is being called without reading the body is the whole point of
178
178
  * mirroring it into a header.
179
179
  */
180
- export const MCP_NAME_HEADER_METHODS: Readonly<Record<string, 'name' | 'uri'>> = {
180
+ export const MCP_NAME_HEADER_METHODS: Readonly<Record<string, 'name' | 'uri' | 'taskId'>> = {
181
181
  'tools/call': 'name',
182
182
  'prompts/get': 'name',
183
183
  'resources/read': 'uri',
184
+ // The tasks extension: an intermediary routes a task's requests to the
185
+ // instance that holds it, by this header.
186
+ 'tasks/get': 'taskId',
187
+ 'tasks/update': 'taskId',
188
+ 'tasks/cancel': 'taskId',
184
189
  }
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Intervals for an eval, by resampling cases.
3
+ *
4
+ * A pass rate printed alone invites conclusions the data cannot carry: on 80
5
+ * tasks a difference of -1.25 points came with a 95% interval from -10.0 to
6
+ * +7.5, which says almost nothing. The interval here is a percentile
7
+ * bootstrap over CASES. A case is the unit that was sampled from the world, so
8
+ * a case is what gets resampled, and all of a case's repeats travel with it:
9
+ * repeats of one case are not independent evidence about the suite, and
10
+ * treating them as such would narrow the interval for free.
11
+ *
12
+ * Nothing in here reads the clock or an unseeded random source. The generator
13
+ * is a small deterministic one fed by a seed that the report records, so the
14
+ * same cases and the same seed give the same interval.
15
+ */
16
+
17
+ /** Fewest cases an interval is computed for. Below it the report says why. */
18
+ export const MIN_CASES_FOR_INTERVAL = 5
19
+
20
+ /** Resamples drawn per interval. */
21
+ export const BOOTSTRAP_RESAMPLES = 2000
22
+
23
+ /** The seed used when none is given. A fixed number, so a report is reproducible. */
24
+ export const DEFAULT_EVAL_SEED = 1
25
+
26
+ /** Coverage of every interval in this module. */
27
+ export const INTERVAL_LEVEL = 0.95
28
+
29
+ /**
30
+ * What one case contributes: the sum of its judged runs' scores, how many of
31
+ * those runs passed, and how many were judged at all. `judged` is at least 1;
32
+ * a case with no judged run has nothing to say and is left out by the caller.
33
+ */
34
+ export interface CaseTally {
35
+ readonly scoreSum: number
36
+ readonly passes: number
37
+ readonly judged: number
38
+ }
39
+
40
+ /**
41
+ * An interval, or the reason there is none.
42
+ *
43
+ * `estimate` is the number the interval is around, and is present even when
44
+ * no interval is computed, so a reader always has the point value and the
45
+ * explanation for why it carries no bounds.
46
+ */
47
+ export type Interval =
48
+ | {
49
+ readonly computed: true
50
+ readonly estimate: number
51
+ readonly low: number
52
+ readonly high: number
53
+ }
54
+ | {
55
+ readonly computed: false
56
+ readonly estimate: number | null
57
+ readonly reason: string
58
+ }
59
+
60
+ /** The method and its settings, written next to the numbers so they can be reproduced. */
61
+ export interface BootstrapMethod {
62
+ readonly kind: 'percentile bootstrap over cases, repeats kept together'
63
+ readonly level: typeof INTERVAL_LEVEL
64
+ readonly resamples: number
65
+ readonly seed: number
66
+ /** Cases the interval was drawn over. */
67
+ readonly cases: number
68
+ readonly minimumCases: number
69
+ }
70
+
71
+ export interface CaseIntervals {
72
+ readonly method: BootstrapMethod
73
+ /** Mean score over judged runs. */
74
+ readonly mean: Interval
75
+ /** Passed runs over judged runs. */
76
+ readonly passRate: Interval
77
+ }
78
+
79
+ /** The same for a paired difference, second side minus first. */
80
+ export interface DifferenceIntervals {
81
+ readonly method: BootstrapMethod
82
+ readonly mean: Interval
83
+ readonly passRate: Interval
84
+ }
85
+
86
+ /**
87
+ * A bad `--repeat` or `--seed`. A RangeError, so existing handlers still see
88
+ * one, and marked with a code so a CLI can tell it from a RangeError a suite's
89
+ * own code threw (and from one thrown by another copy of this class).
90
+ */
91
+ export class EvalUsageError extends RangeError {
92
+ readonly code = 'EVAL_USAGE'
93
+ }
94
+
95
+ /** True for an {@link EvalUsageError}, even one built by another copy of the SDK. */
96
+ export function isEvalUsageError(err: unknown): err is EvalUsageError {
97
+ return err instanceof RangeError && (err as { code?: unknown }).code === 'EVAL_USAGE'
98
+ }
99
+
100
+ /**
101
+ * Validate a seed: an integer that fits in 32 bits, unsigned.
102
+ * Thrown as an EvalUsageError so a CLI can turn it into a usage error.
103
+ */
104
+ export function validateSeed(value: number | undefined): number {
105
+ if (value === undefined) return DEFAULT_EVAL_SEED
106
+ if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) {
107
+ throw new EvalUsageError(
108
+ `seed must be an integer from 0 to 4294967295, or omitted; received ${String(value)}`,
109
+ )
110
+ }
111
+ return value
112
+ }
113
+
114
+ /**
115
+ * mulberry32: a 32-bit generator with one word of state. Not for secrets;
116
+ * its job is to be the same on every machine for a given seed.
117
+ */
118
+ export function createSeededRandom(seed: number): () => number {
119
+ let state = validateSeed(seed) >>> 0
120
+ return () => {
121
+ state = (state + 0x6d2b79f5) >>> 0
122
+ let t = state
123
+ t = Math.imul(t ^ (t >>> 15), t | 1)
124
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
125
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
126
+ }
127
+ }
128
+
129
+ function quantile(sorted: Float64Array, p: number): number {
130
+ const position = p * (sorted.length - 1)
131
+ const lower = Math.floor(position)
132
+ const upper = Math.ceil(position)
133
+ const low = sorted[lower] as number
134
+ const high = sorted[upper] as number
135
+ return low + (high - low) * (position - lower)
136
+ }
137
+
138
+ /**
139
+ * Draw `resamples` samples of `n` indices with replacement, hand each to
140
+ * `statistics`, and return the 2.5th and 97.5th percentiles of the two numbers
141
+ * it returns. Both statistics see the same draw, so the two intervals come
142
+ * from one set of resampled suites.
143
+ */
144
+ function bootstrap(
145
+ n: number,
146
+ seed: number,
147
+ statistics: (draw: Int32Array) => readonly [number, number],
148
+ ): { mean: [number, number]; passRate: [number, number] } {
149
+ const random = createSeededRandom(seed)
150
+ const draw = new Int32Array(n)
151
+ const means = new Float64Array(BOOTSTRAP_RESAMPLES)
152
+ const rates = new Float64Array(BOOTSTRAP_RESAMPLES)
153
+ for (let b = 0; b < BOOTSTRAP_RESAMPLES; b++) {
154
+ for (let i = 0; i < n; i++) draw[i] = Math.floor(random() * n)
155
+ const [mean, rate] = statistics(draw)
156
+ means[b] = mean
157
+ rates[b] = rate
158
+ }
159
+ means.sort()
160
+ rates.sort()
161
+ const lowP = (1 - INTERVAL_LEVEL) / 2
162
+ const highP = 1 - lowP
163
+ return {
164
+ mean: [quantile(means, lowP), quantile(means, highP)],
165
+ passRate: [quantile(rates, lowP), quantile(rates, highP)],
166
+ }
167
+ }
168
+
169
+ function methodOf(seed: number, cases: number): BootstrapMethod {
170
+ return {
171
+ kind: 'percentile bootstrap over cases, repeats kept together',
172
+ level: INTERVAL_LEVEL,
173
+ resamples: BOOTSTRAP_RESAMPLES,
174
+ seed,
175
+ cases,
176
+ minimumCases: MIN_CASES_FOR_INTERVAL,
177
+ }
178
+ }
179
+
180
+ function tooFew(one: string, many: string, n: number): string {
181
+ return `only ${n} ${n === 1 ? one : many}; an interval needs at least ${MIN_CASES_FOR_INTERVAL}`
182
+ }
183
+
184
+ export function pooledMeanAndRate(
185
+ tallies: readonly CaseTally[],
186
+ draw?: Int32Array,
187
+ ): [number, number] {
188
+ let score = 0
189
+ let passes = 0
190
+ let judged = 0
191
+ const count = draw ? draw.length : tallies.length
192
+ for (let i = 0; i < count; i++) {
193
+ const t = tallies[draw ? (draw[i] as number) : i] as CaseTally
194
+ score += t.scoreSum
195
+ passes += t.passes
196
+ judged += t.judged
197
+ }
198
+ return judged === 0 ? [Number.NaN, Number.NaN] : [score / judged, passes / judged]
199
+ }
200
+
201
+ /**
202
+ * 95% intervals for the mean score and the pass rate over a set of cases.
203
+ *
204
+ * Each resample draws as many cases as there are, with replacement, and pools
205
+ * their runs. Cases without a judged run must not be passed in.
206
+ */
207
+ export function bootstrapCases(tallies: readonly CaseTally[], seed: number): CaseIntervals {
208
+ const checked = validateSeed(seed)
209
+ const n = tallies.length
210
+ const method = methodOf(checked, n)
211
+ const [meanEstimate, rateEstimate] = n === 0 ? [null, null] : pooledMeanAndRate(tallies)
212
+ if (n < MIN_CASES_FOR_INTERVAL) {
213
+ const reason = tooFew('case with a judged run', 'cases with a judged run', n)
214
+ return {
215
+ method,
216
+ mean: { computed: false, estimate: meanEstimate, reason },
217
+ passRate: { computed: false, estimate: rateEstimate, reason },
218
+ }
219
+ }
220
+ const result = bootstrap(n, checked, (draw) => pooledMeanAndRate(tallies, draw))
221
+ return {
222
+ method,
223
+ mean: {
224
+ computed: true,
225
+ estimate: meanEstimate as number,
226
+ low: result.mean[0],
227
+ high: result.mean[1],
228
+ },
229
+ passRate: {
230
+ computed: true,
231
+ estimate: rateEstimate as number,
232
+ low: result.passRate[0],
233
+ high: result.passRate[1],
234
+ },
235
+ }
236
+ }
237
+
238
+ /** One case seen from both sides of a comparison. */
239
+ export interface PairedTally {
240
+ readonly a: CaseTally
241
+ readonly b: CaseTally
242
+ }
243
+
244
+ /**
245
+ * 95% intervals for the difference of two runs over the same cases, `b` minus
246
+ * `a`. A resample draws PAIRS, so the two sides of a case always travel
247
+ * together; that is what makes the interval a paired one, and it is much
248
+ * narrower than two separate intervals when the cases are the thing that
249
+ * varies.
250
+ */
251
+ export function bootstrapPairedDifference(
252
+ pairs: readonly PairedTally[],
253
+ seed: number,
254
+ ): DifferenceIntervals {
255
+ const checked = validateSeed(seed)
256
+ const n = pairs.length
257
+ const method = methodOf(checked, n)
258
+ const aSide = pairs.map((p) => p.a)
259
+ const bSide = pairs.map((p) => p.b)
260
+ const difference = (draw?: Int32Array): [number, number] => {
261
+ const [bMean, bRate] = pooledMeanAndRate(bSide, draw)
262
+ const [aMean, aRate] = pooledMeanAndRate(aSide, draw)
263
+ return [bMean - aMean, bRate - aRate]
264
+ }
265
+ const [meanEstimate, rateEstimate] = n === 0 ? [null, null] : difference()
266
+ if (n < MIN_CASES_FOR_INTERVAL) {
267
+ const reason = tooFew('paired case', 'paired cases', n)
268
+ return {
269
+ method,
270
+ mean: { computed: false, estimate: meanEstimate, reason },
271
+ passRate: { computed: false, estimate: rateEstimate, reason },
272
+ }
273
+ }
274
+ const result = bootstrap(n, checked, (draw) => difference(draw))
275
+ return {
276
+ method,
277
+ mean: {
278
+ computed: true,
279
+ estimate: meanEstimate as number,
280
+ low: result.mean[0],
281
+ high: result.mean[1],
282
+ },
283
+ passRate: {
284
+ computed: true,
285
+ estimate: rateEstimate as number,
286
+ low: result.passRate[0],
287
+ high: result.passRate[1],
288
+ },
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Exact two-sided sign test on discordant pairs: under no difference each flip
294
+ * is equally likely to go either way, so the count going one way is
295
+ * Binomial(n, 1/2). Returns twice the smaller tail, capped at 1; with no flips
296
+ * at all the answer is 1.
297
+ */
298
+ export function exactSignTestP(oneWay: number, otherWay: number): number {
299
+ const n = oneWay + otherWay
300
+ if (n === 0) return 1
301
+ const k = Math.min(oneWay, otherWay)
302
+ const logFactorial = new Float64Array(n + 1)
303
+ for (let i = 2; i <= n; i++) logFactorial[i] = (logFactorial[i - 1] as number) + Math.log(i)
304
+ let tail = 0
305
+ for (let i = 0; i <= k; i++) {
306
+ const logTerm =
307
+ (logFactorial[n] as number) -
308
+ (logFactorial[i] as number) -
309
+ (logFactorial[n - i] as number) -
310
+ n * Math.LN2
311
+ tail += Math.exp(logTerm)
312
+ }
313
+ return Math.min(1, 2 * tail)
314
+ }