Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit eaa647c

Browse files
author
Zalenix Gardener
committed
refactor(crossExperimentLearner): delegate stats helpers to shared ./stats module
This module shipped four hand-rolled copies of helpers that already live in docs/shared/stats.js — mean(), stddev(), linearRegression(), and four Math.min/max.apply(null, …) pairs. Two issues with the local copies: 1. Drift risk: stats.mean uses Kahan compensated summation and stats.linearRegression uses a single-pass O(n) algorithm; the local copies were the older, less stable two-pass implementations. Any future numerical fix to stats.js silently skipped this module. 2. Stack-overflow risk: Math.min/max.apply(null, arr) spreads the array as function arguments, which can throw RangeError on inputs > ~100k items (already noted in stats.minMax JSDoc). Changes: - Drop local mean/stddev/linearRegression definitions; import from ./stats. - Keep linearRegression as a thin wrapper that clamps r2 to [0, +inf): all downstream confidence/rationale code in this module assumes r2 ∈ [0, 1]. - Keep local percentile() unchanged — it takes a 0..1 fraction whereas stats.percentile takes 0..100; swapping would silently break GOLDEN_PERCENTILE / FAILURE_PERCENTILE thresholds. - Keep local pearsonCorrelation — its MIN_SAMPLES_FOR_CORRELATION gate is recommendation-engine policy, not a general stats primitive. - Replace 4 Math.min/max.apply pairs with cached minMax() calls (recommend, getDeltaProfile, analyze). Net diff: −37 / +20 lines. Verified: jest crossExperimentLearner → 43/43 tests pass; broader pattern run (8 suites, 296 tests) all green; node -c clean.
1 parent a3ccf74 commit eaa647c

1 file changed

Lines changed: 29 additions & 39 deletions

File tree

docs/shared/crossExperimentLearner.js

