'use strict'; /** * THE GATE — Kev's spec, verified rather than asserted. * * `tests/unit/supplementSystems.test.js` checks the same thresholds but inlines * its own `validateFactor`, so it could never have caught the absence of an * implementation. These tests import the real module, and the p-value is checked * against values computable by hand rather than trusted. */ const cv = require('../../src/services/model/correlateValidator'); /** Deterministic generator: y correlated with x at roughly the requested level. */ function correlated(n, rho, seed = 7) { let s = seed >>> 0; const rnd = () => { s ^= s << 13; s >>>= 0; s ^= s >>> 17; s ^= s << 5; s >>>= 0; return s / 4294967296; }; const gauss = () => { const u = Math.max(1e-12, rnd()); return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * rnd()); }; const xs = []; const ys = []; for (let i = 0; i < n; i += 1) { const x = gauss(); xs.push(x); ys.push(rho * x + Math.sqrt(Math.max(0, 1 - rho * rho)) * gauss()); } return { xs, ys }; } describe('the spec is exactly Kev\'s', () => { it('n>=500, |r|>=0.15, p<0.05, Bonferroni on', () => { expect(cv.VALIDATION_REQUIREMENTS).toEqual({ min_historical_instances: 500, min_pearson_r: 0.15, max_p_value: 0.05, bonferroni_correction: true, }); }); }); describe('Pearson r', () => { it('is exactly 1 and -1 on perfect lines', () => { expect(cv.pearson([1, 2, 3, 4], [2, 4, 6, 8]).r).toBeCloseTo(1, 12); expect(cv.pearson([1, 2, 3, 4], [8, 6, 4, 2]).r).toBeCloseTo(-1, 12); }); it('matches a hand-computable case', () => { // x=[1,2,3,4,5], y=[2,4,5,4,5]; means 3 and 4. // Sxy = 4+0+0+0+2 = 6 · Sxx = 10 · Syy = 6 → r = 6/sqrt(60) = 0.774597 expect(cv.pearson([1, 2, 3, 4, 5], [2, 4, 5, 4, 5]).r).toBeCloseTo(0.774597, 6); }); it('DROPS a pair with an unknown side — never zero-fills it', () => { const clean = cv.pearson([1, 2, 3, 4], [2, 4, 6, 8]); const withNull = cv.pearson([1, 2, 3, 4, 5], [2, 4, 6, 8, null]); expect(withNull.n).toBe(4); expect(withNull.r).toBeCloseTo(clean.r, 12); // Zero-filling would have added a (5,0) point and wrecked it. }); it('a constant series has no correlation, not a zero one', () => { expect(cv.pearson([1, 1, 1, 1], [1, 2, 3, 4]).r).toBeNull(); }); }); describe('the p-value is real', () => { it('matches known two-sided values for r and n', () => { // r=0.5, n=10 -> t=1.6330, df=8 -> p ≈ 0.1411 expect(cv.pearsonPValue(0.5, 10)).toBeCloseTo(0.1411, 3); // r=0.8, n=10 -> t=3.7712, df=8 -> p ≈ 0.00546 expect(cv.pearsonPValue(0.8, 10)).toBeCloseTo(0.00546, 4); // r=0.15, n=500 -> p ≈ 0.000759 expect(cv.pearsonPValue(0.15, 500)).toBeCloseTo(0.00076, 4); }); it('r = 0 is p = 1, and the sign of r does not change p', () => { expect(cv.pearsonPValue(0, 500)).toBeCloseTo(1, 6); expect(cv.pearsonPValue(-0.3, 200)).toBeCloseTo(cv.pearsonPValue(0.3, 200), 12); }); it('is UNDEFINED (null) below 3 points, never a comforting 1.0', () => { expect(cv.pearsonPValue(0.9, 2)).toBeNull(); }); it('the regularized incomplete beta is symmetric where it must be', () => { expect(cv.incompleteBeta(2, 3, 0.5) + cv.incompleteBeta(3, 2, 0.5)).toBeCloseTo(1, 10); }); }); describe('the gate', () => { it('REFUSES below n=500 no matter how strong the correlation', () => { const { xs, ys } = correlated(300, 0.9); const v = cv.validateFactor(xs, ys, 1); expect(v.validated).toBe(false); expect(v.reason).toBe('insufficient_data'); expect(v.sample_size).toBe(300); }); it('REFUSES a weak correlation even at huge n and tiny p', () => { const { xs, ys } = correlated(4000, 0.08); const v = cv.validateFactor(xs, ys, 1); expect(Math.abs(v.pearson_r)).toBeLessThan(0.15); expect(v.validated).toBe(false); expect(v.reason).toBe('weak_correlation'); }); it('PASSES a real effect at sufficient n', () => { const { xs, ys } = correlated(600, 0.30); const v = cv.validateFactor(xs, ys, 1); expect(v.validated).toBe(true); expect(Math.abs(v.pearson_r)).toBeGreaterThanOrEqual(0.15); expect(v.p_value).toBeLessThan(0.05); }); it('BONFERRONI actually bites — the same data can pass alone and fail in a sweep', () => { // Chosen so p sits between 0.05/40 and 0.05: significant alone, not in a sweep. const alone = cv.validateFactor(...Object.values(correlated(520, 0.155)), 1); const sweep = cv.validateFactor(...Object.values(correlated(520, 0.155)), 40); expect(sweep.corrected_alpha).toBeCloseTo(0.05 / 40, 12); expect(sweep.bonferroni_tests).toBe(40); // Whatever the draw, the corrected bar is strictly harder. expect(sweep.corrected_alpha).toBeLessThan(alone.corrected_alpha); if (alone.validated && alone.p_value > 0.05 / 40) expect(sweep.validated).toBe(false); }); it('reports corrected alpha = 0.05/tests, and 0.05 with one test', () => { const { xs, ys } = correlated(600, 0.3); expect(cv.validateFactor(xs, ys, 4).corrected_alpha).toBeCloseTo(0.0125, 12); expect(cv.validateFactor(xs, ys, 1).corrected_alpha).toBeCloseTo(0.05, 12); }); it('always says WHY it failed', () => { const weak = cv.validateFactor(...Object.values(correlated(600, 0.02)), 1); expect(weak.reason).toBeTruthy(); expect(weak.thresholds).toEqual({ min_n: 500, min_abs_r: 0.15, base_alpha: 0.05 }); }); });