@holoscript/std 2.1.0 → 3.1.1

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 (85) hide show
  1. package/README.md +197 -0
  2. package/dist/__tests__/EconomicPrimitives.test.d.ts.map +1 -0
  3. package/dist/__tests__/EconomicTraits.test.d.ts.map +1 -0
  4. package/dist/__tests__/SimulationLabPrimitives.test.d.ts.map +1 -0
  5. package/dist/__tests__/Sprint33.test.d.ts.map +1 -0
  6. package/dist/__tests__/Sprint56.test.d.ts.map +1 -0
  7. package/dist/__tests__/collections.test.d.ts.map +1 -0
  8. package/dist/__tests__/events.test.d.ts.map +1 -0
  9. package/dist/__tests__/materials.test.d.ts.map +1 -0
  10. package/dist/__tests__/math.test.d.ts.map +1 -0
  11. package/dist/__tests__/physics.test.d.ts.map +1 -0
  12. package/dist/__tests__/spatial.test.d.ts.map +1 -0
  13. package/dist/__tests__/string-decoupled.test.d.ts.map +1 -0
  14. package/dist/__tests__/string.test.d.ts.map +1 -0
  15. package/dist/__tests__/time.test.d.ts.map +1 -0
  16. package/dist/collections.d.ts.map +1 -1
  17. package/dist/events.d.ts.map +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +42 -9
  20. package/dist/materials.d.ts.map +1 -0
  21. package/dist/math.d.ts.map +1 -1
  22. package/dist/physics.d.ts.map +1 -0
  23. package/dist/spatial.d.ts.map +1 -0
  24. package/dist/string.d.ts.map +1 -1
  25. package/dist/time.d.ts.map +1 -1
  26. package/dist/traits/ARTraits.d.ts.map +1 -0
  27. package/dist/traits/EconomicPrimitives.d.ts.map +1 -0
  28. package/dist/traits/EconomicTraits.d.ts.map +1 -0
  29. package/dist/traits/IoTTraits.d.ts.map +1 -0
  30. package/dist/traits/SimulationLabPrimitives.d.ts.map +1 -0
  31. package/dist/traits/SimulationLabTraits.d.ts.map +1 -0
  32. package/dist/traits/VRRTraits.d.ts.map +1 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +17 -6
  35. package/src/__tests__/EconomicPrimitives.test.ts +690 -0
  36. package/src/__tests__/EconomicTraits.test.ts +425 -0
  37. package/src/__tests__/SimulationLabPrimitives.test.ts +415 -0
  38. package/src/__tests__/Sprint33.test.ts +1301 -0
  39. package/src/__tests__/Sprint56.test.ts +1070 -0
  40. package/src/__tests__/collections.test.ts +182 -0
  41. package/src/__tests__/events.test.ts +135 -0
  42. package/src/__tests__/materials.test.ts +84 -0
  43. package/src/__tests__/math.test.ts +246 -0
  44. package/src/__tests__/physics.test.ts +83 -0
  45. package/src/__tests__/spatial.test.ts +333 -0
  46. package/src/__tests__/string-decoupled.test.ts +16 -0
  47. package/src/__tests__/string.test.ts +164 -0
  48. package/src/__tests__/time.test.ts +110 -0
  49. package/src/collections.ts +8 -2
  50. package/src/events.ts +88 -0
  51. package/src/index.ts +161 -11
  52. package/src/materials.ts +109 -0
  53. package/src/math.ts +30 -14
  54. package/src/physics.ts +141 -0
  55. package/src/spatial.ts +320 -0
  56. package/src/string.basic.test.ts +14 -0
  57. package/src/string.test.ts +335 -0
  58. package/src/string.ts +19 -2
  59. package/src/time.ts +9 -10
  60. package/src/traits/ARTraits.ts +103 -0
  61. package/src/traits/EconomicPrimitives.ts +755 -0
  62. package/src/traits/EconomicTraits.ts +552 -0
  63. package/src/traits/IoTTraits.ts +102 -0
  64. package/src/traits/SimulationLabPrimitives.ts +650 -0
  65. package/src/traits/SimulationLabTraits.ts +191 -0
  66. package/src/traits/VRRTraits.ts +326 -0
  67. package/src/types.ts +47 -12
  68. package/vitest.config.ts +10 -0
  69. package/dist/collections.d.ts +0 -177
  70. package/dist/collections.js +0 -720
  71. package/dist/collections.js.map +0 -1
  72. package/dist/index.d.ts +0 -89
  73. package/dist/index.js.map +0 -1
  74. package/dist/math.d.ts +0 -162
  75. package/dist/math.js +0 -539
  76. package/dist/math.js.map +0 -1
  77. package/dist/string.d.ts +0 -208
  78. package/dist/string.js +0 -443
  79. package/dist/string.js.map +0 -1
  80. package/dist/time.d.ts +0 -215
  81. package/dist/time.js +0 -530
  82. package/dist/time.js.map +0 -1
  83. package/dist/types.d.ts +0 -193
  84. package/dist/types.js +0 -198
  85. package/dist/types.js.map +0 -1