Lines changed: 29 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -53,23 +53,21 @@ var SENSITIVITY_CLASSES = [
5353

5454
// ── Statistical helpers ────────────────────────────────────────────
5555

56-
function mean(arr) {
57-
if (!arr || arr.length === 0) return 0;
58-
var sum = 0;
59-
for (var i = 0; i < arr.length; i++) sum += arr[i];
60-
return sum / arr.length;
61-
}
62-
63-
function stddev(arr) {
64-
if (!arr || arr.length < 2) return 0;
65-
var m = mean(arr);
66-
var sumSq = 0;
67-
for (var i = 0; i < arr.length; i++) {
68-
var d = arr[i] - m;
69-
sumSq += d * d;
70-
}
71-
return Math.sqrt(sumSq / (arr.length - 1));
72-
}
56+
// Hot-path numerical helpers delegate to the shared `./stats` module
57+
// (Kahan-compensated mean, single-pass linearRegression, single-pass minMax)
58+
// to avoid drift with the canonical implementations and inherit the
59+
// numerical-stability + stack-safety guarantees documented there.
60+
//
61+
// `pearsonCorrelation` keeps the MIN_SAMPLES_FOR_CORRELATION gate that is
62+
// specific to this module's recommendation semantics, and `percentile`
63+
// below keeps its 0..1 fractional API used pervasively in this file (the
64+
// shared `percentile` takes 0..100, so swapping would silently break thresholds).
65+
66+
var _stats = require('./stats');
67+
var mean = _stats.mean;
68+
var stddev = _stats.stddev;
69+
var _statsLinReg = _stats.linearRegression;
70+
var minMax = _stats.minMax;
7371

7472
function pearsonCorrelation(xs, ys) {
7573
if (!xs || !ys || xs.length !== ys.length || xs.length < MIN_SAMPLES_FOR_CORRELATION) return null;
@@ -99,26 +97,15 @@ function percentile(sortedArr, p) {
9997
return sortedArr[lo] * (1 - frac) + sortedArr[hi] * frac;
10098
}
10199

100+
// Thin wrapper that clamps r2 to [0, +inf): when the linear fit is
101+
// worse than predicting the mean (ssRes > ssTot) the shared regression
102+
// returns a negative r2, but all downstream confidence/rationale code
103+
// in this module assumes r2 in [0, 1] ("no explanatory power" floor).
102104
function linearRegression(xs, ys) {
103105
var n = xs.length;
104106
if (n < 2) return { slope: 0, intercept: 0, r2: 0 };
105-
var mx = mean(xs);
106-
var my = mean(ys);
107-
var num = 0, den = 0, ssTot = 0;
108-
for (var i = 0; i < n; i++) {
109-
num += (xs[i] - mx) * (ys[i] - my);
110-
den += (xs[i] - mx) * (xs[i] - mx);
111-
ssTot += (ys[i] - my) * (ys[i] - my);
112-
}
113-
var slope = den === 0 ? 0 : num / den;
114-
var intercept = my - slope * mx;
115-
var ssRes = 0;
116-
for (var i = 0; i < n; i++) {
117-
var pred = slope * xs[i] + intercept;
118-
ssRes += (ys[i] - pred) * (ys[i] - pred);
119-
}
120-
var r2 = ssTot === 0 ? 0 : 1 - ssRes / ssTot;
121-
return { slope: slope, intercept: intercept, r2: Math.max(0, r2) };
107+
var reg = _statsLinReg(xs, ys);
108+
return { slope: reg.slope, intercept: reg.intercept, r2: Math.max(0, reg.r2) };
122109
}
123110

124111
function getConfidenceLevel(score) {
@@ -378,7 +365,8 @@ function createCrossExperimentLearner(options) {
378365
if (typeof v === 'number' && isFinite(v)) allValues.push(v);
379366
}
380367
if (allValues.length < 2) return;
381-
var overallRange = Math.max.apply(null, allValues) - Math.min.apply(null, allValues);
368+
var _allMm = minMax(allValues);
369+
var overallRange = _allMm.max - _allMm.min;
382370
var goldenRange = golden.max - golden.min;
383371
var tightness = overallRange === 0 ? 1 : 1 - (goldenRange / overallRange);
384372
tightness = Math.max(0, Math.min(1, tightness));
@@ -456,8 +444,9 @@ function createCrossExperimentLearner(options) {
456444
zScore: Math.round(zScore * 100) / 100,
457445
direction: zScore > 0 ? 'too_high' : 'too_low',
458446
dangerZone: {
459-
min: zScore < 0 ? Math.min.apply(null, failValues) : failMean - stddev(failValues),
460-
max: zScore > 0 ? Math.max.apply(null, failValues) : failMean + stddev(failValues)
447+
// Single-pass min/max — no Math.*.apply stack risk on large failValues
448+
min: zScore < 0 ? minMax(failValues).min : failMean - stddev(failValues),
449+
max: zScore > 0 ? minMax(failValues).max : failMean + stddev(failValues)
461450
},
462451
failureCount: failValues.length,
463452
severity: Math.abs(zScore) > 2 ? 'CRITICAL' : Math.abs(zScore) > 1.5 ? 'HIGH' : 'MODERATE'
@@ -542,8 +531,9 @@ function createCrossExperimentLearner(options) {
542531
// Solve for x: targetValue = slope*x + intercept
543532
suggestedValue = (targetValue - reg.intercept) / reg.slope;
544533
// Clamp to observed range ±20%
545-
var minObs = Math.min.apply(null, xs);
546-
var maxObs = Math.max.apply(null, xs);
534+
var _xsMm = minMax(xs);
535+
var minObs = _xsMm.min;
536+
var maxObs = _xsMm.max;
547537
var rangeBuffer = (maxObs - minObs) * 0.2;
548538
suggestedValue = Math.max(minObs - rangeBuffer, Math.min(maxObs + rangeBuffer, suggestedValue));
549539
} else {

0 commit comments

Comments
 (0)