@kernel.chat/kbot 3.42.0 → 3.43.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.
- package/dist/auth.d.ts +5 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/cli.js +36 -3
- package/dist/cli.js.map +1 -1
- package/dist/completions.d.ts.map +1 -1
- package/dist/completions.js +7 -0
- package/dist/completions.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +132 -92
- package/dist/doctor.js.map +1 -1
- package/dist/doctor.test.d.ts +2 -0
- package/dist/doctor.test.d.ts.map +1 -0
- package/dist/doctor.test.js +432 -0
- package/dist/doctor.test.js.map +1 -0
- package/dist/tools/hypothesis-engine.d.ts +2 -0
- package/dist/tools/hypothesis-engine.d.ts.map +1 -0
- package/dist/tools/hypothesis-engine.js +2276 -0
- package/dist/tools/hypothesis-engine.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/research-notebook.d.ts +2 -0
- package/dist/tools/research-notebook.d.ts.map +1 -0
- package/dist/tools/research-notebook.js +1165 -0
- package/dist/tools/research-notebook.js.map +1 -0
- package/dist/tools/research-pipeline.d.ts +2 -0
- package/dist/tools/research-pipeline.d.ts.map +1 -0
- package/dist/tools/research-pipeline.js +1094 -0
- package/dist/tools/research-pipeline.js.map +1 -0
- package/dist/tools/science-graph.d.ts +2 -0
- package/dist/tools/science-graph.d.ts.map +1 -0
- package/dist/tools/science-graph.js +995 -0
- package/dist/tools/science-graph.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2276 @@
|
|
|
1
|
+
// kbot Hypothesis Generation Engine — Scientific hypothesis tools
|
|
2
|
+
// Helps researchers form, test, and evaluate scientific hypotheses.
|
|
3
|
+
// All computations are self-contained — no external dependencies.
|
|
4
|
+
import { registerTool } from './index.js';
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Shared math utilities
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
/** Simple seeded PRNG (xoshiro128**) for reproducible Monte Carlo */
|
|
9
|
+
class PRNG {
|
|
10
|
+
s;
|
|
11
|
+
constructor(seed) {
|
|
12
|
+
this.s = new Uint32Array(4);
|
|
13
|
+
this.s[0] = seed >>> 0;
|
|
14
|
+
this.s[1] = (seed * 1812433253 + 1) >>> 0;
|
|
15
|
+
this.s[2] = (this.s[1] * 1812433253 + 1) >>> 0;
|
|
16
|
+
this.s[3] = (this.s[2] * 1812433253 + 1) >>> 0;
|
|
17
|
+
}
|
|
18
|
+
rotl(x, k) {
|
|
19
|
+
return ((x << k) | (x >>> (32 - k))) >>> 0;
|
|
20
|
+
}
|
|
21
|
+
nextU32() {
|
|
22
|
+
const result = (this.rotl((this.s[1] * 5) >>> 0, 7) * 9) >>> 0;
|
|
23
|
+
const t = (this.s[1] << 9) >>> 0;
|
|
24
|
+
this.s[2] ^= this.s[0];
|
|
25
|
+
this.s[3] ^= this.s[1];
|
|
26
|
+
this.s[1] ^= this.s[2];
|
|
27
|
+
this.s[0] ^= this.s[3];
|
|
28
|
+
this.s[2] ^= t;
|
|
29
|
+
this.s[3] = this.rotl(this.s[3], 11);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
/** Uniform [0, 1) */
|
|
33
|
+
random() {
|
|
34
|
+
return this.nextU32() / 4294967296;
|
|
35
|
+
}
|
|
36
|
+
/** Standard normal via Box-Muller */
|
|
37
|
+
normal(mean = 0, sd = 1) {
|
|
38
|
+
const u1 = this.random() || 1e-10;
|
|
39
|
+
const u2 = this.random();
|
|
40
|
+
return mean + sd * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function mean(arr) {
|
|
44
|
+
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
45
|
+
}
|
|
46
|
+
function variance(arr, m) {
|
|
47
|
+
const mu = m ?? mean(arr);
|
|
48
|
+
return arr.reduce((s, x) => s + (x - mu) ** 2, 0) / (arr.length - 1);
|
|
49
|
+
}
|
|
50
|
+
function stddev(arr, m) {
|
|
51
|
+
return Math.sqrt(variance(arr, m));
|
|
52
|
+
}
|
|
53
|
+
function median(arr) {
|
|
54
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
55
|
+
const mid = Math.floor(sorted.length / 2);
|
|
56
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
57
|
+
}
|
|
58
|
+
function quantile(sorted, q) {
|
|
59
|
+
const pos = (sorted.length - 1) * q;
|
|
60
|
+
const lo = Math.floor(pos);
|
|
61
|
+
const hi = Math.ceil(pos);
|
|
62
|
+
if (lo === hi)
|
|
63
|
+
return sorted[lo];
|
|
64
|
+
return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo);
|
|
65
|
+
}
|
|
66
|
+
function pearsonCorrelation(x, y) {
|
|
67
|
+
const n = Math.min(x.length, y.length);
|
|
68
|
+
if (n < 2)
|
|
69
|
+
return 0;
|
|
70
|
+
const mx = mean(x.slice(0, n));
|
|
71
|
+
const my = mean(y.slice(0, n));
|
|
72
|
+
let num = 0, dx2 = 0, dy2 = 0;
|
|
73
|
+
for (let i = 0; i < n; i++) {
|
|
74
|
+
const dx = x[i] - mx;
|
|
75
|
+
const dy = y[i] - my;
|
|
76
|
+
num += dx * dy;
|
|
77
|
+
dx2 += dx * dx;
|
|
78
|
+
dy2 += dy * dy;
|
|
79
|
+
}
|
|
80
|
+
const denom = Math.sqrt(dx2 * dy2);
|
|
81
|
+
return denom === 0 ? 0 : num / denom;
|
|
82
|
+
}
|
|
83
|
+
/** Normal CDF approximation (Abramowitz and Stegun) */
|
|
84
|
+
function normalCDF(z) {
|
|
85
|
+
if (z < -8)
|
|
86
|
+
return 0;
|
|
87
|
+
if (z > 8)
|
|
88
|
+
return 1;
|
|
89
|
+
const a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741;
|
|
90
|
+
const a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911;
|
|
91
|
+
const sign = z < 0 ? -1 : 1;
|
|
92
|
+
const x = Math.abs(z) / Math.SQRT2;
|
|
93
|
+
const t = 1 / (1 + p * x);
|
|
94
|
+
const erf = 1 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
|
|
95
|
+
return 0.5 * (1 + sign * erf);
|
|
96
|
+
}
|
|
97
|
+
/** Inverse normal CDF (rational approximation) */
|
|
98
|
+
function normalInvCDF(p) {
|
|
99
|
+
if (p <= 0)
|
|
100
|
+
return -Infinity;
|
|
101
|
+
if (p >= 1)
|
|
102
|
+
return Infinity;
|
|
103
|
+
if (p === 0.5)
|
|
104
|
+
return 0;
|
|
105
|
+
const a = [
|
|
106
|
+
-3.969683028665376e+01, 2.209460984245205e+02,
|
|
107
|
+
-2.759285104469687e+02, 1.383577518672690e+02,
|
|
108
|
+
-3.066479806614716e+01, 2.506628277459239e+00,
|
|
109
|
+
];
|
|
110
|
+
const b = [
|
|
111
|
+
-5.447609879822406e+01, 1.615858368580409e+02,
|
|
112
|
+
-1.556989798598866e+02, 6.680131188771972e+01,
|
|
113
|
+
-1.328068155288572e+01,
|
|
114
|
+
];
|
|
115
|
+
const c = [
|
|
116
|
+
-7.784894002430293e-03, -3.223964580411365e-01,
|
|
117
|
+
-2.400758277161838e+00, -2.549732539343734e+00,
|
|
118
|
+
4.374664141464968e+00, 2.938163982698783e+00,
|
|
119
|
+
];
|
|
120
|
+
const d = [
|
|
121
|
+
7.784695709041462e-03, 3.224671290700398e-01,
|
|
122
|
+
2.445134137142996e+00, 3.754408661907416e+00,
|
|
123
|
+
];
|
|
124
|
+
const pLow = 0.02425, pHigh = 1 - pLow;
|
|
125
|
+
let q, r;
|
|
126
|
+
if (p < pLow) {
|
|
127
|
+
q = Math.sqrt(-2 * Math.log(p));
|
|
128
|
+
return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
|
|
129
|
+
((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);
|
|
130
|
+
}
|
|
131
|
+
else if (p <= pHigh) {
|
|
132
|
+
q = p - 0.5;
|
|
133
|
+
r = q * q;
|
|
134
|
+
return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q /
|
|
135
|
+
(((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
q = Math.sqrt(-2 * Math.log(1 - p));
|
|
139
|
+
return -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
|
|
140
|
+
((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Student's t CDF approximation for df >= 1 */
|
|
144
|
+
function tCDF(t, df) {
|
|
145
|
+
// Use normal approximation for large df
|
|
146
|
+
if (df > 100)
|
|
147
|
+
return normalCDF(t);
|
|
148
|
+
const x = df / (df + t * t);
|
|
149
|
+
// Regularized incomplete beta function approximation
|
|
150
|
+
const a = df / 2, b = 0.5;
|
|
151
|
+
let result = incompleteBeta(x, a, b);
|
|
152
|
+
if (t < 0)
|
|
153
|
+
return result / 2;
|
|
154
|
+
return 1 - result / 2;
|
|
155
|
+
}
|
|
156
|
+
/** Regularized incomplete beta function (continued fraction, Lentz) */
|
|
157
|
+
function incompleteBeta(x, a, b) {
|
|
158
|
+
if (x <= 0)
|
|
159
|
+
return 0;
|
|
160
|
+
if (x >= 1)
|
|
161
|
+
return 1;
|
|
162
|
+
const lbeta = logGamma(a) + logGamma(b) - logGamma(a + b);
|
|
163
|
+
const front = Math.exp(Math.log(x) * a + Math.log(1 - x) * b - lbeta) / a;
|
|
164
|
+
// Lentz continued fraction
|
|
165
|
+
let f = 1, c = 1, d = 1 - (a + b) * x / (a + 1);
|
|
166
|
+
if (Math.abs(d) < 1e-30)
|
|
167
|
+
d = 1e-30;
|
|
168
|
+
d = 1 / d;
|
|
169
|
+
f = d;
|
|
170
|
+
for (let m = 1; m <= 200; m++) {
|
|
171
|
+
let num = m * (b - m) * x / ((a + 2 * m - 1) * (a + 2 * m));
|
|
172
|
+
d = 1 + num * d;
|
|
173
|
+
if (Math.abs(d) < 1e-30)
|
|
174
|
+
d = 1e-30;
|
|
175
|
+
d = 1 / d;
|
|
176
|
+
c = 1 + num / c;
|
|
177
|
+
if (Math.abs(c) < 1e-30)
|
|
178
|
+
c = 1e-30;
|
|
179
|
+
f *= d * c;
|
|
180
|
+
num = -(a + m) * (a + b + m) * x / ((a + 2 * m) * (a + 2 * m + 1));
|
|
181
|
+
d = 1 + num * d;
|
|
182
|
+
if (Math.abs(d) < 1e-30)
|
|
183
|
+
d = 1e-30;
|
|
184
|
+
d = 1 / d;
|
|
185
|
+
c = 1 + num / c;
|
|
186
|
+
if (Math.abs(c) < 1e-30)
|
|
187
|
+
c = 1e-30;
|
|
188
|
+
const delta = d * c;
|
|
189
|
+
f *= delta;
|
|
190
|
+
if (Math.abs(delta - 1) < 1e-10)
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
return front * f;
|
|
194
|
+
}
|
|
195
|
+
/** Log-gamma via Lanczos approximation */
|
|
196
|
+
function logGamma(z) {
|
|
197
|
+
if (z < 0.5)
|
|
198
|
+
return Math.log(Math.PI / Math.sin(Math.PI * z)) - logGamma(1 - z);
|
|
199
|
+
z -= 1;
|
|
200
|
+
const g = 7;
|
|
201
|
+
const coef = [
|
|
202
|
+
0.99999999999980993, 676.5203681218851, -1259.1392167224028,
|
|
203
|
+
771.32342877765313, -176.61502916214059, 12.507343278686905,
|
|
204
|
+
-0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7,
|
|
205
|
+
];
|
|
206
|
+
let x = coef[0];
|
|
207
|
+
for (let i = 1; i < g + 2; i++)
|
|
208
|
+
x += coef[i] / (z + i);
|
|
209
|
+
const t = z + g + 0.5;
|
|
210
|
+
return 0.5 * Math.log(2 * Math.PI) + (z + 0.5) * Math.log(t) - t + Math.log(x);
|
|
211
|
+
}
|
|
212
|
+
/** Chi-squared CDF (regularized lower incomplete gamma) */
|
|
213
|
+
function chiSquaredCDF(x, k) {
|
|
214
|
+
if (x <= 0)
|
|
215
|
+
return 0;
|
|
216
|
+
return regularizedGammaP(k / 2, x / 2);
|
|
217
|
+
}
|
|
218
|
+
/** Regularized lower incomplete gamma P(a, x) via series expansion */
|
|
219
|
+
function regularizedGammaP(a, x) {
|
|
220
|
+
if (x < 0)
|
|
221
|
+
return 0;
|
|
222
|
+
if (x === 0)
|
|
223
|
+
return 0;
|
|
224
|
+
const lg = logGamma(a);
|
|
225
|
+
// Use series for x < a + 1
|
|
226
|
+
if (x < a + 1) {
|
|
227
|
+
let sum = 1 / a, term = 1 / a;
|
|
228
|
+
for (let n = 1; n < 200; n++) {
|
|
229
|
+
term *= x / (a + n);
|
|
230
|
+
sum += term;
|
|
231
|
+
if (Math.abs(term) < 1e-12 * Math.abs(sum))
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
return sum * Math.exp(-x + a * Math.log(x) - lg);
|
|
235
|
+
}
|
|
236
|
+
// Use continued fraction for x >= a + 1
|
|
237
|
+
return 1 - regularizedGammaQ(a, x);
|
|
238
|
+
}
|
|
239
|
+
/** Regularized upper incomplete gamma Q(a, x) via continued fraction */
|
|
240
|
+
function regularizedGammaQ(a, x) {
|
|
241
|
+
const lg = logGamma(a);
|
|
242
|
+
let f = 1, c = 1, d = x + 1 - a;
|
|
243
|
+
if (Math.abs(d) < 1e-30)
|
|
244
|
+
d = 1e-30;
|
|
245
|
+
d = 1 / d;
|
|
246
|
+
f = d;
|
|
247
|
+
for (let i = 1; i <= 200; i++) {
|
|
248
|
+
const an = -i * (i - a);
|
|
249
|
+
const bn = x + 2 * i + 1 - a;
|
|
250
|
+
d = bn + an * d;
|
|
251
|
+
if (Math.abs(d) < 1e-30)
|
|
252
|
+
d = 1e-30;
|
|
253
|
+
d = 1 / d;
|
|
254
|
+
c = bn + an / c;
|
|
255
|
+
if (Math.abs(c) < 1e-30)
|
|
256
|
+
c = 1e-30;
|
|
257
|
+
const delta = d * c;
|
|
258
|
+
f *= delta;
|
|
259
|
+
if (Math.abs(delta - 1) < 1e-10)
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
return f * Math.exp(-x + a * Math.log(x) - lg);
|
|
263
|
+
}
|
|
264
|
+
/** Parse flexible numeric data: comma-separated or JSON array */
|
|
265
|
+
function parseNumericData(input) {
|
|
266
|
+
const trimmed = input.trim();
|
|
267
|
+
if (trimmed.startsWith('[')) {
|
|
268
|
+
try {
|
|
269
|
+
const parsed = JSON.parse(trimmed);
|
|
270
|
+
if (Array.isArray(parsed))
|
|
271
|
+
return parsed.map(Number).filter(n => !isNaN(n));
|
|
272
|
+
}
|
|
273
|
+
catch { /* fall through */ }
|
|
274
|
+
}
|
|
275
|
+
return trimmed.split(',').map(s => parseFloat(s.trim())).filter(n => !isNaN(n));
|
|
276
|
+
}
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// Tool registration
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
export function registerHypothesisEngineTools() {
|
|
281
|
+
// =========================================================================
|
|
282
|
+
// 1. hypothesis_generate
|
|
283
|
+
// =========================================================================
|
|
284
|
+
registerTool({
|
|
285
|
+
name: 'hypothesis_generate',
|
|
286
|
+
description: 'Generate testable hypotheses from observations or data. Produces 3-5 ranked hypotheses with null hypotheses, predicted outcomes, suggested experimental approaches, required tools, and confidence levels.',
|
|
287
|
+
parameters: {
|
|
288
|
+
observation: {
|
|
289
|
+
type: 'string',
|
|
290
|
+
description: 'Describe what you observed or the data pattern you noticed',
|
|
291
|
+
required: true,
|
|
292
|
+
},
|
|
293
|
+
field: {
|
|
294
|
+
type: 'string',
|
|
295
|
+
description: 'Scientific field (e.g. biology, physics, psychology, economics, computer_science)',
|
|
296
|
+
required: true,
|
|
297
|
+
},
|
|
298
|
+
context: {
|
|
299
|
+
type: 'string',
|
|
300
|
+
description: 'Any prior knowledge, literature references, or constraints (optional)',
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
tier: 'free',
|
|
304
|
+
async execute(args) {
|
|
305
|
+
const observation = String(args.observation);
|
|
306
|
+
const field = String(args.field).toLowerCase();
|
|
307
|
+
const context = args.context ? String(args.context) : '';
|
|
308
|
+
// Field-specific experimental templates
|
|
309
|
+
const fieldTemplates = {
|
|
310
|
+
biology: {
|
|
311
|
+
methods: ['controlled experiment', 'longitudinal study', 'gene knockout', 'comparative analysis', 'field observation'],
|
|
312
|
+
tools: ['microscopy', 'PCR', 'sequencing', 'cell culture', 'flow cytometry', 'Western blot'],
|
|
313
|
+
designs: ['randomized controlled trial', 'case-control study', 'cohort study'],
|
|
314
|
+
variables: ['gene expression', 'protein levels', 'cell viability', 'growth rate', 'survival rate'],
|
|
315
|
+
},
|
|
316
|
+
physics: {
|
|
317
|
+
methods: ['controlled experiment', 'simulation', 'measurement', 'theoretical derivation', 'interferometry'],
|
|
318
|
+
tools: ['oscilloscope', 'spectrometer', 'particle detector', 'laser', 'calorimeter', 'simulation software'],
|
|
319
|
+
designs: ['repeated measures', 'factorial design', 'parameter sweep'],
|
|
320
|
+
variables: ['energy', 'force', 'wavelength', 'temperature', 'velocity', 'field strength'],
|
|
321
|
+
},
|
|
322
|
+
psychology: {
|
|
323
|
+
methods: ['randomized controlled trial', 'survey', 'longitudinal study', 'cross-sectional study', 'meta-analysis'],
|
|
324
|
+
tools: ['psychometric scales', 'fMRI', 'EEG', 'eye-tracking', 'reaction time software', 'questionnaires'],
|
|
325
|
+
designs: ['between-subjects', 'within-subjects', 'mixed design', 'crossover'],
|
|
326
|
+
variables: ['response time', 'accuracy', 'self-report scores', 'cortisol levels', 'neural activation'],
|
|
327
|
+
},
|
|
328
|
+
economics: {
|
|
329
|
+
methods: ['natural experiment', 'regression analysis', 'difference-in-differences', 'instrumental variables', 'simulation'],
|
|
330
|
+
tools: ['econometric software (R/Stata)', 'panel data', 'survey instruments', 'administrative data'],
|
|
331
|
+
designs: ['quasi-experimental', 'randomized controlled trial', 'event study'],
|
|
332
|
+
variables: ['GDP', 'employment rate', 'price index', 'Gini coefficient', 'trade volume'],
|
|
333
|
+
},
|
|
334
|
+
computer_science: {
|
|
335
|
+
methods: ['A/B testing', 'benchmarking', 'ablation study', 'formal verification', 'simulation'],
|
|
336
|
+
tools: ['profiler', 'benchmark suite', 'GPU cluster', 'statistical analysis', 'version control'],
|
|
337
|
+
designs: ['factorial design', 'repeated measures', 'cross-validation'],
|
|
338
|
+
variables: ['latency', 'throughput', 'accuracy', 'F1 score', 'memory usage', 'convergence rate'],
|
|
339
|
+
},
|
|
340
|
+
chemistry: {
|
|
341
|
+
methods: ['titration', 'spectroscopic analysis', 'kinetics study', 'synthesis', 'computational chemistry'],
|
|
342
|
+
tools: ['NMR', 'mass spectrometry', 'IR spectroscopy', 'HPLC', 'X-ray crystallography'],
|
|
343
|
+
designs: ['factorial design', 'response surface methodology', 'sequential design'],
|
|
344
|
+
variables: ['yield', 'reaction rate', 'concentration', 'purity', 'binding affinity'],
|
|
345
|
+
},
|
|
346
|
+
medicine: {
|
|
347
|
+
methods: ['randomized controlled trial', 'cohort study', 'case-control study', 'meta-analysis', 'clinical observation'],
|
|
348
|
+
tools: ['clinical assessment tools', 'imaging (MRI/CT)', 'blood work', 'biopsy', 'patient records'],
|
|
349
|
+
designs: ['double-blind RCT', 'crossover trial', 'parallel group'],
|
|
350
|
+
variables: ['survival rate', 'symptom severity', 'biomarker levels', 'quality of life score', 'adverse events'],
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
const template = fieldTemplates[field] || fieldTemplates['biology'];
|
|
354
|
+
// Generate hypotheses based on observation structure
|
|
355
|
+
const keywords = observation.toLowerCase();
|
|
356
|
+
const isCorrelational = /correlat|associat|relat|link|connect/i.test(keywords);
|
|
357
|
+
const isCausal = /caus|effect|lead|result|impact|increas|decreas/i.test(keywords);
|
|
358
|
+
const isComparative = /differ|compar|more|less|higher|lower|better|worse/i.test(keywords);
|
|
359
|
+
const isTemporal = /time|trend|chang|over\s+time|increas|decreas|grow|decline/i.test(keywords);
|
|
360
|
+
const isMechanistic = /mechanism|how|why|process|pathway|mediator/i.test(keywords);
|
|
361
|
+
const hypotheses = [];
|
|
362
|
+
// H1: Direct causal hypothesis
|
|
363
|
+
hypotheses.push({
|
|
364
|
+
statement: isCausal
|
|
365
|
+
? `The observed ${observation.slice(0, 80)} is caused by a direct mechanistic relationship that can be isolated through ${template.methods[0]}.`
|
|
366
|
+
: `The phenomenon described ("${observation.slice(0, 60)}...") reflects a direct causal mechanism in ${field}.`,
|
|
367
|
+
null_hypothesis: 'There is no causal relationship; observed patterns are due to chance or confounding variables.',
|
|
368
|
+
predicted_outcome: `Controlled manipulation of the proposed cause will produce a measurable change in ${template.variables[0]} (effect size d >= 0.3).`,
|
|
369
|
+
experimental_approach: `${template.designs[0]} with ${template.methods[0]}. Manipulate the independent variable while controlling for confounders. Sample size: power analysis recommended (n >= 30 per group).`,
|
|
370
|
+
required_tools: [template.tools[0], template.tools[1], 'statistical analysis software'],
|
|
371
|
+
confidence: 'Moderate',
|
|
372
|
+
confidence_pct: 55,
|
|
373
|
+
});
|
|
374
|
+
// H2: Correlational / mediator hypothesis
|
|
375
|
+
hypotheses.push({
|
|
376
|
+
statement: isCorrelational
|
|
377
|
+
? `The correlation described in the observation is mediated by an intermediate variable (${template.variables[2] || 'unmeasured mediator'}).`
|
|
378
|
+
: `A correlational relationship exists between the key variables, mediated by ${template.variables[2] || 'an intermediate factor'}.`,
|
|
379
|
+
null_hypothesis: `No mediating variable explains the observed association; any apparent mediation is artifactual.`,
|
|
380
|
+
predicted_outcome: `Structural equation modeling or mediation analysis will show significant indirect effects (p < 0.05) through the proposed mediator.`,
|
|
381
|
+
experimental_approach: `${template.designs[1] || template.designs[0]} measuring the proposed mediator alongside primary variables. Use Sobel test or bootstrap mediation analysis.`,
|
|
382
|
+
required_tools: [template.tools[2] || template.tools[0], 'SEM software (lavaan/AMOS)', 'mediation analysis package'],
|
|
383
|
+
confidence: 'Moderate-Low',
|
|
384
|
+
confidence_pct: 40,
|
|
385
|
+
});
|
|
386
|
+
// H3: Alternative mechanism hypothesis
|
|
387
|
+
hypotheses.push({
|
|
388
|
+
statement: isMechanistic
|
|
389
|
+
? `The mechanism underlying the observation involves ${template.variables[1]} as a primary driver rather than the most obvious candidate.`
|
|
390
|
+
: `An alternative mechanism (involving ${template.variables[1]}) better explains the observation than the default assumption.`,
|
|
391
|
+
null_hypothesis: `The conventional/default mechanism is sufficient; ${template.variables[1]} plays no independent role.`,
|
|
392
|
+
predicted_outcome: `Blocking or removing the alternative mechanism while preserving the default pathway will eliminate the observed effect.`,
|
|
393
|
+
experimental_approach: `${template.methods[2] || template.methods[1]} with selective intervention. Compare full model vs. reduced model.`,
|
|
394
|
+
required_tools: template.tools.slice(1, 4),
|
|
395
|
+
confidence: 'Low-Moderate',
|
|
396
|
+
confidence_pct: 35,
|
|
397
|
+
});
|
|
398
|
+
// H4: Temporal / dose-response hypothesis
|
|
399
|
+
if (isTemporal || hypotheses.length < 4) {
|
|
400
|
+
hypotheses.push({
|
|
401
|
+
statement: `The observed effect follows a dose-response or temporal relationship — increasing the independent variable magnitude or exposure duration will proportionally increase the effect on ${template.variables[0]}.`,
|
|
402
|
+
null_hypothesis: `No dose-response relationship exists; the effect is binary (present/absent) or random.`,
|
|
403
|
+
predicted_outcome: `Regression analysis will show a significant linear or log-linear relationship (R² > 0.3) between dose/time and outcome.`,
|
|
404
|
+
experimental_approach: `Graded exposure design with ${template.methods[0]}. Test at least 4 dose levels with sufficient replication (n >= 10 per level).`,
|
|
405
|
+
required_tools: [template.tools[0], 'dose-response modeling software', 'regression analysis'],
|
|
406
|
+
confidence: 'Moderate',
|
|
407
|
+
confidence_pct: 50,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
// H5: Null / artifact hypothesis
|
|
411
|
+
hypotheses.push({
|
|
412
|
+
statement: `The observed pattern is an artifact of ${isComparative ? 'selection bias or non-random sampling' : 'measurement error, confounding, or statistical noise'} rather than a genuine effect.`,
|
|
413
|
+
null_hypothesis: `(This IS the null hypothesis — the observed pattern is real and replicable.)`,
|
|
414
|
+
predicted_outcome: `Replication with improved methodology (larger sample, better controls, blinding) will fail to reproduce the effect (p > 0.05).`,
|
|
415
|
+
experimental_approach: `Pre-registered replication study with ${template.designs[0]}. Address potential confounders identified in original observation. Use power analysis to ensure adequate sample size.`,
|
|
416
|
+
required_tools: ['pre-registration platform (OSF/AsPredicted)', 'power analysis tool (G*Power)', template.tools[0]],
|
|
417
|
+
confidence: 'Should always be considered',
|
|
418
|
+
confidence_pct: 25,
|
|
419
|
+
});
|
|
420
|
+
// Sort by confidence (descending)
|
|
421
|
+
hypotheses.sort((a, b) => b.confidence_pct - a.confidence_pct);
|
|
422
|
+
// Build output
|
|
423
|
+
const lines = [
|
|
424
|
+
`# Hypothesis Generation Report`,
|
|
425
|
+
'',
|
|
426
|
+
`**Field**: ${field}`,
|
|
427
|
+
`**Observation**: ${observation}`,
|
|
428
|
+
context ? `**Prior Context**: ${context}` : '',
|
|
429
|
+
'',
|
|
430
|
+
`---`,
|
|
431
|
+
'',
|
|
432
|
+
`## Generated Hypotheses (${hypotheses.length})`,
|
|
433
|
+
'',
|
|
434
|
+
];
|
|
435
|
+
hypotheses.forEach((h, i) => {
|
|
436
|
+
lines.push(`### H${i + 1}: ${h.statement}`);
|
|
437
|
+
lines.push('');
|
|
438
|
+
lines.push(`| Attribute | Details |`);
|
|
439
|
+
lines.push(`|-----------|---------|`);
|
|
440
|
+
lines.push(`| **Null Hypothesis** | ${h.null_hypothesis} |`);
|
|
441
|
+
lines.push(`| **Predicted Outcome** | ${h.predicted_outcome} |`);
|
|
442
|
+
lines.push(`| **Experimental Approach** | ${h.experimental_approach} |`);
|
|
443
|
+
lines.push(`| **Required Tools** | ${h.required_tools.join(', ')} |`);
|
|
444
|
+
lines.push(`| **Confidence** | ${h.confidence} (${h.confidence_pct}%) |`);
|
|
445
|
+
lines.push('');
|
|
446
|
+
});
|
|
447
|
+
lines.push(`---`);
|
|
448
|
+
lines.push('');
|
|
449
|
+
lines.push(`## Recommendations`);
|
|
450
|
+
lines.push('');
|
|
451
|
+
lines.push(`1. **Start with H1** (highest confidence) — design a focused experiment to test the primary hypothesis.`);
|
|
452
|
+
lines.push(`2. **Always consider H${hypotheses.length}** (artifact hypothesis) — rule out confounders before claiming a discovery.`);
|
|
453
|
+
lines.push(`3. **Pre-register** your study design before data collection to increase credibility.`);
|
|
454
|
+
lines.push(`4. Use \`experiment_simulate\` to estimate required sample size and statistical power before running experiments.`);
|
|
455
|
+
lines.push(`5. Use \`reproducibility_check\` after obtaining results to assess replication probability.`);
|
|
456
|
+
return lines.filter(l => l !== undefined).join('\n');
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
// =========================================================================
|
|
460
|
+
// 2. anomaly_detect
|
|
461
|
+
// =========================================================================
|
|
462
|
+
registerTool({
|
|
463
|
+
name: 'anomaly_detect',
|
|
464
|
+
description: 'Detect anomalies and outliers in scientific data using z-score, IQR, Grubbs, or isolation forest methods. Returns which data points are anomalous with severity and potential explanations.',
|
|
465
|
+
parameters: {
|
|
466
|
+
data: {
|
|
467
|
+
type: 'string',
|
|
468
|
+
description: 'Numeric data as comma-separated values or JSON array (e.g. "1.2, 3.4, 5.6" or "[1.2, 3.4, 5.6]")',
|
|
469
|
+
required: true,
|
|
470
|
+
},
|
|
471
|
+
method: {
|
|
472
|
+
type: 'string',
|
|
473
|
+
description: 'Detection method: zscore, iqr, grubbs, or isolation',
|
|
474
|
+
required: true,
|
|
475
|
+
},
|
|
476
|
+
threshold: {
|
|
477
|
+
type: 'number',
|
|
478
|
+
description: 'Sensitivity threshold (default 2.0 for z-score, 1.5 for IQR multiplier)',
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
tier: 'free',
|
|
482
|
+
async execute(args) {
|
|
483
|
+
const values = parseNumericData(String(args.data));
|
|
484
|
+
const method = String(args.method).toLowerCase();
|
|
485
|
+
const threshold = typeof args.threshold === 'number' ? args.threshold : 2.0;
|
|
486
|
+
if (values.length < 3) {
|
|
487
|
+
return '**Error**: Need at least 3 data points for anomaly detection.';
|
|
488
|
+
}
|
|
489
|
+
const mu = mean(values);
|
|
490
|
+
const sd = stddev(values, mu);
|
|
491
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
492
|
+
const n = values.length;
|
|
493
|
+
const anomalies = [];
|
|
494
|
+
if (method === 'zscore') {
|
|
495
|
+
if (sd === 0)
|
|
496
|
+
return '**No anomalies**: All values are identical (standard deviation = 0).';
|
|
497
|
+
for (let i = 0; i < values.length; i++) {
|
|
498
|
+
const z = Math.abs((values[i] - mu) / sd);
|
|
499
|
+
if (z > threshold) {
|
|
500
|
+
const severity = z > 4 ? 'extreme' : z > 3 ? 'high' : z > 2.5 ? 'moderate' : 'low';
|
|
501
|
+
anomalies.push({
|
|
502
|
+
index: i,
|
|
503
|
+
value: values[i],
|
|
504
|
+
severity,
|
|
505
|
+
score: z,
|
|
506
|
+
explanation: `z-score = ${z.toFixed(3)}, ${z.toFixed(1)} SDs from mean (${mu.toFixed(3)})`,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
else if (method === 'iqr') {
|
|
512
|
+
const q1 = quantile(sorted, 0.25);
|
|
513
|
+
const q3 = quantile(sorted, 0.75);
|
|
514
|
+
const iqr = q3 - q1;
|
|
515
|
+
const multiplier = typeof args.threshold === 'number' ? args.threshold : 1.5;
|
|
516
|
+
const lower = q1 - multiplier * iqr;
|
|
517
|
+
const upper = q3 + multiplier * iqr;
|
|
518
|
+
for (let i = 0; i < values.length; i++) {
|
|
519
|
+
if (values[i] < lower || values[i] > upper) {
|
|
520
|
+
const distance = values[i] < lower ? lower - values[i] : values[i] - upper;
|
|
521
|
+
const sevScore = iqr > 0 ? distance / iqr : 1;
|
|
522
|
+
const severity = sevScore > 3 ? 'extreme' : sevScore > 2 ? 'high' : sevScore > 1 ? 'moderate' : 'low';
|
|
523
|
+
anomalies.push({
|
|
524
|
+
index: i,
|
|
525
|
+
value: values[i],
|
|
526
|
+
severity,
|
|
527
|
+
score: sevScore,
|
|
528
|
+
explanation: `Outside IQR fence [${lower.toFixed(3)}, ${upper.toFixed(3)}] by ${distance.toFixed(3)} (${sevScore.toFixed(2)}x IQR)`,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else if (method === 'grubbs') {
|
|
534
|
+
// Grubbs test for single outlier (two-sided)
|
|
535
|
+
if (sd === 0)
|
|
536
|
+
return '**No anomalies**: All values are identical.';
|
|
537
|
+
// Find the point farthest from mean
|
|
538
|
+
let maxIdx = 0, maxDev = 0;
|
|
539
|
+
for (let i = 0; i < values.length; i++) {
|
|
540
|
+
const dev = Math.abs(values[i] - mu);
|
|
541
|
+
if (dev > maxDev) {
|
|
542
|
+
maxDev = dev;
|
|
543
|
+
maxIdx = i;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const G = maxDev / sd;
|
|
547
|
+
// Critical value: t-distribution based
|
|
548
|
+
const alpha = 0.05;
|
|
549
|
+
const tCrit = -normalInvCDF(alpha / (2 * n));
|
|
550
|
+
const gCrit = ((n - 1) / Math.sqrt(n)) * Math.sqrt(tCrit * tCrit / (n - 2 + tCrit * tCrit));
|
|
551
|
+
const pValue = G > gCrit ? alpha : 1 - (G / gCrit) * alpha;
|
|
552
|
+
if (G > gCrit) {
|
|
553
|
+
anomalies.push({
|
|
554
|
+
index: maxIdx,
|
|
555
|
+
value: values[maxIdx],
|
|
556
|
+
severity: G > gCrit * 1.5 ? 'extreme' : G > gCrit * 1.2 ? 'high' : 'moderate',
|
|
557
|
+
score: G,
|
|
558
|
+
explanation: `Grubbs G = ${G.toFixed(4)} > critical ${gCrit.toFixed(4)} (p < ${Math.max(pValue, 0.001).toFixed(4)})`,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
// Build output for Grubbs
|
|
562
|
+
const lines = [
|
|
563
|
+
`# Anomaly Detection: Grubbs Test`,
|
|
564
|
+
'',
|
|
565
|
+
`**Data**: ${n} values, mean = ${mu.toFixed(4)}, SD = ${sd.toFixed(4)}`,
|
|
566
|
+
`**Test Statistic (G)**: ${G.toFixed(4)}`,
|
|
567
|
+
`**Critical Value**: ${gCrit.toFixed(4)} (alpha = ${alpha})`,
|
|
568
|
+
`**Most extreme point**: index ${maxIdx}, value = ${values[maxIdx]}`,
|
|
569
|
+
'',
|
|
570
|
+
G > gCrit
|
|
571
|
+
? `**Result**: Value ${values[maxIdx]} IS a significant outlier (G = ${G.toFixed(4)} > ${gCrit.toFixed(4)}, p < ${Math.max(pValue, 0.001).toFixed(4)}).`
|
|
572
|
+
: `**Result**: No significant outlier detected (G = ${G.toFixed(4)} <= ${gCrit.toFixed(4)}).`,
|
|
573
|
+
'',
|
|
574
|
+
`> **Note**: Grubbs test detects only one outlier at a time. For multiple outliers, apply iteratively or use z-score/IQR method.`,
|
|
575
|
+
];
|
|
576
|
+
return lines.join('\n');
|
|
577
|
+
}
|
|
578
|
+
else if (method === 'isolation') {
|
|
579
|
+
// Isolation forest approximation: random split scoring
|
|
580
|
+
const rng = new PRNG(42);
|
|
581
|
+
const numTrees = 100;
|
|
582
|
+
const subSampleSize = Math.min(256, n);
|
|
583
|
+
const avgPathLength = (size) => {
|
|
584
|
+
if (size <= 1)
|
|
585
|
+
return 0;
|
|
586
|
+
if (size === 2)
|
|
587
|
+
return 1;
|
|
588
|
+
const H = Math.log(size - 1) + 0.5772156649;
|
|
589
|
+
return 2 * H - 2 * (size - 1) / size;
|
|
590
|
+
};
|
|
591
|
+
// For each point, estimate isolation depth
|
|
592
|
+
const scores = new Array(n).fill(0);
|
|
593
|
+
for (let t = 0; t < numTrees; t++) {
|
|
594
|
+
// Sub-sample
|
|
595
|
+
const indices = [];
|
|
596
|
+
for (let s = 0; s < subSampleSize; s++) {
|
|
597
|
+
indices.push(Math.floor(rng.random() * n));
|
|
598
|
+
}
|
|
599
|
+
const subData = indices.map(i => values[i]);
|
|
600
|
+
// For each data point, simulate path length
|
|
601
|
+
for (let i = 0; i < n; i++) {
|
|
602
|
+
let depth = 0;
|
|
603
|
+
let lo = Math.min(...subData), hi = Math.max(...subData);
|
|
604
|
+
let current = [...subData];
|
|
605
|
+
const maxDepth = Math.ceil(Math.log2(subSampleSize));
|
|
606
|
+
while (depth < maxDepth && current.length > 1) {
|
|
607
|
+
if (hi - lo < 1e-10)
|
|
608
|
+
break;
|
|
609
|
+
const splitVal = lo + rng.random() * (hi - lo);
|
|
610
|
+
const left = current.filter(v => v < splitVal);
|
|
611
|
+
const right = current.filter(v => v >= splitVal);
|
|
612
|
+
if (values[i] < splitVal) {
|
|
613
|
+
current = left;
|
|
614
|
+
hi = splitVal;
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
current = right;
|
|
618
|
+
lo = splitVal;
|
|
619
|
+
}
|
|
620
|
+
depth++;
|
|
621
|
+
}
|
|
622
|
+
scores[i] += depth + avgPathLength(current.length);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// Normalize to anomaly score: s(x) = 2^(-E[h(x)] / c(n))
|
|
626
|
+
const c = avgPathLength(subSampleSize);
|
|
627
|
+
const anomalyScores = scores.map(s => Math.pow(2, -(s / numTrees) / c));
|
|
628
|
+
const isoThreshold = typeof args.threshold === 'number' ? args.threshold : 0.6;
|
|
629
|
+
for (let i = 0; i < n; i++) {
|
|
630
|
+
if (anomalyScores[i] > isoThreshold) {
|
|
631
|
+
const severity = anomalyScores[i] > 0.8 ? 'extreme' : anomalyScores[i] > 0.7 ? 'high' : 'moderate';
|
|
632
|
+
anomalies.push({
|
|
633
|
+
index: i,
|
|
634
|
+
value: values[i],
|
|
635
|
+
severity,
|
|
636
|
+
score: anomalyScores[i],
|
|
637
|
+
explanation: `Isolation score = ${anomalyScores[i].toFixed(4)} (threshold: ${isoThreshold}). Easier to isolate → more anomalous.`,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
return `**Error**: Unknown method "${method}". Use: zscore, iqr, grubbs, or isolation.`;
|
|
644
|
+
}
|
|
645
|
+
// Build output
|
|
646
|
+
const lines = [
|
|
647
|
+
`# Anomaly Detection Report`,
|
|
648
|
+
'',
|
|
649
|
+
`**Method**: ${method}`,
|
|
650
|
+
`**Data**: ${n} values, mean = ${mu.toFixed(4)}, SD = ${sd.toFixed(4)}, median = ${median(values).toFixed(4)}`,
|
|
651
|
+
`**Threshold**: ${threshold}`,
|
|
652
|
+
'',
|
|
653
|
+
];
|
|
654
|
+
if (anomalies.length === 0) {
|
|
655
|
+
lines.push(`**Result**: No anomalies detected at the given threshold.`);
|
|
656
|
+
lines.push('');
|
|
657
|
+
lines.push(`Consider lowering the threshold or trying a different method for more sensitivity.`);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
anomalies.sort((a, b) => b.score - a.score);
|
|
661
|
+
lines.push(`## Detected Anomalies (${anomalies.length})`);
|
|
662
|
+
lines.push('');
|
|
663
|
+
lines.push(`| Index | Value | Severity | Score | Explanation |`);
|
|
664
|
+
lines.push(`|-------|-------|----------|-------|-------------|`);
|
|
665
|
+
for (const a of anomalies) {
|
|
666
|
+
lines.push(`| ${a.index} | ${a.value} | **${a.severity}** | ${a.score.toFixed(4)} | ${a.explanation} |`);
|
|
667
|
+
}
|
|
668
|
+
lines.push('');
|
|
669
|
+
lines.push(`## Potential Explanations`);
|
|
670
|
+
lines.push('');
|
|
671
|
+
lines.push(`- **Measurement error**: Instrument malfunction, data entry mistake, or sensor drift`);
|
|
672
|
+
lines.push(`- **Natural variation**: Rare but genuine extreme values in the population`);
|
|
673
|
+
lines.push(`- **Subpopulation mixing**: Data from a different population mixed in`);
|
|
674
|
+
lines.push(`- **Process change**: A regime shift or intervention affecting some data points`);
|
|
675
|
+
lines.push('');
|
|
676
|
+
lines.push(`> Investigate anomalies before removing them. Outliers may carry the most important information.`);
|
|
677
|
+
}
|
|
678
|
+
return lines.join('\n');
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
// =========================================================================
|
|
682
|
+
// 3. pattern_match
|
|
683
|
+
// =========================================================================
|
|
684
|
+
registerTool({
|
|
685
|
+
name: 'pattern_match',
|
|
686
|
+
description: 'Find patterns across datasets or experimental results. Supports correlation, trend, periodicity (FFT), cluster (k-means), and change_point (CUSUM) detection.',
|
|
687
|
+
parameters: {
|
|
688
|
+
data_sets: {
|
|
689
|
+
type: 'string',
|
|
690
|
+
description: 'JSON array of {name, values} objects, e.g. [{"name":"A","values":[1,2,3]},{"name":"B","values":[4,5,6]}]',
|
|
691
|
+
required: true,
|
|
692
|
+
},
|
|
693
|
+
pattern_type: {
|
|
694
|
+
type: 'string',
|
|
695
|
+
description: 'Pattern to detect: correlation, trend, periodicity, cluster, or change_point',
|
|
696
|
+
required: true,
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
tier: 'free',
|
|
700
|
+
async execute(args) {
|
|
701
|
+
const patternType = String(args.pattern_type).toLowerCase();
|
|
702
|
+
let dataSets;
|
|
703
|
+
try {
|
|
704
|
+
dataSets = JSON.parse(String(args.data_sets));
|
|
705
|
+
if (!Array.isArray(dataSets) || dataSets.length === 0)
|
|
706
|
+
throw new Error('empty');
|
|
707
|
+
for (const ds of dataSets) {
|
|
708
|
+
if (!Array.isArray(ds.values))
|
|
709
|
+
throw new Error(`Dataset "${ds.name}" has no values array`);
|
|
710
|
+
ds.values = ds.values.map(Number);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
catch (e) {
|
|
714
|
+
return `**Error**: Invalid data_sets JSON. Expected array of {name, values} objects. ${e instanceof Error ? e.message : ''}`;
|
|
715
|
+
}
|
|
716
|
+
const lines = [
|
|
717
|
+
`# Pattern Analysis Report`,
|
|
718
|
+
'',
|
|
719
|
+
`**Pattern Type**: ${patternType}`,
|
|
720
|
+
`**Datasets**: ${dataSets.map(d => `${d.name} (n=${d.values.length})`).join(', ')}`,
|
|
721
|
+
'',
|
|
722
|
+
`---`,
|
|
723
|
+
'',
|
|
724
|
+
];
|
|
725
|
+
if (patternType === 'correlation') {
|
|
726
|
+
// Cross-correlation between all pairs of datasets
|
|
727
|
+
lines.push(`## Correlation Matrix`);
|
|
728
|
+
lines.push('');
|
|
729
|
+
if (dataSets.length < 2) {
|
|
730
|
+
lines.push('Need at least 2 datasets for correlation analysis.');
|
|
731
|
+
return lines.join('\n');
|
|
732
|
+
}
|
|
733
|
+
// Header
|
|
734
|
+
const header = ['Dataset', ...dataSets.map(d => d.name)].join(' | ');
|
|
735
|
+
lines.push(`| ${header} |`);
|
|
736
|
+
lines.push(`|${'---|'.repeat(dataSets.length + 1)}`);
|
|
737
|
+
for (const dsA of dataSets) {
|
|
738
|
+
const row = [dsA.name];
|
|
739
|
+
for (const dsB of dataSets) {
|
|
740
|
+
const r = pearsonCorrelation(dsA.values, dsB.values);
|
|
741
|
+
row.push(r.toFixed(4));
|
|
742
|
+
}
|
|
743
|
+
lines.push(`| ${row.join(' | ')} |`);
|
|
744
|
+
}
|
|
745
|
+
lines.push('');
|
|
746
|
+
lines.push(`## Interpretation`);
|
|
747
|
+
lines.push('');
|
|
748
|
+
// Report significant correlations
|
|
749
|
+
for (let i = 0; i < dataSets.length; i++) {
|
|
750
|
+
for (let j = i + 1; j < dataSets.length; j++) {
|
|
751
|
+
const r = pearsonCorrelation(dataSets[i].values, dataSets[j].values);
|
|
752
|
+
const n = Math.min(dataSets[i].values.length, dataSets[j].values.length);
|
|
753
|
+
// t-test for correlation significance
|
|
754
|
+
const t = n > 2 ? r * Math.sqrt((n - 2) / (1 - r * r + 1e-10)) : 0;
|
|
755
|
+
const df = n - 2;
|
|
756
|
+
const pValue = df > 0 ? 2 * (1 - tCDF(Math.abs(t), df)) : 1;
|
|
757
|
+
const strength = Math.abs(r) > 0.7 ? 'strong' : Math.abs(r) > 0.4 ? 'moderate' : Math.abs(r) > 0.2 ? 'weak' : 'negligible';
|
|
758
|
+
const direction = r > 0 ? 'positive' : 'negative';
|
|
759
|
+
const sig = pValue < 0.001 ? '***' : pValue < 0.01 ? '**' : pValue < 0.05 ? '*' : 'n.s.';
|
|
760
|
+
lines.push(`- **${dataSets[i].name} vs ${dataSets[j].name}**: r = ${r.toFixed(4)}, ${strength} ${direction} correlation (t = ${t.toFixed(3)}, p ${pValue < 0.001 ? '< 0.001' : `= ${pValue.toFixed(4)}`} ${sig})`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
else if (patternType === 'trend') {
|
|
765
|
+
// Mann-Kendall trend test
|
|
766
|
+
lines.push(`## Trend Analysis (Mann-Kendall Test)`);
|
|
767
|
+
lines.push('');
|
|
768
|
+
for (const ds of dataSets) {
|
|
769
|
+
const n = ds.values.length;
|
|
770
|
+
if (n < 4) {
|
|
771
|
+
lines.push(`**${ds.name}**: Too few points for trend detection (need >= 4).`);
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
// Compute S statistic
|
|
775
|
+
let S = 0;
|
|
776
|
+
for (let i = 0; i < n - 1; i++) {
|
|
777
|
+
for (let j = i + 1; j < n; j++) {
|
|
778
|
+
const diff = ds.values[j] - ds.values[i];
|
|
779
|
+
if (diff > 0)
|
|
780
|
+
S++;
|
|
781
|
+
else if (diff < 0)
|
|
782
|
+
S--;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// Compute variance of S (accounting for ties)
|
|
786
|
+
const tieGroups = new Map();
|
|
787
|
+
for (const v of ds.values) {
|
|
788
|
+
tieGroups.set(v, (tieGroups.get(v) || 0) + 1);
|
|
789
|
+
}
|
|
790
|
+
let tieAdjust = 0;
|
|
791
|
+
for (const count of tieGroups.values()) {
|
|
792
|
+
if (count > 1)
|
|
793
|
+
tieAdjust += count * (count - 1) * (2 * count + 5);
|
|
794
|
+
}
|
|
795
|
+
const varS = (n * (n - 1) * (2 * n + 5) - tieAdjust) / 18;
|
|
796
|
+
// Z statistic
|
|
797
|
+
let Z = 0;
|
|
798
|
+
if (S > 0)
|
|
799
|
+
Z = (S - 1) / Math.sqrt(varS);
|
|
800
|
+
else if (S < 0)
|
|
801
|
+
Z = (S + 1) / Math.sqrt(varS);
|
|
802
|
+
const pValue = 2 * (1 - normalCDF(Math.abs(Z)));
|
|
803
|
+
const trend = Z > 0 ? 'increasing' : Z < 0 ? 'decreasing' : 'no trend';
|
|
804
|
+
const sig = pValue < 0.001 ? 'highly significant' : pValue < 0.01 ? 'very significant' : pValue < 0.05 ? 'significant' : 'not significant';
|
|
805
|
+
// Sen's slope
|
|
806
|
+
const slopes = [];
|
|
807
|
+
for (let i = 0; i < n - 1; i++) {
|
|
808
|
+
for (let j = i + 1; j < n; j++) {
|
|
809
|
+
if (j !== i)
|
|
810
|
+
slopes.push((ds.values[j] - ds.values[i]) / (j - i));
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
slopes.sort((a, b) => a - b);
|
|
814
|
+
const senSlope = median(slopes);
|
|
815
|
+
lines.push(`### ${ds.name}`);
|
|
816
|
+
lines.push('');
|
|
817
|
+
lines.push(`| Metric | Value |`);
|
|
818
|
+
lines.push(`|--------|-------|`);
|
|
819
|
+
lines.push(`| S statistic | ${S} |`);
|
|
820
|
+
lines.push(`| Variance(S) | ${varS.toFixed(2)} |`);
|
|
821
|
+
lines.push(`| Z score | ${Z.toFixed(4)} |`);
|
|
822
|
+
lines.push(`| p-value | ${pValue < 0.001 ? '< 0.001' : pValue.toFixed(4)} |`);
|
|
823
|
+
lines.push(`| Trend | **${trend}** (${sig}) |`);
|
|
824
|
+
lines.push(`| Sen's slope | ${senSlope.toFixed(6)} per unit |`);
|
|
825
|
+
lines.push('');
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
else if (patternType === 'periodicity') {
|
|
829
|
+
// FFT-based periodicity detection
|
|
830
|
+
lines.push(`## Periodicity Analysis (FFT)`);
|
|
831
|
+
lines.push('');
|
|
832
|
+
for (const ds of dataSets) {
|
|
833
|
+
const n = ds.values.length;
|
|
834
|
+
if (n < 8) {
|
|
835
|
+
lines.push(`**${ds.name}**: Too few points for FFT (need >= 8).`);
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
// Zero-mean
|
|
839
|
+
const mu = mean(ds.values);
|
|
840
|
+
const centered = ds.values.map(v => v - mu);
|
|
841
|
+
// DFT (not radix-2 FFT, but works for any n)
|
|
842
|
+
const halfN = Math.floor(n / 2);
|
|
843
|
+
const magnitudes = [];
|
|
844
|
+
const frequencies = [];
|
|
845
|
+
for (let k = 1; k <= halfN; k++) {
|
|
846
|
+
let realPart = 0, imagPart = 0;
|
|
847
|
+
for (let t = 0; t < n; t++) {
|
|
848
|
+
const angle = (2 * Math.PI * k * t) / n;
|
|
849
|
+
realPart += centered[t] * Math.cos(angle);
|
|
850
|
+
imagPart -= centered[t] * Math.sin(angle);
|
|
851
|
+
}
|
|
852
|
+
const magnitude = Math.sqrt(realPart * realPart + imagPart * imagPart) / n;
|
|
853
|
+
magnitudes.push(magnitude);
|
|
854
|
+
frequencies.push(k / n);
|
|
855
|
+
}
|
|
856
|
+
// Find dominant frequencies
|
|
857
|
+
const maxMag = Math.max(...magnitudes);
|
|
858
|
+
const dominantThreshold = maxMag * 0.3;
|
|
859
|
+
const peaks = [];
|
|
860
|
+
for (let i = 0; i < magnitudes.length; i++) {
|
|
861
|
+
if (magnitudes[i] >= dominantThreshold) {
|
|
862
|
+
const isPeak = (i === 0 || magnitudes[i] >= magnitudes[i - 1]) &&
|
|
863
|
+
(i === magnitudes.length - 1 || magnitudes[i] >= magnitudes[i + 1]);
|
|
864
|
+
if (isPeak) {
|
|
865
|
+
peaks.push({
|
|
866
|
+
frequency: frequencies[i],
|
|
867
|
+
period: 1 / frequencies[i],
|
|
868
|
+
magnitude: magnitudes[i],
|
|
869
|
+
relativeStrength: magnitudes[i] / maxMag,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
peaks.sort((a, b) => b.magnitude - a.magnitude);
|
|
875
|
+
lines.push(`### ${ds.name}`);
|
|
876
|
+
lines.push('');
|
|
877
|
+
if (peaks.length === 0) {
|
|
878
|
+
lines.push(`No significant periodic components detected.`);
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
lines.push(`| Rank | Period | Frequency | Magnitude | Relative Strength |`);
|
|
882
|
+
lines.push(`|------|--------|-----------|-----------|-------------------|`);
|
|
883
|
+
for (let i = 0; i < Math.min(peaks.length, 5); i++) {
|
|
884
|
+
const p = peaks[i];
|
|
885
|
+
lines.push(`| ${i + 1} | ${p.period.toFixed(2)} | ${p.frequency.toFixed(4)} | ${p.magnitude.toFixed(4)} | ${(p.relativeStrength * 100).toFixed(1)}% |`);
|
|
886
|
+
}
|
|
887
|
+
lines.push('');
|
|
888
|
+
lines.push(`**Dominant period**: ${peaks[0].period.toFixed(2)} time units (frequency = ${peaks[0].frequency.toFixed(4)})`);
|
|
889
|
+
}
|
|
890
|
+
lines.push('');
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
else if (patternType === 'cluster') {
|
|
894
|
+
// K-means clustering (Lloyd's algorithm)
|
|
895
|
+
lines.push(`## Cluster Analysis (K-Means)`);
|
|
896
|
+
lines.push('');
|
|
897
|
+
// Combine all datasets into feature vectors
|
|
898
|
+
const maxLen = Math.max(...dataSets.map(d => d.values.length));
|
|
899
|
+
const features = [];
|
|
900
|
+
for (let i = 0; i < maxLen; i++) {
|
|
901
|
+
const point = [];
|
|
902
|
+
for (const ds of dataSets) {
|
|
903
|
+
point.push(i < ds.values.length ? ds.values[i] : 0);
|
|
904
|
+
}
|
|
905
|
+
features.push(point);
|
|
906
|
+
}
|
|
907
|
+
// Determine k using elbow heuristic (try 2-6)
|
|
908
|
+
function kMeans(data, k, maxIter = 100) {
|
|
909
|
+
const dim = data[0].length;
|
|
910
|
+
const rng = new PRNG(42 + k);
|
|
911
|
+
// Initialize centroids (k-means++)
|
|
912
|
+
const centroids = [];
|
|
913
|
+
centroids.push([...data[Math.floor(rng.random() * data.length)]]);
|
|
914
|
+
for (let c = 1; c < k; c++) {
|
|
915
|
+
const dists = data.map(p => {
|
|
916
|
+
let minD = Infinity;
|
|
917
|
+
for (const cent of centroids) {
|
|
918
|
+
let d = 0;
|
|
919
|
+
for (let j = 0; j < dim; j++)
|
|
920
|
+
d += (p[j] - cent[j]) ** 2;
|
|
921
|
+
minD = Math.min(minD, d);
|
|
922
|
+
}
|
|
923
|
+
return minD;
|
|
924
|
+
});
|
|
925
|
+
const totalDist = dists.reduce((a, b) => a + b, 0);
|
|
926
|
+
let r = rng.random() * totalDist;
|
|
927
|
+
for (let i = 0; i < data.length; i++) {
|
|
928
|
+
r -= dists[i];
|
|
929
|
+
if (r <= 0) {
|
|
930
|
+
centroids.push([...data[i]]);
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (centroids.length <= c)
|
|
935
|
+
centroids.push([...data[Math.floor(rng.random() * data.length)]]);
|
|
936
|
+
}
|
|
937
|
+
let assignments = new Array(data.length).fill(0);
|
|
938
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
939
|
+
// Assign
|
|
940
|
+
const newAssignments = data.map(p => {
|
|
941
|
+
let bestK = 0, bestD = Infinity;
|
|
942
|
+
for (let c = 0; c < k; c++) {
|
|
943
|
+
let d = 0;
|
|
944
|
+
for (let j = 0; j < dim; j++)
|
|
945
|
+
d += (p[j] - centroids[c][j]) ** 2;
|
|
946
|
+
if (d < bestD) {
|
|
947
|
+
bestD = d;
|
|
948
|
+
bestK = c;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return bestK;
|
|
952
|
+
});
|
|
953
|
+
// Update centroids
|
|
954
|
+
let changed = false;
|
|
955
|
+
for (let c = 0; c < k; c++) {
|
|
956
|
+
const members = data.filter((_, i) => newAssignments[i] === c);
|
|
957
|
+
if (members.length === 0)
|
|
958
|
+
continue;
|
|
959
|
+
for (let j = 0; j < dim; j++) {
|
|
960
|
+
const newVal = mean(members.map(m => m[j]));
|
|
961
|
+
if (Math.abs(centroids[c][j] - newVal) > 1e-10)
|
|
962
|
+
changed = true;
|
|
963
|
+
centroids[c][j] = newVal;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
assignments = newAssignments;
|
|
967
|
+
if (!changed)
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
// Compute inertia
|
|
971
|
+
let inertia = 0;
|
|
972
|
+
for (let i = 0; i < data.length; i++) {
|
|
973
|
+
for (let j = 0; j < dim; j++) {
|
|
974
|
+
inertia += (data[i][j] - centroids[assignments[i]][j]) ** 2;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return { assignments, centroids, inertia };
|
|
978
|
+
}
|
|
979
|
+
const maxK = Math.min(6, features.length - 1);
|
|
980
|
+
const results = [];
|
|
981
|
+
let bestK = 2;
|
|
982
|
+
for (let k = 1; k <= maxK; k++) {
|
|
983
|
+
const res = kMeans(features, k);
|
|
984
|
+
results.push({ k, inertia: res.inertia });
|
|
985
|
+
}
|
|
986
|
+
// Elbow detection: largest drop in inertia
|
|
987
|
+
if (results.length >= 3) {
|
|
988
|
+
let maxDrop = 0;
|
|
989
|
+
for (let i = 1; i < results.length - 1; i++) {
|
|
990
|
+
const drop = (results[i - 1].inertia - results[i].inertia) - (results[i].inertia - results[i + 1].inertia);
|
|
991
|
+
if (drop > maxDrop) {
|
|
992
|
+
maxDrop = drop;
|
|
993
|
+
bestK = results[i].k;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
const finalResult = kMeans(features, bestK);
|
|
998
|
+
lines.push(`**Optimal k**: ${bestK} (elbow method)`);
|
|
999
|
+
lines.push('');
|
|
1000
|
+
lines.push(`### Elbow Curve`);
|
|
1001
|
+
lines.push('');
|
|
1002
|
+
lines.push(`| k | Inertia |`);
|
|
1003
|
+
lines.push(`|---|---------|`);
|
|
1004
|
+
for (const r of results) {
|
|
1005
|
+
lines.push(`| ${r.k} | ${r.inertia.toFixed(2)} |`);
|
|
1006
|
+
}
|
|
1007
|
+
lines.push('');
|
|
1008
|
+
lines.push(`### Cluster Assignments (k=${bestK})`);
|
|
1009
|
+
lines.push('');
|
|
1010
|
+
for (let c = 0; c < bestK; c++) {
|
|
1011
|
+
const members = finalResult.assignments
|
|
1012
|
+
.map((a, i) => a === c ? i : -1)
|
|
1013
|
+
.filter(i => i >= 0);
|
|
1014
|
+
lines.push(`**Cluster ${c + 1}** (${members.length} points): indices [${members.slice(0, 20).join(', ')}${members.length > 20 ? '...' : ''}]`);
|
|
1015
|
+
lines.push(` Centroid: [${finalResult.centroids[c].map(v => v.toFixed(3)).join(', ')}]`);
|
|
1016
|
+
lines.push('');
|
|
1017
|
+
}
|
|
1018
|
+
lines.push(`**Total inertia**: ${finalResult.inertia.toFixed(4)}`);
|
|
1019
|
+
}
|
|
1020
|
+
else if (patternType === 'change_point') {
|
|
1021
|
+
// CUSUM algorithm for regime change detection
|
|
1022
|
+
lines.push(`## Change Point Detection (CUSUM)`);
|
|
1023
|
+
lines.push('');
|
|
1024
|
+
for (const ds of dataSets) {
|
|
1025
|
+
const n = ds.values.length;
|
|
1026
|
+
if (n < 5) {
|
|
1027
|
+
lines.push(`**${ds.name}**: Too few points (need >= 5).`);
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
const mu = mean(ds.values);
|
|
1031
|
+
const sd = stddev(ds.values, mu);
|
|
1032
|
+
// CUSUM: cumulative sum of deviations from mean
|
|
1033
|
+
const cusum = [];
|
|
1034
|
+
let cumSum = 0;
|
|
1035
|
+
for (const v of ds.values) {
|
|
1036
|
+
cumSum += (v - mu);
|
|
1037
|
+
cusum.push(cumSum);
|
|
1038
|
+
}
|
|
1039
|
+
// Detect change points: where CUSUM changes sign or has max absolute value
|
|
1040
|
+
const changePoints = [];
|
|
1041
|
+
// Method 1: Maximum absolute CUSUM value
|
|
1042
|
+
let maxAbsCusum = 0, maxIdx = 0;
|
|
1043
|
+
for (let i = 0; i < cusum.length; i++) {
|
|
1044
|
+
if (Math.abs(cusum[i]) > maxAbsCusum) {
|
|
1045
|
+
maxAbsCusum = Math.abs(cusum[i]);
|
|
1046
|
+
maxIdx = i;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
// Significance: compare to threshold h = 4 * SD or 5 * SD
|
|
1050
|
+
const h = 4 * sd;
|
|
1051
|
+
if (maxAbsCusum > h) {
|
|
1052
|
+
changePoints.push({
|
|
1053
|
+
index: maxIdx,
|
|
1054
|
+
value: ds.values[maxIdx],
|
|
1055
|
+
cusum: cusum[maxIdx],
|
|
1056
|
+
direction: cusum[maxIdx] > 0 ? 'upward shift' : 'downward shift',
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
// Method 2: Sign changes in CUSUM
|
|
1060
|
+
for (let i = 1; i < cusum.length; i++) {
|
|
1061
|
+
if ((cusum[i - 1] > 0 && cusum[i] < 0) || (cusum[i - 1] < 0 && cusum[i] > 0)) {
|
|
1062
|
+
// Verify this is a real change, not noise
|
|
1063
|
+
const leftMean = mean(ds.values.slice(Math.max(0, i - 3), i));
|
|
1064
|
+
const rightMean = mean(ds.values.slice(i, Math.min(n, i + 3)));
|
|
1065
|
+
if (Math.abs(rightMean - leftMean) > sd * 0.5) {
|
|
1066
|
+
// Check for duplicates
|
|
1067
|
+
const isDup = changePoints.some(cp => Math.abs(cp.index - i) < 3);
|
|
1068
|
+
if (!isDup) {
|
|
1069
|
+
changePoints.push({
|
|
1070
|
+
index: i,
|
|
1071
|
+
value: ds.values[i],
|
|
1072
|
+
cusum: cusum[i],
|
|
1073
|
+
direction: rightMean > leftMean ? 'upward shift' : 'downward shift',
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
lines.push(`### ${ds.name}`);
|
|
1080
|
+
lines.push('');
|
|
1081
|
+
lines.push(`**Mean**: ${mu.toFixed(4)}, **SD**: ${sd.toFixed(4)}, **Detection threshold**: ${h.toFixed(4)}`);
|
|
1082
|
+
lines.push('');
|
|
1083
|
+
if (changePoints.length === 0) {
|
|
1084
|
+
lines.push(`No significant change points detected. The data appears stationary.`);
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
changePoints.sort((a, b) => a.index - b.index);
|
|
1088
|
+
lines.push(`| Index | Value | CUSUM | Direction |`);
|
|
1089
|
+
lines.push(`|-------|-------|-------|-----------|`);
|
|
1090
|
+
for (const cp of changePoints) {
|
|
1091
|
+
lines.push(`| ${cp.index} | ${cp.value.toFixed(4)} | ${cp.cusum.toFixed(4)} | ${cp.direction} |`);
|
|
1092
|
+
}
|
|
1093
|
+
lines.push('');
|
|
1094
|
+
// Report segments
|
|
1095
|
+
const breakpoints = [0, ...changePoints.map(cp => cp.index), n];
|
|
1096
|
+
lines.push(`### Segments`);
|
|
1097
|
+
lines.push('');
|
|
1098
|
+
for (let s = 0; s < breakpoints.length - 1; s++) {
|
|
1099
|
+
const seg = ds.values.slice(breakpoints[s], breakpoints[s + 1]);
|
|
1100
|
+
if (seg.length > 0) {
|
|
1101
|
+
lines.push(`- **Segment ${s + 1}** [${breakpoints[s]}:${breakpoints[s + 1]}]: mean = ${mean(seg).toFixed(4)}, sd = ${seg.length > 1 ? stddev(seg).toFixed(4) : 'N/A'}, n = ${seg.length}`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
lines.push('');
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
return `**Error**: Unknown pattern_type "${patternType}". Use: correlation, trend, periodicity, cluster, or change_point.`;
|
|
1110
|
+
}
|
|
1111
|
+
return lines.join('\n');
|
|
1112
|
+
},
|
|
1113
|
+
});
|
|
1114
|
+
// =========================================================================
|
|
1115
|
+
// 4. experiment_simulate
|
|
1116
|
+
// =========================================================================
|
|
1117
|
+
registerTool({
|
|
1118
|
+
name: 'experiment_simulate',
|
|
1119
|
+
description: 'Simulate an experiment before running it. Uses Monte Carlo simulation to estimate statistical power, type I/II error rates, and required sample size. Helps decide if an experiment is worth running.',
|
|
1120
|
+
parameters: {
|
|
1121
|
+
hypothesis: {
|
|
1122
|
+
type: 'string',
|
|
1123
|
+
description: 'The hypothesis being tested (for labeling)',
|
|
1124
|
+
required: true,
|
|
1125
|
+
},
|
|
1126
|
+
sample_size: {
|
|
1127
|
+
type: 'number',
|
|
1128
|
+
description: 'Planned sample size per group',
|
|
1129
|
+
required: true,
|
|
1130
|
+
},
|
|
1131
|
+
effect_size: {
|
|
1132
|
+
type: 'number',
|
|
1133
|
+
description: "Expected Cohen's d effect size (0.2 = small, 0.5 = medium, 0.8 = large)",
|
|
1134
|
+
required: true,
|
|
1135
|
+
},
|
|
1136
|
+
noise_level: {
|
|
1137
|
+
type: 'number',
|
|
1138
|
+
description: 'Noise standard deviation relative to effect (default: 0.1 means 10% noise)',
|
|
1139
|
+
},
|
|
1140
|
+
simulations: {
|
|
1141
|
+
type: 'number',
|
|
1142
|
+
description: 'Number of Monte Carlo simulations (default: 1000)',
|
|
1143
|
+
},
|
|
1144
|
+
},
|
|
1145
|
+
tier: 'free',
|
|
1146
|
+
async execute(args) {
|
|
1147
|
+
const hypothesis = String(args.hypothesis);
|
|
1148
|
+
const sampleSize = Math.max(2, Math.round(Number(args.sample_size)));
|
|
1149
|
+
const effectSize = Number(args.effect_size);
|
|
1150
|
+
const noiseLevel = typeof args.noise_level === 'number' ? args.noise_level : 0.1;
|
|
1151
|
+
const numSims = typeof args.simulations === 'number' ? Math.min(Math.max(100, args.simulations), 10000) : 1000;
|
|
1152
|
+
const alpha = 0.05;
|
|
1153
|
+
const rng = new PRNG(12345);
|
|
1154
|
+
// Two-sample t-test simulation
|
|
1155
|
+
function simulateTTest(d, n) {
|
|
1156
|
+
// Generate two groups
|
|
1157
|
+
const group1 = [];
|
|
1158
|
+
const group2 = [];
|
|
1159
|
+
for (let i = 0; i < n; i++) {
|
|
1160
|
+
group1.push(rng.normal(0, 1 + noiseLevel));
|
|
1161
|
+
group2.push(rng.normal(d, 1 + noiseLevel));
|
|
1162
|
+
}
|
|
1163
|
+
const m1 = mean(group1), m2 = mean(group2);
|
|
1164
|
+
const v1 = variance(group1, m1), v2 = variance(group2, m2);
|
|
1165
|
+
const pooledSE = Math.sqrt(v1 / n + v2 / n);
|
|
1166
|
+
if (pooledSE === 0)
|
|
1167
|
+
return { pValue: 1, tStat: 0 };
|
|
1168
|
+
const t = (m2 - m1) / pooledSE;
|
|
1169
|
+
const df = n + n - 2;
|
|
1170
|
+
const pValue = 2 * (1 - tCDF(Math.abs(t), df));
|
|
1171
|
+
return { pValue, tStat: t };
|
|
1172
|
+
}
|
|
1173
|
+
// Simulate under H0 (no effect) — estimate type I error
|
|
1174
|
+
let typeIErrors = 0;
|
|
1175
|
+
for (let i = 0; i < numSims; i++) {
|
|
1176
|
+
const result = simulateTTest(0, sampleSize);
|
|
1177
|
+
if (result.pValue < alpha)
|
|
1178
|
+
typeIErrors++;
|
|
1179
|
+
}
|
|
1180
|
+
const typeIRate = typeIErrors / numSims;
|
|
1181
|
+
// Simulate under H1 (real effect) — estimate power
|
|
1182
|
+
let rejects = 0;
|
|
1183
|
+
const pValues = [];
|
|
1184
|
+
for (let i = 0; i < numSims; i++) {
|
|
1185
|
+
const result = simulateTTest(effectSize, sampleSize);
|
|
1186
|
+
pValues.push(result.pValue);
|
|
1187
|
+
if (result.pValue < alpha)
|
|
1188
|
+
rejects++;
|
|
1189
|
+
}
|
|
1190
|
+
const power = rejects / numSims;
|
|
1191
|
+
const typeIIRate = 1 - power;
|
|
1192
|
+
// Compute power curve for different sample sizes
|
|
1193
|
+
const powerCurve = [];
|
|
1194
|
+
const nValues = [5, 10, 15, 20, 30, 50, 75, 100, 150, 200, 300, 500];
|
|
1195
|
+
for (const n of nValues) {
|
|
1196
|
+
let rejCount = 0;
|
|
1197
|
+
const quickSims = Math.min(500, numSims);
|
|
1198
|
+
for (let i = 0; i < quickSims; i++) {
|
|
1199
|
+
const result = simulateTTest(effectSize, n);
|
|
1200
|
+
if (result.pValue < alpha)
|
|
1201
|
+
rejCount++;
|
|
1202
|
+
}
|
|
1203
|
+
powerCurve.push({ n, power: rejCount / quickSims });
|
|
1204
|
+
}
|
|
1205
|
+
// Find minimum sample size for 80% power
|
|
1206
|
+
let minN80 = nValues[nValues.length - 1];
|
|
1207
|
+
for (const pc of powerCurve) {
|
|
1208
|
+
if (pc.power >= 0.80) {
|
|
1209
|
+
minN80 = pc.n;
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
// Analytical power estimate (for comparison)
|
|
1214
|
+
const analyticalPower = normalCDF(effectSize * Math.sqrt(sampleSize / 2) - normalInvCDF(1 - alpha / 2));
|
|
1215
|
+
// Recommendation
|
|
1216
|
+
let recommendation;
|
|
1217
|
+
if (power >= 0.8) {
|
|
1218
|
+
recommendation = 'The experiment is well-powered. Proceed with the planned design.';
|
|
1219
|
+
}
|
|
1220
|
+
else if (power >= 0.5) {
|
|
1221
|
+
recommendation = `The experiment has moderate power. Consider increasing sample size to ${minN80} for 80% power.`;
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
recommendation = `The experiment is underpowered (${(power * 100).toFixed(1)}%). You would need approximately ${minN80} samples per group for adequate power. Consider whether the expected effect size is realistic.`;
|
|
1225
|
+
}
|
|
1226
|
+
const lines = [
|
|
1227
|
+
`# Experiment Simulation Report`,
|
|
1228
|
+
'',
|
|
1229
|
+
`**Hypothesis**: ${hypothesis}`,
|
|
1230
|
+
`**Planned sample size**: ${sampleSize} per group`,
|
|
1231
|
+
`**Expected effect size (Cohen's d)**: ${effectSize}`,
|
|
1232
|
+
`**Noise level**: ${noiseLevel}`,
|
|
1233
|
+
`**Simulations**: ${numSims}`,
|
|
1234
|
+
`**Significance level (alpha)**: ${alpha}`,
|
|
1235
|
+
'',
|
|
1236
|
+
`---`,
|
|
1237
|
+
'',
|
|
1238
|
+
`## Results`,
|
|
1239
|
+
'',
|
|
1240
|
+
`| Metric | Value |`,
|
|
1241
|
+
`|--------|-------|`,
|
|
1242
|
+
`| **Statistical Power** | ${(power * 100).toFixed(1)}% |`,
|
|
1243
|
+
`| **Type I Error Rate** | ${(typeIRate * 100).toFixed(1)}% (nominal: ${(alpha * 100).toFixed(1)}%) |`,
|
|
1244
|
+
`| **Type II Error Rate** | ${(typeIIRate * 100).toFixed(1)}% |`,
|
|
1245
|
+
`| **Analytical Power (approx.)** | ${(analyticalPower * 100).toFixed(1)}% |`,
|
|
1246
|
+
`| **Min n for 80% power** | ~${minN80} per group |`,
|
|
1247
|
+
'',
|
|
1248
|
+
`## Power Curve`,
|
|
1249
|
+
'',
|
|
1250
|
+
`| Sample Size (per group) | Estimated Power |`,
|
|
1251
|
+
`|-------------------------|-----------------|`,
|
|
1252
|
+
];
|
|
1253
|
+
for (const pc of powerCurve) {
|
|
1254
|
+
const bar = '#'.repeat(Math.round(pc.power * 30));
|
|
1255
|
+
lines.push(`| ${String(pc.n).padStart(4)} | ${(pc.power * 100).toFixed(1).padStart(5)}% ${bar} |`);
|
|
1256
|
+
}
|
|
1257
|
+
lines.push('');
|
|
1258
|
+
lines.push(`## P-Value Distribution (under H1)`);
|
|
1259
|
+
lines.push('');
|
|
1260
|
+
// Histogram of p-values
|
|
1261
|
+
const bins = [0.001, 0.01, 0.05, 0.10, 0.25, 0.50, 1.0];
|
|
1262
|
+
const binCounts = new Array(bins.length).fill(0);
|
|
1263
|
+
for (const p of pValues) {
|
|
1264
|
+
for (let b = 0; b < bins.length; b++) {
|
|
1265
|
+
if (p <= bins[b]) {
|
|
1266
|
+
binCounts[b]++;
|
|
1267
|
+
break;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
lines.push(`| P-value Range | Count | Percentage |`);
|
|
1272
|
+
lines.push(`|---------------|-------|------------|`);
|
|
1273
|
+
let prevBin = 0;
|
|
1274
|
+
for (let b = 0; b < bins.length; b++) {
|
|
1275
|
+
const pct = (binCounts[b] / numSims * 100).toFixed(1);
|
|
1276
|
+
lines.push(`| ${prevBin} - ${bins[b]} | ${binCounts[b]} | ${pct}% |`);
|
|
1277
|
+
prevBin = bins[b];
|
|
1278
|
+
}
|
|
1279
|
+
lines.push('');
|
|
1280
|
+
lines.push(`## Recommendation`);
|
|
1281
|
+
lines.push('');
|
|
1282
|
+
lines.push(recommendation);
|
|
1283
|
+
return lines.join('\n');
|
|
1284
|
+
},
|
|
1285
|
+
});
|
|
1286
|
+
// =========================================================================
|
|
1287
|
+
// 5. research_gap_finder
|
|
1288
|
+
// =========================================================================
|
|
1289
|
+
registerTool({
|
|
1290
|
+
name: 'research_gap_finder',
|
|
1291
|
+
description: 'Identify gaps in scientific literature for a given topic. Queries OpenAlex for publication trends, most-cited subtopics, and least-explored areas. Suggests research questions for unexplored territory.',
|
|
1292
|
+
parameters: {
|
|
1293
|
+
topic: {
|
|
1294
|
+
type: 'string',
|
|
1295
|
+
description: 'Research topic to analyze',
|
|
1296
|
+
required: true,
|
|
1297
|
+
},
|
|
1298
|
+
field: {
|
|
1299
|
+
type: 'string',
|
|
1300
|
+
description: 'Scientific field for context (e.g. neuroscience, machine_learning, ecology)',
|
|
1301
|
+
required: true,
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
tier: 'free',
|
|
1305
|
+
async execute(args) {
|
|
1306
|
+
const topic = String(args.topic);
|
|
1307
|
+
const field = String(args.field);
|
|
1308
|
+
const encodedTopic = encodeURIComponent(topic);
|
|
1309
|
+
const lines = [
|
|
1310
|
+
`# Research Gap Analysis`,
|
|
1311
|
+
'',
|
|
1312
|
+
`**Topic**: ${topic}`,
|
|
1313
|
+
`**Field**: ${field}`,
|
|
1314
|
+
'',
|
|
1315
|
+
`---`,
|
|
1316
|
+
'',
|
|
1317
|
+
];
|
|
1318
|
+
// Query OpenAlex for works grouped by year
|
|
1319
|
+
let yearData = [];
|
|
1320
|
+
try {
|
|
1321
|
+
const yearRes = await fetch(`https://api.openalex.org/works?search=${encodedTopic}&group_by=publication_year&per_page=50`, { headers: { 'User-Agent': 'KBot/3.0 (mailto:kernel.chat@gmail.com)' }, signal: AbortSignal.timeout(10000) });
|
|
1322
|
+
if (yearRes.ok) {
|
|
1323
|
+
const yearJson = await yearRes.json();
|
|
1324
|
+
if (yearJson.group_by) {
|
|
1325
|
+
yearData = yearJson.group_by
|
|
1326
|
+
.map((g) => ({ year: String(g.key), count: Number(g.count) }))
|
|
1327
|
+
.filter((g) => {
|
|
1328
|
+
const y = parseInt(g.year);
|
|
1329
|
+
return y >= 2015 && y <= 2026;
|
|
1330
|
+
})
|
|
1331
|
+
.sort((a, b) => a.year.localeCompare(b.year));
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
catch { /* API unavailable — continue with what we have */ }
|
|
1336
|
+
// Query OpenAlex for related concepts
|
|
1337
|
+
let conceptData = [];
|
|
1338
|
+
try {
|
|
1339
|
+
const conceptRes = await fetch(`https://api.openalex.org/concepts?search=${encodedTopic}&per_page=20`, { headers: { 'User-Agent': 'KBot/3.0 (mailto:kernel.chat@gmail.com)' }, signal: AbortSignal.timeout(10000) });
|
|
1340
|
+
if (conceptRes.ok) {
|
|
1341
|
+
const conceptJson = await conceptRes.json();
|
|
1342
|
+
if (conceptJson.results) {
|
|
1343
|
+
conceptData = conceptJson.results.map((c) => ({
|
|
1344
|
+
name: c.display_name,
|
|
1345
|
+
score: c.relevance_score || 0,
|
|
1346
|
+
works_count: c.works_count || 0,
|
|
1347
|
+
}));
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
catch { /* continue */ }
|
|
1352
|
+
// Query for recent high-impact works
|
|
1353
|
+
let recentWorks = [];
|
|
1354
|
+
try {
|
|
1355
|
+
const worksRes = await fetch(`https://api.openalex.org/works?search=${encodedTopic}&sort=cited_by_count:desc&per_page=10&filter=from_publication_date:2020-01-01`, { headers: { 'User-Agent': 'KBot/3.0 (mailto:kernel.chat@gmail.com)' }, signal: AbortSignal.timeout(10000) });
|
|
1356
|
+
if (worksRes.ok) {
|
|
1357
|
+
const worksJson = await worksRes.json();
|
|
1358
|
+
if (worksJson.results) {
|
|
1359
|
+
recentWorks = worksJson.results.map((w) => ({
|
|
1360
|
+
title: w.title,
|
|
1361
|
+
year: w.publication_year,
|
|
1362
|
+
cited: w.cited_by_count,
|
|
1363
|
+
}));
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
catch { /* continue */ }
|
|
1368
|
+
// Publication trends
|
|
1369
|
+
if (yearData.length > 0) {
|
|
1370
|
+
lines.push(`## Publication Trends`);
|
|
1371
|
+
lines.push('');
|
|
1372
|
+
lines.push(`| Year | Publications |`);
|
|
1373
|
+
lines.push(`|------|-------------|`);
|
|
1374
|
+
for (const yd of yearData) {
|
|
1375
|
+
const bar = '#'.repeat(Math.min(40, Math.round(yd.count / Math.max(1, Math.max(...yearData.map(y => y.count))) * 40)));
|
|
1376
|
+
lines.push(`| ${yd.year} | ${yd.count.toLocaleString()} ${bar} |`);
|
|
1377
|
+
}
|
|
1378
|
+
lines.push('');
|
|
1379
|
+
// Trend analysis
|
|
1380
|
+
if (yearData.length >= 3) {
|
|
1381
|
+
const counts = yearData.map(y => y.count);
|
|
1382
|
+
const recentGrowth = counts.length >= 2
|
|
1383
|
+
? ((counts[counts.length - 1] - counts[counts.length - 2]) / Math.max(1, counts[counts.length - 2]) * 100)
|
|
1384
|
+
: 0;
|
|
1385
|
+
const overallTrend = counts[counts.length - 1] > counts[0] ? 'growing' : counts[counts.length - 1] < counts[0] ? 'declining' : 'stable';
|
|
1386
|
+
lines.push(`**Trend**: ${overallTrend} (recent YoY change: ${recentGrowth > 0 ? '+' : ''}${recentGrowth.toFixed(1)}%)`);
|
|
1387
|
+
lines.push('');
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
else {
|
|
1391
|
+
lines.push(`> Could not retrieve publication trends from OpenAlex. The API may be temporarily unavailable.`);
|
|
1392
|
+
lines.push('');
|
|
1393
|
+
}
|
|
1394
|
+
// Related concepts
|
|
1395
|
+
if (conceptData.length > 0) {
|
|
1396
|
+
lines.push(`## Related Concepts & Subtopics`);
|
|
1397
|
+
lines.push('');
|
|
1398
|
+
const sortedByWorks = [...conceptData].sort((a, b) => b.works_count - a.works_count);
|
|
1399
|
+
const mostExplored = sortedByWorks.slice(0, 5);
|
|
1400
|
+
const leastExplored = sortedByWorks.slice(-5).reverse();
|
|
1401
|
+
lines.push(`### Most-Explored Subtopics`);
|
|
1402
|
+
lines.push('');
|
|
1403
|
+
lines.push(`| Concept | Works Count |`);
|
|
1404
|
+
lines.push(`|---------|-------------|`);
|
|
1405
|
+
for (const c of mostExplored) {
|
|
1406
|
+
lines.push(`| ${c.name} | ${c.works_count.toLocaleString()} |`);
|
|
1407
|
+
}
|
|
1408
|
+
lines.push('');
|
|
1409
|
+
lines.push(`### Least-Explored Subtopics (Potential Gaps)`);
|
|
1410
|
+
lines.push('');
|
|
1411
|
+
lines.push(`| Concept | Works Count |`);
|
|
1412
|
+
lines.push(`|---------|-------------|`);
|
|
1413
|
+
for (const c of leastExplored) {
|
|
1414
|
+
lines.push(`| ${c.name} | ${c.works_count.toLocaleString()} |`);
|
|
1415
|
+
}
|
|
1416
|
+
lines.push('');
|
|
1417
|
+
}
|
|
1418
|
+
// High-impact recent works
|
|
1419
|
+
if (recentWorks.length > 0) {
|
|
1420
|
+
lines.push(`## High-Impact Recent Works`);
|
|
1421
|
+
lines.push('');
|
|
1422
|
+
lines.push(`| Title | Year | Citations |`);
|
|
1423
|
+
lines.push(`|-------|------|-----------|`);
|
|
1424
|
+
for (const w of recentWorks.slice(0, 8)) {
|
|
1425
|
+
lines.push(`| ${w.title.slice(0, 80)}${w.title.length > 80 ? '...' : ''} | ${w.year} | ${w.cited} |`);
|
|
1426
|
+
}
|
|
1427
|
+
lines.push('');
|
|
1428
|
+
}
|
|
1429
|
+
// Identified gaps and suggested research questions
|
|
1430
|
+
lines.push(`## Identified Gaps`);
|
|
1431
|
+
lines.push('');
|
|
1432
|
+
const gaps = [];
|
|
1433
|
+
if (conceptData.length > 0) {
|
|
1434
|
+
const leastExplored = [...conceptData].sort((a, b) => a.works_count - b.works_count).slice(0, 3);
|
|
1435
|
+
for (const c of leastExplored) {
|
|
1436
|
+
gaps.push(`**${c.name}** has relatively few publications (${c.works_count.toLocaleString()} works) — may represent an underexplored connection to ${topic}.`);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
if (yearData.length >= 3) {
|
|
1440
|
+
const counts = yearData.map(y => y.count);
|
|
1441
|
+
const recentAvg = mean(counts.slice(-3));
|
|
1442
|
+
const earlyAvg = mean(counts.slice(0, 3));
|
|
1443
|
+
if (recentAvg > earlyAvg * 1.5) {
|
|
1444
|
+
gaps.push(`The field is experiencing rapid growth — new sub-areas are likely emerging faster than the literature can consolidate. Look for contradictory findings or unresolved debates.`);
|
|
1445
|
+
}
|
|
1446
|
+
if (recentAvg < earlyAvg * 0.7) {
|
|
1447
|
+
gaps.push(`Publication volume is declining — this may indicate the low-hanging fruit has been picked. High-impact work may require novel methodologies or interdisciplinary approaches.`);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (gaps.length === 0) {
|
|
1451
|
+
gaps.push(`Direct gap identification requires deeper analysis. Use the suggested research questions below as starting points.`);
|
|
1452
|
+
}
|
|
1453
|
+
for (const g of gaps) {
|
|
1454
|
+
lines.push(`- ${g}`);
|
|
1455
|
+
}
|
|
1456
|
+
lines.push('');
|
|
1457
|
+
lines.push(`## Suggested Research Questions`);
|
|
1458
|
+
lines.push('');
|
|
1459
|
+
lines.push(`1. What mechanisms underlie the relationship between ${topic} and ${conceptData[0]?.name || 'related phenomena'} in ${field}?`);
|
|
1460
|
+
lines.push(`2. How do recent findings in ${topic} translate to ${conceptData[conceptData.length - 1]?.name || 'adjacent fields'}?`);
|
|
1461
|
+
lines.push(`3. What methodological limitations in current ${topic} research could be addressed by ${field === 'computer_science' ? 'novel computational approaches' : 'new experimental paradigms'}?`);
|
|
1462
|
+
lines.push(`4. Are the reported effect sizes in ${topic} research reproducible? (Use \`meta_analysis\` and \`reproducibility_check\` to investigate)`);
|
|
1463
|
+
lines.push(`5. What cross-disciplinary connections between ${topic} and underexplored areas remain untested?`);
|
|
1464
|
+
return lines.join('\n');
|
|
1465
|
+
},
|
|
1466
|
+
});
|
|
1467
|
+
// =========================================================================
|
|
1468
|
+
// 6. meta_analysis
|
|
1469
|
+
// =========================================================================
|
|
1470
|
+
registerTool({
|
|
1471
|
+
name: 'meta_analysis',
|
|
1472
|
+
description: 'Perform a meta-analysis on reported effect sizes from multiple studies. Supports fixed-effects (inverse-variance) and random-effects (DerSimonian-Laird) models. Reports pooled effect, CI, heterogeneity (I², Q), forest plot data, and publication bias (Egger\'s test).',
|
|
1473
|
+
parameters: {
|
|
1474
|
+
studies: {
|
|
1475
|
+
type: 'string',
|
|
1476
|
+
description: 'JSON array of studies: [{name, effect_size, se, n, weight?}, ...]',
|
|
1477
|
+
required: true,
|
|
1478
|
+
},
|
|
1479
|
+
method: {
|
|
1480
|
+
type: 'string',
|
|
1481
|
+
description: 'Meta-analysis method: fixed or random (default: random)',
|
|
1482
|
+
},
|
|
1483
|
+
},
|
|
1484
|
+
tier: 'free',
|
|
1485
|
+
async execute(args) {
|
|
1486
|
+
const method = (args.method ? String(args.method) : 'random').toLowerCase();
|
|
1487
|
+
let studies;
|
|
1488
|
+
try {
|
|
1489
|
+
studies = JSON.parse(String(args.studies));
|
|
1490
|
+
if (!Array.isArray(studies) || studies.length < 2) {
|
|
1491
|
+
return '**Error**: Need at least 2 studies for meta-analysis.';
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
catch {
|
|
1495
|
+
return '**Error**: Invalid JSON. Expected array of {name, effect_size, se, n} objects.';
|
|
1496
|
+
}
|
|
1497
|
+
const k = studies.length; // number of studies
|
|
1498
|
+
// Fixed-effects: inverse-variance weighted
|
|
1499
|
+
const fixedWeights = studies.map(s => 1 / (s.se * s.se));
|
|
1500
|
+
const totalFixedWeight = fixedWeights.reduce((a, b) => a + b, 0);
|
|
1501
|
+
const fixedPooled = fixedWeights.reduce((sum, w, i) => sum + w * studies[i].effect_size, 0) / totalFixedWeight;
|
|
1502
|
+
const fixedSE = Math.sqrt(1 / totalFixedWeight);
|
|
1503
|
+
const fixedCI = [fixedPooled - 1.96 * fixedSE, fixedPooled + 1.96 * fixedSE];
|
|
1504
|
+
// Q statistic for heterogeneity
|
|
1505
|
+
const Q = fixedWeights.reduce((sum, w, i) => sum + w * (studies[i].effect_size - fixedPooled) ** 2, 0);
|
|
1506
|
+
const df = k - 1;
|
|
1507
|
+
const qPValue = 1 - chiSquaredCDF(Q, df);
|
|
1508
|
+
// I² statistic
|
|
1509
|
+
const I2 = Math.max(0, ((Q - df) / Q) * 100);
|
|
1510
|
+
// DerSimonian-Laird estimator for tau² (between-study variance)
|
|
1511
|
+
const C = totalFixedWeight - fixedWeights.reduce((s, w) => s + w * w, 0) / totalFixedWeight;
|
|
1512
|
+
const tau2 = Math.max(0, (Q - df) / C);
|
|
1513
|
+
// Random-effects weights
|
|
1514
|
+
const randomWeights = studies.map(s => 1 / (s.se * s.se + tau2));
|
|
1515
|
+
const totalRandomWeight = randomWeights.reduce((a, b) => a + b, 0);
|
|
1516
|
+
const randomPooled = randomWeights.reduce((sum, w, i) => sum + w * studies[i].effect_size, 0) / totalRandomWeight;
|
|
1517
|
+
const randomSE = Math.sqrt(1 / totalRandomWeight);
|
|
1518
|
+
const randomCI = [randomPooled - 1.96 * randomSE, randomPooled + 1.96 * randomSE];
|
|
1519
|
+
// Select model
|
|
1520
|
+
const isRandom = method === 'random';
|
|
1521
|
+
const pooled = isRandom ? randomPooled : fixedPooled;
|
|
1522
|
+
const pooledSE = isRandom ? randomSE : fixedSE;
|
|
1523
|
+
const ci = isRandom ? randomCI : fixedCI;
|
|
1524
|
+
const weights = isRandom ? randomWeights : fixedWeights;
|
|
1525
|
+
const totalWeight = isRandom ? totalRandomWeight : totalFixedWeight;
|
|
1526
|
+
// Egger's test for publication bias (weighted regression of effect / SE on 1/SE)
|
|
1527
|
+
const precision = studies.map(s => 1 / s.se);
|
|
1528
|
+
const snd = studies.map(s => s.effect_size / s.se); // standard normal deviate
|
|
1529
|
+
// Weighted linear regression: SND = a + b * precision
|
|
1530
|
+
const n = studies.length;
|
|
1531
|
+
const mPrec = mean(precision);
|
|
1532
|
+
const mSND = mean(snd);
|
|
1533
|
+
let ssPrec = 0, ssPrecSND = 0;
|
|
1534
|
+
for (let i = 0; i < n; i++) {
|
|
1535
|
+
ssPrec += (precision[i] - mPrec) ** 2;
|
|
1536
|
+
ssPrecSND += (precision[i] - mPrec) * (snd[i] - mSND);
|
|
1537
|
+
}
|
|
1538
|
+
const eggerSlope = ssPrec > 0 ? ssPrecSND / ssPrec : 0;
|
|
1539
|
+
const eggerIntercept = mSND - eggerSlope * mPrec;
|
|
1540
|
+
// t-test for intercept
|
|
1541
|
+
const residuals = snd.map((s, i) => s - (eggerIntercept + eggerSlope * precision[i]));
|
|
1542
|
+
const residSE = Math.sqrt(residuals.reduce((s, r) => s + r * r, 0) / (n - 2));
|
|
1543
|
+
const interceptSE = residSE * Math.sqrt(1 / n + mPrec * mPrec / ssPrec);
|
|
1544
|
+
const eggerT = interceptSE > 0 ? eggerIntercept / interceptSE : 0;
|
|
1545
|
+
const eggerP = n > 2 ? 2 * (1 - tCDF(Math.abs(eggerT), n - 2)) : 1;
|
|
1546
|
+
// Build output
|
|
1547
|
+
const lines = [
|
|
1548
|
+
`# Meta-Analysis Report`,
|
|
1549
|
+
'',
|
|
1550
|
+
`**Model**: ${isRandom ? 'Random Effects (DerSimonian-Laird)' : 'Fixed Effects (Inverse-Variance)'}`,
|
|
1551
|
+
`**Studies**: ${k}`,
|
|
1552
|
+
`**Total N**: ${studies.reduce((s, st) => s + st.n, 0)}`,
|
|
1553
|
+
'',
|
|
1554
|
+
`---`,
|
|
1555
|
+
'',
|
|
1556
|
+
`## Pooled Results`,
|
|
1557
|
+
'',
|
|
1558
|
+
`| Metric | Value |`,
|
|
1559
|
+
`|--------|-------|`,
|
|
1560
|
+
`| **Pooled Effect Size** | ${pooled.toFixed(4)} |`,
|
|
1561
|
+
`| **Standard Error** | ${pooledSE.toFixed(4)} |`,
|
|
1562
|
+
`| **95% CI** | [${ci[0].toFixed(4)}, ${ci[1].toFixed(4)}] |`,
|
|
1563
|
+
`| **Z** | ${(pooled / pooledSE).toFixed(4)} |`,
|
|
1564
|
+
`| **p-value** | ${(2 * (1 - normalCDF(Math.abs(pooled / pooledSE)))).toFixed(6)} |`,
|
|
1565
|
+
'',
|
|
1566
|
+
`## Heterogeneity`,
|
|
1567
|
+
'',
|
|
1568
|
+
`| Metric | Value |`,
|
|
1569
|
+
`|--------|-------|`,
|
|
1570
|
+
`| **Q statistic** | ${Q.toFixed(4)} (df = ${df}, p = ${qPValue < 0.001 ? '< 0.001' : qPValue.toFixed(4)}) |`,
|
|
1571
|
+
`| **I²** | ${I2.toFixed(1)}% |`,
|
|
1572
|
+
`| **tau²** | ${tau2.toFixed(6)} |`,
|
|
1573
|
+
`| **tau** | ${Math.sqrt(tau2).toFixed(4)} |`,
|
|
1574
|
+
'',
|
|
1575
|
+
I2 < 25 ? '**Interpretation**: Low heterogeneity — studies are consistent.' :
|
|
1576
|
+
I2 < 50 ? '**Interpretation**: Moderate heterogeneity — some variation between studies.' :
|
|
1577
|
+
I2 < 75 ? '**Interpretation**: Substantial heterogeneity — consider subgroup analysis or moderator search.' :
|
|
1578
|
+
'**Interpretation**: Considerable heterogeneity — pooled estimate should be interpreted cautiously.',
|
|
1579
|
+
'',
|
|
1580
|
+
`## Forest Plot Data`,
|
|
1581
|
+
'',
|
|
1582
|
+
`| Study | Effect | SE | 95% CI | Weight |`,
|
|
1583
|
+
`|-------|--------|----|--------|--------|`,
|
|
1584
|
+
];
|
|
1585
|
+
for (let i = 0; i < k; i++) {
|
|
1586
|
+
const s = studies[i];
|
|
1587
|
+
const sCI = [s.effect_size - 1.96 * s.se, s.effect_size + 1.96 * s.se];
|
|
1588
|
+
const relWeight = (weights[i] / totalWeight * 100);
|
|
1589
|
+
lines.push(`| ${s.name} | ${s.effect_size.toFixed(4)} | ${s.se.toFixed(4)} | [${sCI[0].toFixed(3)}, ${sCI[1].toFixed(3)}] | ${relWeight.toFixed(1)}% |`);
|
|
1590
|
+
}
|
|
1591
|
+
lines.push(`| **Pooled** | **${pooled.toFixed(4)}** | **${pooledSE.toFixed(4)}** | **[${ci[0].toFixed(3)}, ${ci[1].toFixed(3)}]** | **100%** |`);
|
|
1592
|
+
lines.push('');
|
|
1593
|
+
// Visual forest plot (ASCII)
|
|
1594
|
+
lines.push(`### ASCII Forest Plot`);
|
|
1595
|
+
lines.push('');
|
|
1596
|
+
const allLower = Math.min(...studies.map(s => s.effect_size - 2 * s.se), ci[0]);
|
|
1597
|
+
const allUpper = Math.max(...studies.map(s => s.effect_size + 2 * s.se), ci[1]);
|
|
1598
|
+
const range = allUpper - allLower || 1;
|
|
1599
|
+
const plotWidth = 50;
|
|
1600
|
+
for (let i = 0; i < k; i++) {
|
|
1601
|
+
const s = studies[i];
|
|
1602
|
+
const pos = Math.round(((s.effect_size - allLower) / range) * plotWidth);
|
|
1603
|
+
const ciLo = Math.round(((s.effect_size - 1.96 * s.se - allLower) / range) * plotWidth);
|
|
1604
|
+
const ciHi = Math.round(((s.effect_size + 1.96 * s.se - allLower) / range) * plotWidth);
|
|
1605
|
+
const row = new Array(plotWidth + 1).fill(' ');
|
|
1606
|
+
for (let j = Math.max(0, ciLo); j <= Math.min(plotWidth, ciHi); j++)
|
|
1607
|
+
row[j] = '-';
|
|
1608
|
+
row[Math.max(0, Math.min(plotWidth, pos))] = '*';
|
|
1609
|
+
const label = s.name.padEnd(15).slice(0, 15);
|
|
1610
|
+
lines.push(`${label} |${row.join('')}|`);
|
|
1611
|
+
}
|
|
1612
|
+
// Pooled
|
|
1613
|
+
const pooledPos = Math.round(((pooled - allLower) / range) * plotWidth);
|
|
1614
|
+
const pooledRow = new Array(plotWidth + 1).fill(' ');
|
|
1615
|
+
const pLo = Math.round(((ci[0] - allLower) / range) * plotWidth);
|
|
1616
|
+
const pHi = Math.round(((ci[1] - allLower) / range) * plotWidth);
|
|
1617
|
+
for (let j = Math.max(0, pLo); j <= Math.min(plotWidth, pHi); j++)
|
|
1618
|
+
pooledRow[j] = '=';
|
|
1619
|
+
pooledRow[Math.max(0, Math.min(plotWidth, pooledPos))] = '#';
|
|
1620
|
+
lines.push(`${'Pooled'.padEnd(15)} |${pooledRow.join('')}|`);
|
|
1621
|
+
lines.push('');
|
|
1622
|
+
// Publication bias
|
|
1623
|
+
lines.push(`## Publication Bias (Egger's Test)`);
|
|
1624
|
+
lines.push('');
|
|
1625
|
+
lines.push(`| Metric | Value |`);
|
|
1626
|
+
lines.push(`|--------|-------|`);
|
|
1627
|
+
lines.push(`| **Intercept** | ${eggerIntercept.toFixed(4)} |`);
|
|
1628
|
+
lines.push(`| **SE** | ${interceptSE.toFixed(4)} |`);
|
|
1629
|
+
lines.push(`| **t** | ${eggerT.toFixed(4)} |`);
|
|
1630
|
+
lines.push(`| **p-value** | ${eggerP < 0.001 ? '< 0.001' : eggerP.toFixed(4)} |`);
|
|
1631
|
+
lines.push('');
|
|
1632
|
+
lines.push(eggerP < 0.05
|
|
1633
|
+
? '**Warning**: Egger\'s test suggests significant funnel plot asymmetry (p < 0.05), indicating potential publication bias.'
|
|
1634
|
+
: '**Result**: No significant evidence of publication bias detected by Egger\'s test.');
|
|
1635
|
+
return lines.join('\n');
|
|
1636
|
+
},
|
|
1637
|
+
});
|
|
1638
|
+
// =========================================================================
|
|
1639
|
+
// 7. causal_inference
|
|
1640
|
+
// =========================================================================
|
|
1641
|
+
registerTool({
|
|
1642
|
+
name: 'causal_inference',
|
|
1643
|
+
description: 'Assess potential causal relationships from observational data. Supports Bradford Hill criteria scoring, Granger causality test, and correlation-vs-causation analysis.',
|
|
1644
|
+
parameters: {
|
|
1645
|
+
data: {
|
|
1646
|
+
type: 'string',
|
|
1647
|
+
description: 'JSON object with x (array), y (array), and optional confounders (array of arrays). e.g. {"x":[1,2,3],"y":[4,5,6],"confounders":[[7,8,9]]}',
|
|
1648
|
+
required: true,
|
|
1649
|
+
},
|
|
1650
|
+
method: {
|
|
1651
|
+
type: 'string',
|
|
1652
|
+
description: 'Method: correlation_vs_causation, granger, instrumental, or propensity',
|
|
1653
|
+
required: true,
|
|
1654
|
+
},
|
|
1655
|
+
},
|
|
1656
|
+
tier: 'free',
|
|
1657
|
+
async execute(args) {
|
|
1658
|
+
const method = String(args.method).toLowerCase();
|
|
1659
|
+
let data;
|
|
1660
|
+
try {
|
|
1661
|
+
data = JSON.parse(String(args.data));
|
|
1662
|
+
if (!Array.isArray(data.x) || !Array.isArray(data.y))
|
|
1663
|
+
throw new Error('x and y must be arrays');
|
|
1664
|
+
data.x = data.x.map(Number);
|
|
1665
|
+
data.y = data.y.map(Number);
|
|
1666
|
+
if (data.confounders) {
|
|
1667
|
+
data.confounders = data.confounders.map(c => c.map(Number));
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
catch (e) {
|
|
1671
|
+
return `**Error**: Invalid data JSON. Expected {x: number[], y: number[], confounders?: number[][]}. ${e instanceof Error ? e.message : ''}`;
|
|
1672
|
+
}
|
|
1673
|
+
const n = Math.min(data.x.length, data.y.length);
|
|
1674
|
+
if (n < 5)
|
|
1675
|
+
return '**Error**: Need at least 5 data points for causal inference.';
|
|
1676
|
+
const x = data.x.slice(0, n);
|
|
1677
|
+
const y = data.y.slice(0, n);
|
|
1678
|
+
const r = pearsonCorrelation(x, y);
|
|
1679
|
+
const lines = [
|
|
1680
|
+
`# Causal Inference Report`,
|
|
1681
|
+
'',
|
|
1682
|
+
`**Method**: ${method}`,
|
|
1683
|
+
`**N**: ${n}`,
|
|
1684
|
+
`**Pearson r (X, Y)**: ${r.toFixed(4)}`,
|
|
1685
|
+
'',
|
|
1686
|
+
`---`,
|
|
1687
|
+
'',
|
|
1688
|
+
];
|
|
1689
|
+
if (method === 'correlation_vs_causation') {
|
|
1690
|
+
// Bradford Hill criteria evaluation
|
|
1691
|
+
lines.push(`## Bradford Hill Criteria Assessment`);
|
|
1692
|
+
lines.push('');
|
|
1693
|
+
lines.push(`These 9 criteria help evaluate whether an observed association is likely causal.`);
|
|
1694
|
+
lines.push('');
|
|
1695
|
+
const criteria = [];
|
|
1696
|
+
// 1. Strength of association
|
|
1697
|
+
const absR = Math.abs(r);
|
|
1698
|
+
const strengthScore = absR > 0.7 ? 3 : absR > 0.4 ? 2 : absR > 0.2 ? 1 : 0;
|
|
1699
|
+
criteria.push({
|
|
1700
|
+
name: 'Strength of Association',
|
|
1701
|
+
score: strengthScore,
|
|
1702
|
+
maxScore: 3,
|
|
1703
|
+
rationale: `|r| = ${absR.toFixed(4)} — ${absR > 0.7 ? 'strong' : absR > 0.4 ? 'moderate' : absR > 0.2 ? 'weak' : 'negligible'} correlation.`,
|
|
1704
|
+
});
|
|
1705
|
+
// 2. Consistency
|
|
1706
|
+
// Without multiple studies, we estimate from data stability
|
|
1707
|
+
const halfN = Math.floor(n / 2);
|
|
1708
|
+
const r1 = pearsonCorrelation(x.slice(0, halfN), y.slice(0, halfN));
|
|
1709
|
+
const r2 = pearsonCorrelation(x.slice(halfN), y.slice(halfN));
|
|
1710
|
+
const consistency = Math.abs(r1 - r2) < 0.3 ? 2 : Math.abs(r1 - r2) < 0.5 ? 1 : 0;
|
|
1711
|
+
criteria.push({
|
|
1712
|
+
name: 'Consistency',
|
|
1713
|
+
score: consistency,
|
|
1714
|
+
maxScore: 2,
|
|
1715
|
+
rationale: `Split-half correlation: r1 = ${r1.toFixed(3)}, r2 = ${r2.toFixed(3)} (diff = ${Math.abs(r1 - r2).toFixed(3)}). ${consistency === 2 ? 'Consistent' : consistency === 1 ? 'Somewhat consistent' : 'Inconsistent'} across subsets.`,
|
|
1716
|
+
});
|
|
1717
|
+
// 3. Specificity
|
|
1718
|
+
let specificityScore = 2;
|
|
1719
|
+
let specificityRationale = 'Cannot fully assess without multiple outcome variables. Default moderate score.';
|
|
1720
|
+
if (data.confounders && data.confounders.length > 0) {
|
|
1721
|
+
const confR = data.confounders.map(c => Math.abs(pearsonCorrelation(x, c.slice(0, n))));
|
|
1722
|
+
const maxConfR = Math.max(...confR);
|
|
1723
|
+
specificityScore = maxConfR > absR ? 0 : maxConfR > absR * 0.7 ? 1 : 2;
|
|
1724
|
+
specificityRationale = `Max |r| with confounders = ${maxConfR.toFixed(3)}. ${specificityScore === 2 ? 'X-Y association is specific' : 'Confounders show comparable correlations — low specificity'}.`;
|
|
1725
|
+
}
|
|
1726
|
+
criteria.push({
|
|
1727
|
+
name: 'Specificity',
|
|
1728
|
+
score: specificityScore,
|
|
1729
|
+
maxScore: 2,
|
|
1730
|
+
rationale: specificityRationale,
|
|
1731
|
+
});
|
|
1732
|
+
// 4. Temporality (check if x precedes y in time series sense)
|
|
1733
|
+
// Lag-1 cross-correlation
|
|
1734
|
+
let lagCorr = 0;
|
|
1735
|
+
if (n > 3) {
|
|
1736
|
+
lagCorr = pearsonCorrelation(x.slice(0, n - 1), y.slice(1));
|
|
1737
|
+
}
|
|
1738
|
+
const temporalScore = Math.abs(lagCorr) > Math.abs(r) * 0.5 ? 2 : Math.abs(lagCorr) > 0.1 ? 1 : 0;
|
|
1739
|
+
criteria.push({
|
|
1740
|
+
name: 'Temporality',
|
|
1741
|
+
score: temporalScore,
|
|
1742
|
+
maxScore: 2,
|
|
1743
|
+
rationale: `Lag-1 cross-correlation (X[t] → Y[t+1]) = ${lagCorr.toFixed(4)}. ${temporalScore === 2 ? 'Temporal precedence supported' : temporalScore === 1 ? 'Weak temporal precedence' : 'No clear temporal precedence'}.`,
|
|
1744
|
+
});
|
|
1745
|
+
// 5. Biological gradient (dose-response)
|
|
1746
|
+
// Check monotonicity
|
|
1747
|
+
const sortedByX = x.map((xi, i) => ({ x: xi, y: y[i] })).sort((a, b) => a.x - b.x);
|
|
1748
|
+
let monotonic = 0;
|
|
1749
|
+
for (let i = 1; i < sortedByX.length; i++) {
|
|
1750
|
+
if (sortedByX[i].y >= sortedByX[i - 1].y)
|
|
1751
|
+
monotonic++;
|
|
1752
|
+
else
|
|
1753
|
+
monotonic--;
|
|
1754
|
+
}
|
|
1755
|
+
const monoPct = Math.abs(monotonic) / (sortedByX.length - 1);
|
|
1756
|
+
const gradientScore = monoPct > 0.7 ? 2 : monoPct > 0.5 ? 1 : 0;
|
|
1757
|
+
criteria.push({
|
|
1758
|
+
name: 'Biological Gradient',
|
|
1759
|
+
score: gradientScore,
|
|
1760
|
+
maxScore: 2,
|
|
1761
|
+
rationale: `Monotonicity score = ${(monoPct * 100).toFixed(1)}%. ${gradientScore === 2 ? 'Clear dose-response' : gradientScore === 1 ? 'Partial dose-response' : 'No dose-response pattern'}.`,
|
|
1762
|
+
});
|
|
1763
|
+
// 6-9: Plausibility, Coherence, Experiment, Analogy
|
|
1764
|
+
// These require domain knowledge — assign default moderate scores with explanations
|
|
1765
|
+
criteria.push({
|
|
1766
|
+
name: 'Plausibility',
|
|
1767
|
+
score: 1,
|
|
1768
|
+
maxScore: 2,
|
|
1769
|
+
rationale: 'Requires domain expertise to assess. Score 1/2 (neutral) — provide domain context for better assessment.',
|
|
1770
|
+
});
|
|
1771
|
+
criteria.push({
|
|
1772
|
+
name: 'Coherence',
|
|
1773
|
+
score: 1,
|
|
1774
|
+
maxScore: 2,
|
|
1775
|
+
rationale: 'Requires knowledge of existing theory. Score 1/2 (neutral) — provide domain context for better assessment.',
|
|
1776
|
+
});
|
|
1777
|
+
criteria.push({
|
|
1778
|
+
name: 'Experiment',
|
|
1779
|
+
score: 0,
|
|
1780
|
+
maxScore: 2,
|
|
1781
|
+
rationale: 'No experimental evidence provided (observational data only). Experimental confirmation would strengthen causal claim.',
|
|
1782
|
+
});
|
|
1783
|
+
criteria.push({
|
|
1784
|
+
name: 'Analogy',
|
|
1785
|
+
score: 1,
|
|
1786
|
+
maxScore: 2,
|
|
1787
|
+
rationale: 'Requires knowledge of analogous systems. Score 1/2 (neutral).',
|
|
1788
|
+
});
|
|
1789
|
+
const totalScore = criteria.reduce((s, c) => s + c.score, 0);
|
|
1790
|
+
const maxScore = criteria.reduce((s, c) => s + c.maxScore, 0);
|
|
1791
|
+
const causalPct = (totalScore / maxScore) * 100;
|
|
1792
|
+
lines.push(`| Criterion | Score | Max | Rationale |`);
|
|
1793
|
+
lines.push(`|-----------|-------|-----|-----------|`);
|
|
1794
|
+
for (const c of criteria) {
|
|
1795
|
+
lines.push(`| ${c.name} | ${c.score} | ${c.maxScore} | ${c.rationale} |`);
|
|
1796
|
+
}
|
|
1797
|
+
lines.push(`| **Total** | **${totalScore}** | **${maxScore}** | **${causalPct.toFixed(0)}% causal plausibility** |`);
|
|
1798
|
+
lines.push('');
|
|
1799
|
+
const verdict = causalPct >= 70 ? 'Strong support for causal relationship'
|
|
1800
|
+
: causalPct >= 50 ? 'Moderate support — further investigation warranted'
|
|
1801
|
+
: causalPct >= 30 ? 'Weak support — likely correlational, not causal'
|
|
1802
|
+
: 'Little evidence for causation';
|
|
1803
|
+
lines.push(`## Verdict: ${verdict}`);
|
|
1804
|
+
}
|
|
1805
|
+
else if (method === 'granger') {
|
|
1806
|
+
// Granger causality test
|
|
1807
|
+
lines.push(`## Granger Causality Test`);
|
|
1808
|
+
lines.push('');
|
|
1809
|
+
lines.push(`Tests whether past values of X help predict Y beyond Y's own past values.`);
|
|
1810
|
+
lines.push('');
|
|
1811
|
+
// Try lags 1-4
|
|
1812
|
+
const maxLag = Math.min(4, Math.floor(n / 5));
|
|
1813
|
+
for (let lag = 1; lag <= maxLag; lag++) {
|
|
1814
|
+
// Restricted model: Y[t] = a0 + a1*Y[t-1] + ... + aL*Y[t-L]
|
|
1815
|
+
// Unrestricted model: Y[t] = a0 + a1*Y[t-1] + ... + aL*Y[t-L] + b1*X[t-1] + ... + bL*X[t-L]
|
|
1816
|
+
const effectiveN = n - lag;
|
|
1817
|
+
// Build design matrices
|
|
1818
|
+
// Restricted: just lagged Y
|
|
1819
|
+
const yTarget = [];
|
|
1820
|
+
const xRestricted = [];
|
|
1821
|
+
const xUnrestricted = [];
|
|
1822
|
+
for (let t = lag; t < n; t++) {
|
|
1823
|
+
yTarget.push(y[t]);
|
|
1824
|
+
const rowR = [1]; // intercept
|
|
1825
|
+
const rowU = [1];
|
|
1826
|
+
for (let l = 1; l <= lag; l++) {
|
|
1827
|
+
rowR.push(y[t - l]);
|
|
1828
|
+
rowU.push(y[t - l]);
|
|
1829
|
+
}
|
|
1830
|
+
for (let l = 1; l <= lag; l++) {
|
|
1831
|
+
rowU.push(x[t - l]);
|
|
1832
|
+
}
|
|
1833
|
+
xRestricted.push(rowR);
|
|
1834
|
+
xUnrestricted.push(rowU);
|
|
1835
|
+
}
|
|
1836
|
+
// OLS: b = (X'X)^-1 X'y — simplified for small matrices
|
|
1837
|
+
function olsResidualSS(X, y) {
|
|
1838
|
+
const m = X[0].length;
|
|
1839
|
+
const n = X.length;
|
|
1840
|
+
// X'X
|
|
1841
|
+
const XtX = Array.from({ length: m }, () => new Array(m).fill(0));
|
|
1842
|
+
const Xty = new Array(m).fill(0);
|
|
1843
|
+
for (let i = 0; i < n; i++) {
|
|
1844
|
+
for (let j = 0; j < m; j++) {
|
|
1845
|
+
Xty[j] += X[i][j] * y[i];
|
|
1846
|
+
for (let k = 0; k < m; k++) {
|
|
1847
|
+
XtX[j][k] += X[i][j] * X[i][k];
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
// Solve via Gaussian elimination
|
|
1852
|
+
const aug = XtX.map((row, i) => [...row, Xty[i]]);
|
|
1853
|
+
for (let col = 0; col < m; col++) {
|
|
1854
|
+
// Pivot
|
|
1855
|
+
let maxRow = col;
|
|
1856
|
+
for (let row = col + 1; row < m; row++) {
|
|
1857
|
+
if (Math.abs(aug[row][col]) > Math.abs(aug[maxRow][col]))
|
|
1858
|
+
maxRow = row;
|
|
1859
|
+
}
|
|
1860
|
+
[aug[col], aug[maxRow]] = [aug[maxRow], aug[col]];
|
|
1861
|
+
if (Math.abs(aug[col][col]) < 1e-12)
|
|
1862
|
+
continue;
|
|
1863
|
+
for (let row = 0; row < m; row++) {
|
|
1864
|
+
if (row === col)
|
|
1865
|
+
continue;
|
|
1866
|
+
const factor = aug[row][col] / aug[col][col];
|
|
1867
|
+
for (let j = col; j <= m; j++) {
|
|
1868
|
+
aug[row][j] -= factor * aug[col][j];
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
const beta = aug.map((row, i) => Math.abs(row[i]) > 1e-12 ? row[m] / row[i] : 0);
|
|
1873
|
+
// Compute residuals
|
|
1874
|
+
let rss = 0;
|
|
1875
|
+
for (let i = 0; i < n; i++) {
|
|
1876
|
+
let pred = 0;
|
|
1877
|
+
for (let j = 0; j < m; j++)
|
|
1878
|
+
pred += X[i][j] * beta[j];
|
|
1879
|
+
rss += (y[i] - pred) ** 2;
|
|
1880
|
+
}
|
|
1881
|
+
return rss;
|
|
1882
|
+
}
|
|
1883
|
+
const rssR = olsResidualSS(xRestricted, yTarget);
|
|
1884
|
+
const rssU = olsResidualSS(xUnrestricted, yTarget);
|
|
1885
|
+
// F-test: ((RSS_R - RSS_U) / q) / (RSS_U / (n - p))
|
|
1886
|
+
const q = lag; // number of restrictions
|
|
1887
|
+
const p = 1 + 2 * lag; // unrestricted model params
|
|
1888
|
+
const dfResidual = effectiveN - p;
|
|
1889
|
+
if (dfResidual <= 0) {
|
|
1890
|
+
lines.push(`**Lag ${lag}**: Insufficient data (need > ${p} observations after lagging).`);
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
const fStat = dfResidual > 0 ? ((rssR - rssU) / q) / (rssU / dfResidual) : 0;
|
|
1894
|
+
// F to p-value approximation
|
|
1895
|
+
const fPValue = 1 - regularizedGammaP(q / 2, q * fStat / (q * fStat + dfResidual) * dfResidual / 2);
|
|
1896
|
+
const significant = fPValue < 0.05;
|
|
1897
|
+
lines.push(`### Lag = ${lag}`);
|
|
1898
|
+
lines.push('');
|
|
1899
|
+
lines.push(`| Metric | Value |`);
|
|
1900
|
+
lines.push(`|--------|-------|`);
|
|
1901
|
+
lines.push(`| RSS (restricted) | ${rssR.toFixed(4)} |`);
|
|
1902
|
+
lines.push(`| RSS (unrestricted) | ${rssU.toFixed(4)} |`);
|
|
1903
|
+
lines.push(`| F-statistic | ${fStat.toFixed(4)} |`);
|
|
1904
|
+
lines.push(`| df1, df2 | ${q}, ${dfResidual} |`);
|
|
1905
|
+
lines.push(`| p-value | ${fPValue < 0.001 ? '< 0.001' : fPValue.toFixed(4)} |`);
|
|
1906
|
+
lines.push(`| **X Granger-causes Y?** | **${significant ? 'Yes' : 'No'}** |`);
|
|
1907
|
+
lines.push('');
|
|
1908
|
+
}
|
|
1909
|
+
lines.push(`## Caveats`);
|
|
1910
|
+
lines.push('');
|
|
1911
|
+
lines.push(`- Granger causality tests temporal precedence, not true causation.`);
|
|
1912
|
+
lines.push(`- Assumes linear relationships and stationary time series.`);
|
|
1913
|
+
lines.push(`- Confounders can produce spurious Granger causality.`);
|
|
1914
|
+
lines.push(`- Consider differencing non-stationary data before testing.`);
|
|
1915
|
+
}
|
|
1916
|
+
else if (method === 'instrumental' || method === 'propensity') {
|
|
1917
|
+
lines.push(`## ${method === 'instrumental' ? 'Instrumental Variables' : 'Propensity Score'} Analysis`);
|
|
1918
|
+
lines.push('');
|
|
1919
|
+
if (method === 'instrumental') {
|
|
1920
|
+
if (!data.confounders || data.confounders.length === 0) {
|
|
1921
|
+
lines.push(`**Error**: Instrumental variables analysis requires at least one instrument in the confounders array.`);
|
|
1922
|
+
lines.push('');
|
|
1923
|
+
lines.push(`Provide data as: {"x": [...], "y": [...], "confounders": [[instrument_values]]}`);
|
|
1924
|
+
lines.push('');
|
|
1925
|
+
lines.push(`A valid instrument must:`);
|
|
1926
|
+
lines.push(`1. Be correlated with X (relevance)`);
|
|
1927
|
+
lines.push(`2. Affect Y only through X (exclusion restriction)`);
|
|
1928
|
+
}
|
|
1929
|
+
else {
|
|
1930
|
+
const z = data.confounders[0].slice(0, n);
|
|
1931
|
+
const rZX = pearsonCorrelation(z, x);
|
|
1932
|
+
const rZY = pearsonCorrelation(z, y);
|
|
1933
|
+
// 2SLS: first stage X = a + b*Z, second stage Y = c + d*X_hat
|
|
1934
|
+
// Simple IV estimator: beta_IV = Cov(Z,Y) / Cov(Z,X)
|
|
1935
|
+
const mz = mean(z), mx = mean(x), my = mean(y);
|
|
1936
|
+
let covZX = 0, covZY = 0;
|
|
1937
|
+
for (let i = 0; i < n; i++) {
|
|
1938
|
+
covZX += (z[i] - mz) * (x[i] - mx);
|
|
1939
|
+
covZY += (z[i] - mz) * (y[i] - my);
|
|
1940
|
+
}
|
|
1941
|
+
covZX /= (n - 1);
|
|
1942
|
+
covZY /= (n - 1);
|
|
1943
|
+
const ivEstimate = covZX !== 0 ? covZY / covZX : NaN;
|
|
1944
|
+
const olsEstimate = pearsonCorrelation(x, y) * stddev(y) / (stddev(x) || 1);
|
|
1945
|
+
// First-stage F-statistic (instrument strength)
|
|
1946
|
+
const rZX2 = rZX * rZX;
|
|
1947
|
+
const firstStageF = n > 2 ? rZX2 / (1 - rZX2) * (n - 2) : 0;
|
|
1948
|
+
lines.push(`| Metric | Value |`);
|
|
1949
|
+
lines.push(`|--------|-------|`);
|
|
1950
|
+
lines.push(`| **OLS Estimate** | ${olsEstimate.toFixed(4)} |`);
|
|
1951
|
+
lines.push(`| **IV Estimate** | ${isNaN(ivEstimate) ? 'N/A (weak instrument)' : ivEstimate.toFixed(4)} |`);
|
|
1952
|
+
lines.push(`| **r(Z, X)** | ${rZX.toFixed(4)} (instrument relevance) |`);
|
|
1953
|
+
lines.push(`| **r(Z, Y)** | ${rZY.toFixed(4)} (reduced form) |`);
|
|
1954
|
+
lines.push(`| **First-stage F** | ${firstStageF.toFixed(2)} ${firstStageF < 10 ? '(WEAK — below threshold of 10)' : '(adequate)'} |`);
|
|
1955
|
+
lines.push('');
|
|
1956
|
+
if (firstStageF < 10) {
|
|
1957
|
+
lines.push(`**Warning**: The instrument is weak (F < 10). IV estimates may be severely biased.`);
|
|
1958
|
+
}
|
|
1959
|
+
lines.push('');
|
|
1960
|
+
lines.push(`**Interpretation**: ${Math.abs(ivEstimate - olsEstimate) > 0.1 * Math.abs(olsEstimate) ? 'The IV and OLS estimates differ substantially, suggesting endogeneity bias in OLS.' : 'IV and OLS estimates are similar, suggesting limited endogeneity.'}`);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
else {
|
|
1964
|
+
// Propensity score analysis
|
|
1965
|
+
lines.push(`**Note**: Propensity score analysis requires a binary treatment variable.`);
|
|
1966
|
+
lines.push('');
|
|
1967
|
+
lines.push(`Converting X to binary treatment using median split.`);
|
|
1968
|
+
lines.push('');
|
|
1969
|
+
const medX = median(x);
|
|
1970
|
+
const treated = y.filter((_, i) => x[i] >= medX);
|
|
1971
|
+
const control = y.filter((_, i) => x[i] < medX);
|
|
1972
|
+
const treatedMean = mean(treated);
|
|
1973
|
+
const controlMean = mean(control);
|
|
1974
|
+
const naiveATE = treatedMean - controlMean;
|
|
1975
|
+
// Simple propensity weighting approximation
|
|
1976
|
+
// Propensity = P(treatment | confounders) — without confounders, use inverse probability
|
|
1977
|
+
const pTreat = treated.length / n;
|
|
1978
|
+
// IPW estimate
|
|
1979
|
+
let ipwNum = 0, ipwDen = 0;
|
|
1980
|
+
for (let i = 0; i < n; i++) {
|
|
1981
|
+
const t = x[i] >= medX ? 1 : 0;
|
|
1982
|
+
const p = pTreat; // simplified — true propensity score needs logistic regression
|
|
1983
|
+
if (t === 1) {
|
|
1984
|
+
ipwNum += y[i] / p;
|
|
1985
|
+
ipwDen += 1 / p;
|
|
1986
|
+
}
|
|
1987
|
+
else {
|
|
1988
|
+
ipwNum -= y[i] / (1 - p);
|
|
1989
|
+
ipwDen += 1 / (1 - p);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
// Note: with constant propensity, IPW = naive ATE
|
|
1993
|
+
lines.push(`| Metric | Value |`);
|
|
1994
|
+
lines.push(`|--------|-------|`);
|
|
1995
|
+
lines.push(`| **Treated group mean** | ${treatedMean.toFixed(4)} (n=${treated.length}) |`);
|
|
1996
|
+
lines.push(`| **Control group mean** | ${controlMean.toFixed(4)} (n=${control.length}) |`);
|
|
1997
|
+
lines.push(`| **Naive ATE** | ${naiveATE.toFixed(4)} |`);
|
|
1998
|
+
lines.push(`| **P(treatment)** | ${pTreat.toFixed(4)} |`);
|
|
1999
|
+
lines.push('');
|
|
2000
|
+
if (data.confounders && data.confounders.length > 0) {
|
|
2001
|
+
lines.push(`### Confounder Balance`);
|
|
2002
|
+
lines.push('');
|
|
2003
|
+
lines.push(`| Confounder | Treated Mean | Control Mean | Std. Diff |`);
|
|
2004
|
+
lines.push(`|------------|-------------|-------------|-----------|`);
|
|
2005
|
+
data.confounders.forEach((conf, ci) => {
|
|
2006
|
+
const cTreated = conf.slice(0, n).filter((_, i) => x[i] >= medX);
|
|
2007
|
+
const cControl = conf.slice(0, n).filter((_, i) => x[i] < medX);
|
|
2008
|
+
const pooledSD = Math.sqrt((variance(cTreated) + variance(cControl)) / 2);
|
|
2009
|
+
const stdDiff = pooledSD > 0 ? (mean(cTreated) - mean(cControl)) / pooledSD : 0;
|
|
2010
|
+
lines.push(`| Confounder ${ci + 1} | ${mean(cTreated).toFixed(3)} | ${mean(cControl).toFixed(3)} | ${stdDiff.toFixed(3)} ${Math.abs(stdDiff) > 0.1 ? '(IMBALANCED)' : ''} |`);
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
lines.push('');
|
|
2015
|
+
lines.push(`## Further Steps for Establishing Causality`);
|
|
2016
|
+
lines.push('');
|
|
2017
|
+
lines.push(`1. Conduct a randomized controlled experiment if feasible`);
|
|
2018
|
+
lines.push(`2. Test for robustness with different model specifications`);
|
|
2019
|
+
lines.push(`3. Check for sensitivity to unmeasured confounders (Rosenbaum bounds)`);
|
|
2020
|
+
lines.push(`4. Replicate findings in independent datasets`);
|
|
2021
|
+
}
|
|
2022
|
+
else {
|
|
2023
|
+
return `**Error**: Unknown method "${method}". Use: correlation_vs_causation, granger, instrumental, or propensity.`;
|
|
2024
|
+
}
|
|
2025
|
+
return lines.join('\n');
|
|
2026
|
+
},
|
|
2027
|
+
});
|
|
2028
|
+
// =========================================================================
|
|
2029
|
+
// 8. reproducibility_check
|
|
2030
|
+
// =========================================================================
|
|
2031
|
+
registerTool({
|
|
2032
|
+
name: 'reproducibility_check',
|
|
2033
|
+
description: 'Analyze a study\'s methodology for reproducibility risks. Computes positive predictive value (PPV), R-index, and replication probability based on sample size, p-value, effect size, pre-registration status, and multiple comparisons.',
|
|
2034
|
+
parameters: {
|
|
2035
|
+
sample_size: {
|
|
2036
|
+
type: 'number',
|
|
2037
|
+
description: 'Total sample size of the study',
|
|
2038
|
+
required: true,
|
|
2039
|
+
},
|
|
2040
|
+
p_value: {
|
|
2041
|
+
type: 'number',
|
|
2042
|
+
description: 'Reported p-value',
|
|
2043
|
+
required: true,
|
|
2044
|
+
},
|
|
2045
|
+
effect_size: {
|
|
2046
|
+
type: 'number',
|
|
2047
|
+
description: "Reported Cohen's d effect size",
|
|
2048
|
+
required: true,
|
|
2049
|
+
},
|
|
2050
|
+
pre_registered: {
|
|
2051
|
+
type: 'string',
|
|
2052
|
+
description: 'Was the study pre-registered? (true/false)',
|
|
2053
|
+
required: true,
|
|
2054
|
+
},
|
|
2055
|
+
multiple_comparisons: {
|
|
2056
|
+
type: 'number',
|
|
2057
|
+
description: 'Number of statistical tests / comparisons performed (default: 1)',
|
|
2058
|
+
},
|
|
2059
|
+
},
|
|
2060
|
+
tier: 'free',
|
|
2061
|
+
async execute(args) {
|
|
2062
|
+
const sampleSize = Number(args.sample_size);
|
|
2063
|
+
const pValue = Number(args.p_value);
|
|
2064
|
+
const effectSize = Number(args.effect_size);
|
|
2065
|
+
const preRegistered = String(args.pre_registered).toLowerCase() === 'true';
|
|
2066
|
+
const numComparisons = typeof args.multiple_comparisons === 'number' ? Math.max(1, args.multiple_comparisons) : 1;
|
|
2067
|
+
if (sampleSize < 2 || isNaN(pValue) || isNaN(effectSize)) {
|
|
2068
|
+
return '**Error**: Provide valid numeric values for sample_size, p_value, and effect_size.';
|
|
2069
|
+
}
|
|
2070
|
+
// Bonferroni-adjusted alpha
|
|
2071
|
+
const alpha = 0.05;
|
|
2072
|
+
const adjustedAlpha = alpha / numComparisons;
|
|
2073
|
+
const adjustedPValue = Math.min(pValue * numComparisons, 1); // Bonferroni-corrected p
|
|
2074
|
+
// Is p-value still significant after correction?
|
|
2075
|
+
const significantAfterCorrection = pValue < adjustedAlpha;
|
|
2076
|
+
// Statistical power estimation (post-hoc)
|
|
2077
|
+
const nPerGroup = sampleSize / 2;
|
|
2078
|
+
const ncp = effectSize * Math.sqrt(nPerGroup / 2); // non-centrality parameter
|
|
2079
|
+
const critZ = normalInvCDF(1 - alpha / 2);
|
|
2080
|
+
const power = 1 - normalCDF(critZ - ncp);
|
|
2081
|
+
// Positive Predictive Value (PPV) — Ioannidis framework
|
|
2082
|
+
// PPV = (1 - beta) * R / ((1 - beta) * R + alpha)
|
|
2083
|
+
// where R = pre-study odds that the hypothesis is true
|
|
2084
|
+
// Estimate R based on context
|
|
2085
|
+
const R = preRegistered ? 0.5 : 0.1; // pre-registered = higher prior
|
|
2086
|
+
const beta = 1 - power;
|
|
2087
|
+
const ppv = ((1 - beta) * R) / ((1 - beta) * R + alpha * numComparisons);
|
|
2088
|
+
// With bias factor (u)
|
|
2089
|
+
const u = preRegistered ? 0.1 : 0.3; // publication bias factor
|
|
2090
|
+
const ppvWithBias = ((1 - beta) * R + u * beta * R) /
|
|
2091
|
+
((1 - beta) * R + alpha + u * beta * R + u * alpha);
|
|
2092
|
+
// R-index: median power minus excess significance
|
|
2093
|
+
// Simplified: power - (observed significance rate - expected significance rate)
|
|
2094
|
+
const observedSigRate = pValue < alpha ? 1 : 0;
|
|
2095
|
+
const rIndex = power - Math.abs(observedSigRate - power);
|
|
2096
|
+
// Replication probability
|
|
2097
|
+
// Based on Killeen (2005) prep statistic and power
|
|
2098
|
+
const prep = power > 0 ? Math.pow(1 - Math.exp(-0.5 * ncp * ncp / sampleSize), 1) : 0;
|
|
2099
|
+
// More practically: replication probability based on power
|
|
2100
|
+
const replicationProb = power * ppv;
|
|
2101
|
+
// p-curve analysis (single study approximation)
|
|
2102
|
+
const pCurveZ = -normalInvCDF(pValue);
|
|
2103
|
+
const isRightSkewed = pValue < 0.025;
|
|
2104
|
+
const risks = [];
|
|
2105
|
+
// Underpowered?
|
|
2106
|
+
if (power < 0.5) {
|
|
2107
|
+
risks.push({
|
|
2108
|
+
flag: 'Severely underpowered',
|
|
2109
|
+
severity: 'critical',
|
|
2110
|
+
detail: `Post-hoc power = ${(power * 100).toFixed(1)}%. Studies below 50% power have inflated effect sizes when significant (winner's curse).`,
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
else if (power < 0.8) {
|
|
2114
|
+
risks.push({
|
|
2115
|
+
flag: 'Underpowered',
|
|
2116
|
+
severity: 'high',
|
|
2117
|
+
detail: `Post-hoc power = ${(power * 100).toFixed(1)}%. Below the conventional 80% threshold.`,
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
// Suspicious p-value?
|
|
2121
|
+
if (pValue > 0.01 && pValue < 0.05) {
|
|
2122
|
+
risks.push({
|
|
2123
|
+
flag: 'P-value in suspicious zone',
|
|
2124
|
+
severity: 'moderate',
|
|
2125
|
+
detail: `p = ${pValue} is between 0.01 and 0.05. A disproportionate number of published p-values cluster just below 0.05, suggesting possible p-hacking.`,
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
// Multiple comparisons?
|
|
2129
|
+
if (numComparisons > 1) {
|
|
2130
|
+
if (!significantAfterCorrection) {
|
|
2131
|
+
risks.push({
|
|
2132
|
+
flag: 'Not significant after multiple comparison correction',
|
|
2133
|
+
severity: 'critical',
|
|
2134
|
+
detail: `Bonferroni-corrected p = ${adjustedPValue.toFixed(4)} > ${alpha}. With ${numComparisons} comparisons, the result does not survive correction.`,
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
else {
|
|
2138
|
+
risks.push({
|
|
2139
|
+
flag: 'Multiple comparisons',
|
|
2140
|
+
severity: 'moderate',
|
|
2141
|
+
detail: `${numComparisons} comparisons performed. Bonferroni-corrected p = ${adjustedPValue.toFixed(4)}. Result survives correction.`,
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
// Not pre-registered?
|
|
2146
|
+
if (!preRegistered) {
|
|
2147
|
+
risks.push({
|
|
2148
|
+
flag: 'Not pre-registered',
|
|
2149
|
+
severity: 'high',
|
|
2150
|
+
detail: 'Without pre-registration, researcher degrees of freedom allow flexible analysis (p-hacking, HARKing). PPV is significantly reduced.',
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
// Small sample?
|
|
2154
|
+
if (sampleSize < 20) {
|
|
2155
|
+
risks.push({
|
|
2156
|
+
flag: 'Very small sample',
|
|
2157
|
+
severity: 'critical',
|
|
2158
|
+
detail: `N = ${sampleSize}. Extremely small samples produce unstable estimates, inflated effects, and low power.`,
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
else if (sampleSize < 50) {
|
|
2162
|
+
risks.push({
|
|
2163
|
+
flag: 'Small sample',
|
|
2164
|
+
severity: 'high',
|
|
2165
|
+
detail: `N = ${sampleSize}. Small samples reduce precision and may not generalize.`,
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
// Large effect with small sample?
|
|
2169
|
+
if (effectSize > 0.8 && sampleSize < 50) {
|
|
2170
|
+
risks.push({
|
|
2171
|
+
flag: 'Large effect with small sample (possible inflation)',
|
|
2172
|
+
severity: 'high',
|
|
2173
|
+
detail: `Cohen's d = ${effectSize} with N = ${sampleSize}. The "winner's curse" means significant effects in small samples are often inflated.`,
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
// Compute reproducibility score (0-100)
|
|
2177
|
+
let score = 100;
|
|
2178
|
+
// Power penalty
|
|
2179
|
+
if (power < 0.8)
|
|
2180
|
+
score -= (0.8 - power) * 40;
|
|
2181
|
+
if (power < 0.5)
|
|
2182
|
+
score -= 15;
|
|
2183
|
+
// Sample size penalty
|
|
2184
|
+
if (sampleSize < 20)
|
|
2185
|
+
score -= 25;
|
|
2186
|
+
else if (sampleSize < 50)
|
|
2187
|
+
score -= 15;
|
|
2188
|
+
else if (sampleSize < 100)
|
|
2189
|
+
score -= 5;
|
|
2190
|
+
// P-value zone penalty
|
|
2191
|
+
if (pValue > 0.01 && pValue < 0.05)
|
|
2192
|
+
score -= 10;
|
|
2193
|
+
// Multiple comparisons penalty
|
|
2194
|
+
if (!significantAfterCorrection && numComparisons > 1)
|
|
2195
|
+
score -= 20;
|
|
2196
|
+
else if (numComparisons > 5)
|
|
2197
|
+
score -= 10;
|
|
2198
|
+
else if (numComparisons > 1)
|
|
2199
|
+
score -= 5;
|
|
2200
|
+
// Pre-registration bonus/penalty
|
|
2201
|
+
if (preRegistered)
|
|
2202
|
+
score += 10;
|
|
2203
|
+
else
|
|
2204
|
+
score -= 15;
|
|
2205
|
+
// Effect inflation check
|
|
2206
|
+
if (effectSize > 0.8 && sampleSize < 50)
|
|
2207
|
+
score -= 10;
|
|
2208
|
+
score = Math.max(0, Math.min(100, Math.round(score)));
|
|
2209
|
+
const rating = score >= 80 ? 'High' : score >= 60 ? 'Moderate' : score >= 40 ? 'Low' : 'Very Low';
|
|
2210
|
+
const color = score >= 80 ? 'likely replicable' : score >= 60 ? 'may replicate with adequate power' : score >= 40 ? 'replication uncertain' : 'replication unlikely';
|
|
2211
|
+
// Build output
|
|
2212
|
+
const lines = [
|
|
2213
|
+
`# Reproducibility Check`,
|
|
2214
|
+
'',
|
|
2215
|
+
`## Study Parameters`,
|
|
2216
|
+
'',
|
|
2217
|
+
`| Parameter | Value |`,
|
|
2218
|
+
`|-----------|-------|`,
|
|
2219
|
+
`| Sample Size | ${sampleSize} |`,
|
|
2220
|
+
`| P-value | ${pValue} |`,
|
|
2221
|
+
`| Effect Size (d) | ${effectSize} |`,
|
|
2222
|
+
`| Pre-registered | ${preRegistered ? 'Yes' : 'No'} |`,
|
|
2223
|
+
`| Comparisons | ${numComparisons} |`,
|
|
2224
|
+
'',
|
|
2225
|
+
`---`,
|
|
2226
|
+
'',
|
|
2227
|
+
`## Reproducibility Score: ${score}/100 (${rating})`,
|
|
2228
|
+
'',
|
|
2229
|
+
`*${color}*`,
|
|
2230
|
+
'',
|
|
2231
|
+
`## Detailed Metrics`,
|
|
2232
|
+
'',
|
|
2233
|
+
`| Metric | Value | Interpretation |`,
|
|
2234
|
+
`|--------|-------|----------------|`,
|
|
2235
|
+
`| **Post-hoc Power** | ${(power * 100).toFixed(1)}% | ${power >= 0.8 ? 'Adequate' : power >= 0.5 ? 'Below threshold' : 'Severely underpowered'} |`,
|
|
2236
|
+
`| **PPV (no bias)** | ${(ppv * 100).toFixed(1)}% | Probability result is a true positive |`,
|
|
2237
|
+
`| **PPV (with bias)** | ${(ppvWithBias * 100).toFixed(1)}% | Accounting for publication bias |`,
|
|
2238
|
+
`| **R-index** | ${rIndex.toFixed(3)} | ${rIndex > 0.5 ? 'Favorable' : 'Unfavorable'} |`,
|
|
2239
|
+
`| **Replication Prob.** | ${(replicationProb * 100).toFixed(1)}% | Estimated chance of replication |`,
|
|
2240
|
+
`| **Bonferroni p** | ${adjustedPValue.toFixed(4)} | ${significantAfterCorrection ? 'Significant' : 'NOT significant'} after correction |`,
|
|
2241
|
+
`| **P-curve z** | ${pCurveZ.toFixed(3)} | ${isRightSkewed ? 'Right-skewed (evidential value)' : 'Not right-skewed'} |`,
|
|
2242
|
+
'',
|
|
2243
|
+
];
|
|
2244
|
+
if (risks.length > 0) {
|
|
2245
|
+
lines.push(`## Identified Risks`);
|
|
2246
|
+
lines.push('');
|
|
2247
|
+
lines.push(`| Risk | Severity | Details |`);
|
|
2248
|
+
lines.push(`|------|----------|---------|`);
|
|
2249
|
+
for (const r of risks) {
|
|
2250
|
+
lines.push(`| ${r.flag} | **${r.severity}** | ${r.detail} |`);
|
|
2251
|
+
}
|
|
2252
|
+
lines.push('');
|
|
2253
|
+
}
|
|
2254
|
+
lines.push(`## Recommendations`);
|
|
2255
|
+
lines.push('');
|
|
2256
|
+
if (power < 0.8) {
|
|
2257
|
+
const requiredN = Math.ceil(2 * (critZ + normalInvCDF(0.8)) ** 2 / (effectSize ** 2 || 0.01));
|
|
2258
|
+
lines.push(`1. **Increase sample size**: Need approximately N = ${requiredN} total for 80% power at d = ${effectSize}.`);
|
|
2259
|
+
}
|
|
2260
|
+
if (!preRegistered) {
|
|
2261
|
+
lines.push(`${power < 0.8 ? '2' : '1'}. **Pre-register** future replications on OSF or AsPredicted.`);
|
|
2262
|
+
}
|
|
2263
|
+
if (numComparisons > 1) {
|
|
2264
|
+
lines.push(`- **Apply correction**: Use Bonferroni, Holm, or FDR correction for ${numComparisons} comparisons.`);
|
|
2265
|
+
}
|
|
2266
|
+
if (pValue > 0.01 && pValue < 0.05) {
|
|
2267
|
+
lines.push(`- **Report exact p-values** and effect sizes with confidence intervals, not just "p < 0.05".`);
|
|
2268
|
+
}
|
|
2269
|
+
lines.push(`- **Share data and analysis code** for transparency and independent verification.`);
|
|
2270
|
+
lines.push(`- Use \`experiment_simulate\` to plan adequately powered replications.`);
|
|
2271
|
+
lines.push(`- Use \`meta_analysis\` to synthesize across multiple replication attempts.`);
|
|
2272
|
+
return lines.join('\n');
|
|
2273
|
+
},
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
//# sourceMappingURL=hypothesis-engine.js.map
|