@@ -0,0 +1,650 @@
1
+ /**
2
+ * @fileoverview SimulationLab Primitives - Runtime math and types for hypothesis testing
3
+ * @module @holoscript/std/simulation
4
+ *
5
+ * Provides the mathematical building blocks for scientific hypothesis testing
6
+ * in HoloScript simulations. Run N epochs with parameter sweeps, collect metrics,
7
+ * and perform statistical analysis to prove or disprove hypotheses.
8
+ *
9
+ * The universe gets one sample. HoloScript gives you infinite.
10
+ *
11
+ * @version 1.0.0
12
+ * @category simulation
13
+ */
14
+
15
+ // =============================================================================
16
+ // CORE TYPES
17
+ // =============================================================================
18
+
19
+ /**
20
+ * Direction of expected effect in a hypothesis.
21
+ */
22
+ export type HypothesisDirection = 'greater' | 'less' | 'different' | 'equal';
23
+
24
+ /**
25
+ * Status of a simulation run.
26
+ */
27
+ export type SimulationStatus = 'pending' | 'running' | 'completed' | 'failed';
28
+
29
+ /**
30
+ * A scientific hypothesis to test.
31
+ */
32
+ export interface Hypothesis {
33
+ /** Human-readable description */
34
+ description: string;
35
+ /** The null hypothesis (what we're trying to reject) */
36
+ null_hypothesis: string;
37
+ /** The alternative hypothesis (what we're testing for) */
38
+ alternative_hypothesis: string;
39
+ /** Expected direction of effect */
40
+ direction: HypothesisDirection;
41
+ /** Significance threshold (default: 0.05) */
42
+ alpha: number;
43
+ }
44
+
45
+ /**
46
+ * A range of parameter values to sweep.
47
+ */
48
+ export interface ParameterRange {
49
+ /** Parameter name */
50
+ name: string;
51
+ /** Start value */
52
+ min: number;
53
+ /** End value */
54
+ max: number;
55
+ /** Step size */
56
+ step: number;
57
+ }
58
+
59
+ /**
60
+ * A set of discrete parameter values to sweep.
61
+ */
62
+ export interface ParameterSet {
63
+ /** Parameter name */
64
+ name: string;
65
+ /** Discrete values to test */
66
+ values: number[];
67
+ }
68
+
69
+ /**
70
+ * Either a range or discrete set of parameter values.
71
+ */
72
+ export type ParameterSweep = ParameterRange | ParameterSet;
73
+
74
+ /**
75
+ * Metrics collected from a single simulation run.
76
+ */
77
+ export interface SimulationMetrics {
78
+ /** Named metric values */
79
+ values: Record<string, number>;
80
+ /** Seed used for this run */
81
+ seed: number;
82
+ /** Parameter values used */
83
+ params: Record<string, number>;
84
+ /** Wall-clock time in ms */
85
+ durationMs: number;
86
+ }
87
+
88
+ /**
89
+ * Result of a statistical test.
90
+ */
91
+ export interface StatisticalResult {
92
+ /** Test statistic value */
93
+ statistic: number;
94
+ /** p-value (probability of observing result under null hypothesis) */
95
+ pValue: number;
96
+ /** Whether the null hypothesis is rejected at the given alpha */
97
+ significant: boolean;
98
+ /** Confidence interval [lower, upper] */
99
+ confidenceInterval: [number, number];
100
+ /** Effect size (Cohen's d for t-test) */
101
+ effectSize: number;
102
+ /** Name of the test used */
103
+ testName: string;
104
+ /** Number of samples */
105
+ sampleSize: number;
106
+ }
107
+
108
+ /**
109
+ * Complete simulation experiment result.
110
+ */
111
+ export interface ExperimentResult {
112
+ /** The hypothesis tested */
113
+ hypothesis: Hypothesis;
114
+ /** Statistical results per metric */
115
+ results: Record<string, StatisticalResult>;
116
+ /** All collected metrics */
117
+ metrics: SimulationMetrics[];
118
+ /** Total epochs run */
119
+ totalEpochs: number;
120
+ /** Total wall-clock time */
121
+ totalDurationMs: number;
122
+ /** Status */
123
+ status: SimulationStatus;
124
+ /** Composition hash for reproducibility */
125
+ compositionHash: string;
126
+ }
127
+
128
+ // =============================================================================
129
+ // PARAMETER SWEEP GENERATION
130
+ // =============================================================================
131
+
132
+ /**
133
+ * Check if a parameter sweep is a range (vs discrete set).
134
+ */
135
+ export function isParameterRange(sweep: ParameterSweep): sweep is ParameterRange {
136
+ return 'min' in sweep && 'max' in sweep && 'step' in sweep;
137
+ }
138
+
139
+ /**
140
+ * Generate all values from a parameter sweep.
141
+ */
142
+ export function expandSweep(sweep: ParameterSweep): number[] {
143
+ if (!isParameterRange(sweep)) {
144
+ return (sweep as ParameterSet).values.slice();
145
+ }
146
+
147
+ const range = sweep as ParameterRange;
148
+ if (range.step <= 0) return [range.min];
149
+ if (range.min > range.max) return [];
150
+
151
+ const values: number[] = [];
152
+ for (let v = range.min; v <= range.max + range.step * 0.001; v += range.step) {
153
+ values.push(Math.round(v * 1e10) / 1e10); // Avoid floating point drift
154
+ }
155
+ return values;
156
+ }
157
+
158
+ /**
159
+ * Generate all parameter combinations from multiple sweeps.
160
+ * Returns an array of parameter maps.
161
+ */
162
+ export function generateSweepCombinations(sweeps: ParameterSweep[]): Record<string, number>[] {
163
+ if (sweeps.length === 0) return [{}];
164
+
165
+ const expanded = sweeps.map((s) => ({
166
+ name: s.name,
167
+ values: expandSweep(s),
168
+ }));
169
+
170
+ // Cartesian product
171
+ let combinations: Record<string, number>[] = [{}];
172
+
173
+ for (const { name, values } of expanded) {
174
+ const newCombinations: Record<string, number>[] = [];
175
+ for (const combo of combinations) {
176
+ for (const val of values) {
177
+ newCombinations.push({ ...combo, [name]: val });
178
+ }
179
+ }
180
+ combinations = newCombinations;
181
+ }
182
+
183
+ return combinations;
184
+ }
185
+
186
+ // =============================================================================
187
+ // STATISTICAL FUNCTIONS
188
+ // =============================================================================
189
+
190
+ /**
191
+ * Calculate the arithmetic mean of an array of numbers.
192
+ */
193
+ export function mean(data: number[]): number {
194
+ if (data.length === 0) return 0;
195
+ return data.reduce((sum, v) => sum + v, 0) / data.length;
196
+ }
197
+
198
+ /**
199
+ * Calculate the sample variance (Bessel's correction).
200
+ */
201
+ export function variance(data: number[]): number {
202
+ if (data.length < 2) return 0;
203
+ const m = mean(data);
204
+ const squaredDiffs = data.reduce((sum, v) => sum + (v - m) ** 2, 0);
205
+ return squaredDiffs / (data.length - 1);
206
+ }
207
+
208
+ /**
209
+ * Calculate the sample standard deviation.
210
+ */
211
+ export function standardDeviation(data: number[]): number {
212
+ return Math.sqrt(variance(data));
213
+ }
214
+
215
+ /**
216
+ * Calculate the standard error of the mean.
217
+ */
218
+ export function standardError(data: number[]): number {
219
+ if (data.length === 0) return 0;
220
+ return standardDeviation(data) / Math.sqrt(data.length);
221
+ }
222
+
223
+ /**
224
+ * Calculate Cohen's d effect size between two groups.
225
+ * d = (mean1 - mean2) / pooled_std
226
+ */
227
+ export function cohensD(group1: number[], group2: number[]): number {
228
+ if (group1.length < 2 || group2.length < 2) return 0;
229
+
230
+ const m1 = mean(group1);
231
+ const m2 = mean(group2);
232
+ const v1 = variance(group1);
233
+ const v2 = variance(group2);
234
+
235
+ // Pooled standard deviation
236
+ const n1 = group1.length;
237
+ const n2 = group2.length;
238
+ const pooledVar = ((n1 - 1) * v1 + (n2 - 1) * v2) / (n1 + n2 - 2);
239
+ const pooledStd = Math.sqrt(pooledVar);
240
+
241
+ if (pooledStd === 0) return 0;
242
+ return (m1 - m2) / pooledStd;
243
+ }
244
+
245
+ /**
246
+ * Two-sample independent t-test.
247
+ *
248
+ * Tests whether two groups have significantly different means.
249
+ * Uses Welch's t-test (does not assume equal variances).
250
+ */
251
+ export function tTest(
252
+ group1: number[],
253
+ group2: number[],
254
+ alpha: number = 0.05,
255
+ direction: HypothesisDirection = 'different'
256
+ ): StatisticalResult {
257
+ const n1 = group1.length;
258
+ const n2 = group2.length;
259
+
260
+ if (n1 < 2 || n2 < 2) {
261
+ return {
262
+ statistic: 0,
263
+ pValue: 1,
264
+ significant: false,
265
+ confidenceInterval: [0, 0],
266
+ effectSize: 0,
267
+ testName: 'welch_t_test',
268
+ sampleSize: n1 + n2,
269
+ };
270
+ }
271
+
272
+ const m1 = mean(group1);
273
+ const m2 = mean(group2);
274
+ const v1 = variance(group1);
275
+ const v2 = variance(group2);
276
+ const se = Math.sqrt(v1 / n1 + v2 / n2);
277
+
278
+ if (se === 0) {
279
+ return {
280
+ statistic: 0,
281
+ pValue: m1 === m2 ? 1 : 0,
282
+ significant: m1 !== m2,
283
+ confidenceInterval: [m1 - m2, m1 - m2],
284
+ effectSize: 0,
285
+ testName: 'welch_t_test',
286
+ sampleSize: n1 + n2,
287
+ };
288
+ }
289
+
290
+ const t = (m1 - m2) / se;
291
+
292
+ // Welch–Satterthwaite degrees of freedom
293
+ const num = (v1 / n1 + v2 / n2) ** 2;
294
+ const den = (v1 / n1) ** 2 / (n1 - 1) + (v2 / n2) ** 2 / (n2 - 1);
295
+ const df = num / den;
296
+
297
+ // Approximate p-value using the t-distribution CDF approximation
298
+ const pTwoTailed = tDistPValue(Math.abs(t), df);
299
+ let pValue: number;
300
+
301
+ switch (direction) {
302
+ case 'greater':
303
+ pValue = t > 0 ? pTwoTailed / 2 : 1 - pTwoTailed / 2;
304
+ break;
305
+ case 'less':
306
+ pValue = t < 0 ? pTwoTailed / 2 : 1 - pTwoTailed / 2;
307
+ break;
308
+ case 'equal':
309
+ pValue = 1 - pTwoTailed;
310
+ break;
311
+ default: // 'different'
312
+ pValue = pTwoTailed;
313
+ }
314
+
315
+ // Confidence interval for the difference in means
316
+ const tCrit = tDistCritical(alpha, df);
317
+ const diff = m1 - m2;
318
+ const ci: [number, number] = [diff - tCrit * se, diff + tCrit * se];
319
+
320
+ return {
321
+ statistic: t,
322
+ pValue,
323
+ significant: pValue < alpha,
324
+ confidenceInterval: ci,
325
+ effectSize: cohensD(group1, group2),
326
+ testName: 'welch_t_test',
327
+ sampleSize: n1 + n2,
328
+ };
329
+ }
330
+
331
+ /**
332
+ * One-sample t-test against a known value.
333
+ */
334
+ export function oneSampleTTest(
335
+ data: number[],
336
+ populationMean: number,
337
+ alpha: number = 0.05,
338
+ direction: HypothesisDirection = 'different'
339
+ ): StatisticalResult {
340
+ const n = data.length;
341
+
342
+ if (n < 2) {
343
+ return {
344
+ statistic: 0,
345
+ pValue: 1,
346
+ significant: false,
347
+ confidenceInterval: [0, 0],
348
+ effectSize: 0,
349
+ testName: 'one_sample_t_test',
350
+ sampleSize: n,
351
+ };
352
+ }
353
+
354
+ const m = mean(data);
355
+ const se = standardError(data);
356
+
357
+ if (se === 0) {
358
+ return {
359
+ statistic: 0,
360
+ pValue: m === populationMean ? 1 : 0,
361
+ significant: m !== populationMean,
362
+ confidenceInterval: [m, m],
363
+ effectSize: 0,
364
+ testName: 'one_sample_t_test',
365
+ sampleSize: n,
366
+ };
367
+ }
368
+
369
+ const t = (m - populationMean) / se;
370
+ const df = n - 1;
371
+
372
+ const pTwoTailed = tDistPValue(Math.abs(t), df);
373
+ let pValue: number;
374
+
375
+ switch (direction) {
376
+ case 'greater':
377
+ pValue = t > 0 ? pTwoTailed / 2 : 1 - pTwoTailed / 2;
378
+ break;
379
+ case 'less':
380
+ pValue = t < 0 ? pTwoTailed / 2 : 1 - pTwoTailed / 2;
381
+ break;
382
+ default:
383
+ pValue = pTwoTailed;
384
+ }
385
+
386
+ const tCrit = tDistCritical(alpha, df);
387
+ const ci: [number, number] = [m - tCrit * se, m + tCrit * se];
388
+
389
+ const sd = standardDeviation(data);
390
+ const effectSize = sd > 0 ? (m - populationMean) / sd : 0;
391
+
392
+ return {
393
+ statistic: t,
394
+ pValue,
395
+ significant: pValue < alpha,
396
+ confidenceInterval: ci,
397
+ effectSize,
398
+ testName: 'one_sample_t_test',
399
+ sampleSize: n,
400
+ };
401
+ }
402
+
403
+ /**
404
+ * Mann-Whitney U test (non-parametric alternative to t-test).
405
+ * Tests whether one group tends to have larger values than the other.
406
+ */
407
+ export function mannWhitneyU(
408
+ group1: number[],
409
+ group2: number[],
410
+ alpha: number = 0.05
411
+ ): StatisticalResult {
412
+ const n1 = group1.length;
413
+ const n2 = group2.length;
414
+
415
+ if (n1 === 0 || n2 === 0) {
416
+ return {
417
+ statistic: 0,
418
+ pValue: 1,
419
+ significant: false,
420
+ confidenceInterval: [0, 0],
421
+ effectSize: 0,
422
+ testName: 'mann_whitney_u',
423
+ sampleSize: n1 + n2,
424
+ };
425
+ }
426
+
427
+ // Count how many times group1 values exceed group2 values
428
+ let u1 = 0;
429
+ for (const a of group1) {
430
+ for (const b of group2) {
431
+ if (a > b) u1++;
432
+ else if (a === b) u1 += 0.5;
433
+ }
434
+ }
435
+
436
+ const u2 = n1 * n2 - u1;
437
+ const u = Math.min(u1, u2);
438
+
439
+ // Normal approximation for larger samples
440
+ const meanU = (n1 * n2) / 2;
441
+ const sigmaU = Math.sqrt((n1 * n2 * (n1 + n2 + 1)) / 12);
442
+
443
+ if (sigmaU === 0) {
444
+ return {
445
+ statistic: u,
446
+ pValue: 1,
447
+ significant: false,
448
+ confidenceInterval: [0, 0],
449
+ effectSize: 0,
450
+ testName: 'mann_whitney_u',
451
+ sampleSize: n1 + n2,
452
+ };
453
+ }
454
+
455
+ const z = (u - meanU) / sigmaU;
456
+ const pValue = 2 * normalCDF(-Math.abs(z)); // Two-tailed
457
+
458
+ // Effect size: rank-biserial correlation r = 1 - 2U/(n1*n2)
459
+ const effectSize = 1 - (2 * u) / (n1 * n2);
460
+
461
+ return {
462
+ statistic: u,
463
+ pValue,
464
+ significant: pValue < alpha,
465
+ confidenceInterval: [0, 0], // CI not standard for U-test
466
+ effectSize,
467
+ testName: 'mann_whitney_u',
468
+ sampleSize: n1 + n2,
469
+ };
470
+ }
471
+
472
+ /**
473
+ * Chi-squared goodness-of-fit test.
474
+ * Tests whether observed frequencies match expected frequencies.
475
+ */
476
+ export function chiSquaredTest(
477
+ observed: number[],
478
+ expected: number[],
479
+ alpha: number = 0.05
480
+ ): StatisticalResult {
481
+ if (observed.length !== expected.length || observed.length === 0) {
482
+ return {
483
+ statistic: 0,
484
+ pValue: 1,
485
+ significant: false,
486
+ confidenceInterval: [0, 0],
487
+ effectSize: 0,
488
+ testName: 'chi_squared',
489
+ sampleSize: 0,
490
+ };
491
+ }
492
+
493
+ let chiSq = 0;
494
+ let totalObserved = 0;
495
+
496
+ for (let i = 0; i < observed.length; i++) {
497
+ if (expected[i] <= 0) continue;
498
+ chiSq += (observed[i] - expected[i]) ** 2 / expected[i];
499
+ totalObserved += observed[i];
500
+ }
501
+
502
+ const df = observed.length - 1;
503
+ if (df <= 0) {
504
+ return {
505
+ statistic: chiSq,
506
+ pValue: 1,
507
+ significant: false,
508
+ confidenceInterval: [0, 0],
509
+ effectSize: 0,
510
+ testName: 'chi_squared',
511
+ sampleSize: totalObserved,
512
+ };
513
+ }
514
+
515
+ // Approximate p-value using Wilson-Hilferty approximation
516
+ const pValue = chiSquaredPValue(chiSq, df);
517
+
518
+ // Cramér's V effect size
519
+ const k = observed.length;
520
+ const effectSize = totalObserved > 0 ? Math.sqrt(chiSq / (totalObserved * (k - 1))) : 0;
521
+
522
+ return {
523
+ statistic: chiSq,
524
+ pValue,
525
+ significant: pValue < alpha,
526
+ confidenceInterval: [0, 0],
527
+ effectSize,
528
+ testName: 'chi_squared',
529
+ sampleSize: totalObserved,
530
+ };
531
+ }
532
+
533
+ // =============================================================================
534
+ // METRIC AGGREGATION
535
+ // =============================================================================
536
+
537
+ /**
538
+ * Extract a named metric from an array of simulation results.
539
+ */
540
+ export function extractMetric(results: SimulationMetrics[], metricName: string): number[] {
541
+ return results.filter((r) => metricName in r.values).map((r) => r.values[metricName]);
542
+ }
543
+
544
+ /**
545
+ * Group simulation results by a parameter value.
546
+ */
547
+ export function groupByParameter(
548
+ results: SimulationMetrics[],
549
+ paramName: string
550
+ ): Map<number, SimulationMetrics[]> {
551
+ const groups = new Map<number, SimulationMetrics[]>();
552
+ for (const r of results) {
553
+ const val = r.params[paramName];
554
+ if (val === undefined) continue;
555
+ if (!groups.has(val)) groups.set(val, []);
556
+ groups.get(val)!.push(r);
557
+ }
558
+ return groups;
559
+ }
560
+
561
+ /**
562
+ * Generate a summary of metrics across all runs.
563
+ */
564
+ export function summarizeMetrics(
565
+ results: SimulationMetrics[],
566
+ metricName: string
567
+ ): { mean: number; std: number; min: number; max: number; n: number } {
568
+ const values = extractMetric(results, metricName);
569
+ if (values.length === 0) {
570
+ return { mean: 0, std: 0, min: 0, max: 0, n: 0 };
571
+ }
572
+ return {
573
+ mean: mean(values),
574
+ std: standardDeviation(values),
575
+ min: Math.min(...values),
576
+ max: Math.max(...values),
577
+ n: values.length,
578
+ };
579
+ }
580
+
581
+ // =============================================================================
582
+ // INTERNAL MATH HELPERS
583
+ // =============================================================================
584
+
585
+ /**
586
+ * Standard normal CDF approximation (Abramowitz and Stegun 26.2.17).
587
+ * Accurate to ~1.5e-7.
588
+ */
589
+ function normalCDF(x: number): number {
590
+ if (x < -8) return 0;
591
+ if (x > 8) return 1;
592
+
593
+ const a1 = 0.254829592;
594
+ const a2 = -0.284496736;
595
+ const a3 = 1.421413741;
596
+ const a4 = -1.453152027;
597
+ const a5 = 1.061405429;
598
+ const p = 0.3275911;
599
+
600
+ const sign = x < 0 ? -1 : 1;
601
+ const absX = Math.abs(x);
602
+ const t = 1.0 / (1.0 + p * absX);
603
+ const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp((-absX * absX) / 2);
604
+
605
+ return 0.5 * (1.0 + sign * y);
606
+ }
607
+
608
+ /**
609
+ * Approximate two-tailed p-value from t-distribution.
610
+ * Uses the Inamdar-Griessammer approximation for large df.
611
+ */
612
+ function tDistPValue(absT: number, df: number): number {
613
+ if (df <= 0) return 1;
614
+ // Convert t to approximately normal z for large df
615
+ const z = (absT * (1 - 1 / (4 * df))) / Math.sqrt(1 + (absT * absT) / (2 * df));
616
+ return 2 * (1 - normalCDF(z));
617
+ }
618
+
619
+ /**
620
+ * Approximate critical t-value for a given alpha and df.
621
+ */
622
+ function tDistCritical(alpha: number, df: number): number {
623
+ // Approximate using normal quantile adjusted for df
624
+ const p = 1 - alpha / 2;
625
+ // Rational approximation of the normal quantile (Beasley-Springer-Moro)
626
+ const a = p - 0.5;
627
+ const r = a * a;
628
+ let z =
629
+ (a * (2.50662823884 + r * (-18.61500062529 + r * (41.39119773534 + r * -25.44106049637)))) /
630
+ (1 + r * (-8.4735109309 + r * (23.08336743743 + r * (-21.06224101826 + r * 3.13082909833))));
631
+
632
+ // Cornish-Fisher expansion for t-distribution
633
+ const g1 = (z ** 3 + z) / (4 * df);
634
+ const g2 = (5 * z ** 5 + 16 * z ** 3 + 3 * z) / (96 * df * df);
635
+ z = z + g1 + g2;
636
+
637
+ return Math.abs(z);
638
+ }
639
+
640
+ /**
641
+ * Approximate chi-squared p-value using Wilson-Hilferty.
642
+ */
643
+ function chiSquaredPValue(chiSq: number, df: number): number {
644
+ if (df <= 0) return 1;
645
+ if (chiSq <= 0) return 1;
646
+ // Wilson-Hilferty transformation to normal
647
+ const z = Math.pow(chiSq / df, 1 / 3) - (1 - 2 / (9 * df));
648
+ const denom = Math.sqrt(2 / (9 * df));
649
+ return 1 - normalCDF(z / denom);
650
+ }