@kernel.chat/kbot 3.41.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.
Files changed (88) hide show
  1. package/README.md +5 -5
  2. package/dist/agent-teams.d.ts +1 -1
  3. package/dist/agent-teams.d.ts.map +1 -1
  4. package/dist/agent-teams.js +36 -3
  5. package/dist/agent-teams.js.map +1 -1
  6. package/dist/agents/specialists.d.ts.map +1 -1
  7. package/dist/agents/specialists.js +20 -0
  8. package/dist/agents/specialists.js.map +1 -1
  9. package/dist/auth.d.ts +5 -1
  10. package/dist/auth.d.ts.map +1 -1
  11. package/dist/auth.js +1 -1
  12. package/dist/auth.js.map +1 -1
  13. package/dist/channels/kbot-channel.js +8 -31
  14. package/dist/channels/kbot-channel.js.map +1 -1
  15. package/dist/cli.js +44 -11
  16. package/dist/cli.js.map +1 -1
  17. package/dist/completions.d.ts.map +1 -1
  18. package/dist/completions.js +7 -0
  19. package/dist/completions.js.map +1 -1
  20. package/dist/digest.js +1 -1
  21. package/dist/digest.js.map +1 -1
  22. package/dist/doctor.d.ts.map +1 -1
  23. package/dist/doctor.js +132 -92
  24. package/dist/doctor.js.map +1 -1
  25. package/dist/doctor.test.d.ts +2 -0
  26. package/dist/doctor.test.d.ts.map +1 -0
  27. package/dist/doctor.test.js +432 -0
  28. package/dist/doctor.test.js.map +1 -0
  29. package/dist/email-service.d.ts.map +1 -1
  30. package/dist/email-service.js +1 -2
  31. package/dist/email-service.js.map +1 -1
  32. package/dist/episodic-memory.d.ts.map +1 -1
  33. package/dist/episodic-memory.js +14 -0
  34. package/dist/episodic-memory.js.map +1 -1
  35. package/dist/learned-router.d.ts.map +1 -1
  36. package/dist/learned-router.js +29 -0
  37. package/dist/learned-router.js.map +1 -1
  38. package/dist/tools/email.d.ts.map +1 -1
  39. package/dist/tools/email.js +2 -3
  40. package/dist/tools/email.js.map +1 -1
  41. package/dist/tools/hypothesis-engine.d.ts +2 -0
  42. package/dist/tools/hypothesis-engine.d.ts.map +1 -0
  43. package/dist/tools/hypothesis-engine.js +2276 -0
  44. package/dist/tools/hypothesis-engine.js.map +1 -0
  45. package/dist/tools/index.d.ts.map +1 -1
  46. package/dist/tools/index.js +11 -1
  47. package/dist/tools/index.js.map +1 -1
  48. package/dist/tools/lab-bio.d.ts +2 -0
  49. package/dist/tools/lab-bio.d.ts.map +1 -0
  50. package/dist/tools/lab-bio.js +1392 -0
  51. package/dist/tools/lab-bio.js.map +1 -0
  52. package/dist/tools/lab-chem.d.ts +2 -0
  53. package/dist/tools/lab-chem.d.ts.map +1 -0
  54. package/dist/tools/lab-chem.js +1257 -0
  55. package/dist/tools/lab-chem.js.map +1 -0
  56. package/dist/tools/lab-core.d.ts +2 -0
  57. package/dist/tools/lab-core.d.ts.map +1 -0
  58. package/dist/tools/lab-core.js +2452 -0
  59. package/dist/tools/lab-core.js.map +1 -0
  60. package/dist/tools/lab-data.d.ts +2 -0
  61. package/dist/tools/lab-data.d.ts.map +1 -0
  62. package/dist/tools/lab-data.js +2464 -0
  63. package/dist/tools/lab-data.js.map +1 -0
  64. package/dist/tools/lab-earth.d.ts +2 -0
  65. package/dist/tools/lab-earth.d.ts.map +1 -0
  66. package/dist/tools/lab-earth.js +1124 -0
  67. package/dist/tools/lab-earth.js.map +1 -0
  68. package/dist/tools/lab-math.d.ts +2 -0
  69. package/dist/tools/lab-math.d.ts.map +1 -0
  70. package/dist/tools/lab-math.js +3021 -0
  71. package/dist/tools/lab-math.js.map +1 -0
  72. package/dist/tools/lab-physics.d.ts +2 -0
  73. package/dist/tools/lab-physics.d.ts.map +1 -0
  74. package/dist/tools/lab-physics.js +2423 -0
  75. package/dist/tools/lab-physics.js.map +1 -0
  76. package/dist/tools/research-notebook.d.ts +2 -0
  77. package/dist/tools/research-notebook.d.ts.map +1 -0
  78. package/dist/tools/research-notebook.js +1165 -0
  79. package/dist/tools/research-notebook.js.map +1 -0
  80. package/dist/tools/research-pipeline.d.ts +2 -0
  81. package/dist/tools/research-pipeline.d.ts.map +1 -0
  82. package/dist/tools/research-pipeline.js +1094 -0
  83. package/dist/tools/research-pipeline.js.map +1 -0
  84. package/dist/tools/science-graph.d.ts +2 -0
  85. package/dist/tools/science-graph.d.ts.map +1 -0
  86. package/dist/tools/science-graph.js +995 -0
  87. package/dist/tools/science-graph.js.map +1 -0
  88. package/package.json +2 -3
@@ -0,0 +1,2452 @@
1
+ // kbot Lab Core Tools — Universal Research Tools
2
+ // Provides statistical testing, literature search, citation mapping,
3
+ // unit conversion, physical constants, formula solving, methodology
4
+ // generation, preprint tracking, and open access discovery.
5
+ import { registerTool } from './index.js';
6
+ // ─── Math Helpers ────────────────────────────────────────────────────────────
7
+ /** Gamma function via Lanczos approximation */
8
+ function gammaLn(z) {
9
+ const g = 7;
10
+ const c = [
11
+ 0.99999999999980993, 676.5203681218851, -1259.1392167224028,
12
+ 771.32342877765313, -176.61502916214059, 12.507343278686905,
13
+ -0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7,
14
+ ];
15
+ if (z < 0.5) {
16
+ return Math.log(Math.PI / Math.sin(Math.PI * z)) - gammaLn(1 - z);
17
+ }
18
+ z -= 1;
19
+ let x = c[0];
20
+ for (let i = 1; i < g + 2; i++)
21
+ x += c[i] / (z + i);
22
+ const t = z + g + 0.5;
23
+ return 0.5 * Math.log(2 * Math.PI) + (z + 0.5) * Math.log(t) - t + Math.log(x);
24
+ }
25
+ function gamma(z) {
26
+ return Math.exp(gammaLn(z));
27
+ }
28
+ /** Regularized incomplete beta function I_x(a, b) via continued fraction */
29
+ function betaIncomplete(x, a, b) {
30
+ if (x <= 0)
31
+ return 0;
32
+ if (x >= 1)
33
+ return 1;
34
+ if (a <= 0 || b <= 0)
35
+ return 0;
36
+ // Use symmetry if needed for convergence
37
+ if (x > (a + 1) / (a + b + 2)) {
38
+ return 1 - betaIncomplete(1 - x, b, a);
39
+ }
40
+ const lnBeta = gammaLn(a) + gammaLn(b) - gammaLn(a + b);
41
+ const front = Math.exp(Math.log(x) * a + Math.log(1 - x) * b - lnBeta) / a;
42
+ // Lentz's continued fraction
43
+ const maxIter = 200;
44
+ const eps = 1e-14;
45
+ let f = 1, c = 1, d = 0;
46
+ for (let m = 0; m <= maxIter; m++) {
47
+ let numerator;
48
+ if (m === 0) {
49
+ numerator = 1;
50
+ }
51
+ else if (m % 2 === 0) {
52
+ const k = m / 2;
53
+ numerator = (k * (b - k) * x) / ((a + 2 * k - 1) * (a + 2 * k));
54
+ }
55
+ else {
56
+ const k = (m - 1) / 2;
57
+ numerator = -((a + k) * (a + b + k) * x) / ((a + 2 * k) * (a + 2 * k + 1));
58
+ }
59
+ d = 1 + numerator * d;
60
+ if (Math.abs(d) < 1e-30)
61
+ d = 1e-30;
62
+ d = 1 / d;
63
+ c = 1 + numerator / c;
64
+ if (Math.abs(c) < 1e-30)
65
+ c = 1e-30;
66
+ const delta = c * d;
67
+ f *= delta;
68
+ if (Math.abs(delta - 1) < eps)
69
+ break;
70
+ }
71
+ return front * (f - 1);
72
+ }
73
+ /** Regularized lower incomplete gamma function P(a, x) */
74
+ function gammaPLower(a, x) {
75
+ if (x <= 0)
76
+ return 0;
77
+ if (x < a + 1) {
78
+ // Series representation
79
+ let sum = 1 / a;
80
+ let term = 1 / a;
81
+ for (let n = 1; n < 200; n++) {
82
+ term *= x / (a + n);
83
+ sum += term;
84
+ if (Math.abs(term) < Math.abs(sum) * 1e-14)
85
+ break;
86
+ }
87
+ return sum * Math.exp(-x + a * Math.log(x) - gammaLn(a));
88
+ }
89
+ else {
90
+ // Continued fraction
91
+ return 1 - gammaQUpper(a, x);
92
+ }
93
+ }
94
+ /** Regularized upper incomplete gamma function Q(a, x) */
95
+ function gammaQUpper(a, x) {
96
+ if (x <= 0)
97
+ return 1;
98
+ if (x < a + 1)
99
+ return 1 - gammaPLower(a, x);
100
+ // Continued fraction (Lentz)
101
+ let f = 1, c = 1, d = 0;
102
+ for (let i = 1; i < 200; i++) {
103
+ const an = (i % 2 === 1) ? ((i + 1) / 2 - a) : (i / 2);
104
+ const bn = (i === 1) ? (x + 1 - a) : x + (2 * i - 1) + 1 - a;
105
+ // Actually use the standard CF for upper incomplete gamma
106
+ // Let's use a simpler series approach for moderate x
107
+ break; // fall through to series
108
+ }
109
+ // Use series for Q when x >= a+1
110
+ let sum = 0, term = 1 / x;
111
+ let prev = term;
112
+ for (let k = 1; k < 300; k++) {
113
+ term *= (a - k) / x;
114
+ if (Math.abs(term) > Math.abs(prev))
115
+ break; // diverging
116
+ sum += term;
117
+ if (Math.abs(term) < 1e-14)
118
+ break;
119
+ prev = term;
120
+ }
121
+ return Math.exp(-x + a * Math.log(x) - gammaLn(a)) * (1 / x + sum);
122
+ }
123
+ /** Student's t-distribution CDF */
124
+ function tCdf(t, df) {
125
+ const x = df / (df + t * t);
126
+ const p = 0.5 * betaIncomplete(x, df / 2, 0.5);
127
+ return t >= 0 ? 1 - p : p;
128
+ }
129
+ /** Two-tailed p-value from t-statistic and df */
130
+ function tTestPValue(t, df) {
131
+ return 2 * (1 - tCdf(Math.abs(t), df));
132
+ }
133
+ /** Chi-square CDF P(X <= x) with k degrees of freedom */
134
+ function chiSquareCdf(x, k) {
135
+ if (x <= 0)
136
+ return 0;
137
+ return gammaPLower(k / 2, x / 2);
138
+ }
139
+ /** Normal CDF approximation (Abramowitz & Stegun) */
140
+ function normalCdf(z) {
141
+ if (z < -8)
142
+ return 0;
143
+ if (z > 8)
144
+ return 1;
145
+ const a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741;
146
+ const a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911;
147
+ const sign = z < 0 ? -1 : 1;
148
+ const x = Math.abs(z) / Math.SQRT2;
149
+ const t = 1 / (1 + p * x);
150
+ const erf = 1 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
151
+ return 0.5 * (1 + sign * erf);
152
+ }
153
+ function mean(arr) {
154
+ return arr.reduce((s, v) => s + v, 0) / arr.length;
155
+ }
156
+ function variance(arr, ddof = 1) {
157
+ const m = mean(arr);
158
+ return arr.reduce((s, v) => s + (v - m) ** 2, 0) / (arr.length - ddof);
159
+ }
160
+ function stddev(arr, ddof = 1) {
161
+ return Math.sqrt(variance(arr, ddof));
162
+ }
163
+ function parseNumbers(s) {
164
+ return s.split(',').map(x => x.trim()).filter(x => x !== '').map(Number).filter(x => !isNaN(x));
165
+ }
166
+ const UNITS = {
167
+ // Length (base: meter)
168
+ m: { factor: 1, dimension: 'length' },
169
+ km: { factor: 1000, dimension: 'length' },
170
+ cm: { factor: 0.01, dimension: 'length' },
171
+ mm: { factor: 0.001, dimension: 'length' },
172
+ um: { factor: 1e-6, dimension: 'length' },
173
+ nm: { factor: 1e-9, dimension: 'length' },
174
+ pm: { factor: 1e-12, dimension: 'length' },
175
+ fm: { factor: 1e-15, dimension: 'length' },
176
+ angstrom: { factor: 1e-10, dimension: 'length' },
177
+ in: { factor: 0.0254, dimension: 'length' },
178
+ ft: { factor: 0.3048, dimension: 'length' },
179
+ yd: { factor: 0.9144, dimension: 'length' },
180
+ mi: { factor: 1609.344, dimension: 'length' },
181
+ nmi: { factor: 1852, dimension: 'length' },
182
+ au: { factor: 1.495978707e11, dimension: 'length' },
183
+ ly: { factor: 9.4607e15, dimension: 'length' },
184
+ pc: { factor: 3.0857e16, dimension: 'length' },
185
+ mil: { factor: 2.54e-5, dimension: 'length' },
186
+ fathom: { factor: 1.8288, dimension: 'length' },
187
+ // Mass (base: kilogram)
188
+ kg: { factor: 1, dimension: 'mass' },
189
+ g: { factor: 0.001, dimension: 'mass' },
190
+ mg: { factor: 1e-6, dimension: 'mass' },
191
+ ug: { factor: 1e-9, dimension: 'mass' },
192
+ ng: { factor: 1e-12, dimension: 'mass' },
193
+ tonne: { factor: 1000, dimension: 'mass' },
194
+ lb: { factor: 0.45359237, dimension: 'mass' },
195
+ oz: { factor: 0.028349523125, dimension: 'mass' },
196
+ stone: { factor: 6.35029318, dimension: 'mass' },
197
+ slug: { factor: 14.593903, dimension: 'mass' },
198
+ grain: { factor: 6.479891e-5, dimension: 'mass' },
199
+ carat: { factor: 0.0002, dimension: 'mass' },
200
+ amu: { factor: 1.66053906660e-27, dimension: 'mass' },
201
+ dalton: { factor: 1.66053906660e-27, dimension: 'mass' },
202
+ ton_short: { factor: 907.18474, dimension: 'mass' },
203
+ ton_long: { factor: 1016.0469088, dimension: 'mass' },
204
+ // Time (base: second)
205
+ s: { factor: 1, dimension: 'time' },
206
+ ms: { factor: 0.001, dimension: 'time' },
207
+ us: { factor: 1e-6, dimension: 'time' },
208
+ ns: { factor: 1e-9, dimension: 'time' },
209
+ ps: { factor: 1e-12, dimension: 'time' },
210
+ min: { factor: 60, dimension: 'time' },
211
+ hr: { factor: 3600, dimension: 'time' },
212
+ day: { factor: 86400, dimension: 'time' },
213
+ week: { factor: 604800, dimension: 'time' },
214
+ year: { factor: 31557600, dimension: 'time' },
215
+ // Energy (base: joule)
216
+ J: { factor: 1, dimension: 'energy' },
217
+ kJ: { factor: 1000, dimension: 'energy' },
218
+ MJ: { factor: 1e6, dimension: 'energy' },
219
+ GJ: { factor: 1e9, dimension: 'energy' },
220
+ cal: { factor: 4.184, dimension: 'energy' },
221
+ kcal: { factor: 4184, dimension: 'energy' },
222
+ Wh: { factor: 3600, dimension: 'energy' },
223
+ kWh: { factor: 3.6e6, dimension: 'energy' },
224
+ eV: { factor: 1.602176634e-19, dimension: 'energy' },
225
+ keV: { factor: 1.602176634e-16, dimension: 'energy' },
226
+ MeV: { factor: 1.602176634e-13, dimension: 'energy' },
227
+ GeV: { factor: 1.602176634e-10, dimension: 'energy' },
228
+ BTU: { factor: 1055.06, dimension: 'energy' },
229
+ erg: { factor: 1e-7, dimension: 'energy' },
230
+ Ry: { factor: 2.1798723611035e-18, dimension: 'energy' },
231
+ hartree: { factor: 4.3597447222071e-18, dimension: 'energy' },
232
+ // Pressure (base: pascal)
233
+ Pa: { factor: 1, dimension: 'pressure' },
234
+ kPa: { factor: 1000, dimension: 'pressure' },
235
+ MPa: { factor: 1e6, dimension: 'pressure' },
236
+ GPa: { factor: 1e9, dimension: 'pressure' },
237
+ bar: { factor: 100000, dimension: 'pressure' },
238
+ mbar: { factor: 100, dimension: 'pressure' },
239
+ atm: { factor: 101325, dimension: 'pressure' },
240
+ torr: { factor: 133.322, dimension: 'pressure' },
241
+ mmHg: { factor: 133.322, dimension: 'pressure' },
242
+ psi: { factor: 6894.757, dimension: 'pressure' },
243
+ inHg: { factor: 3386.389, dimension: 'pressure' },
244
+ // Force (base: newton)
245
+ N: { factor: 1, dimension: 'force' },
246
+ kN: { factor: 1000, dimension: 'force' },
247
+ MN: { factor: 1e6, dimension: 'force' },
248
+ dyn: { factor: 1e-5, dimension: 'force' },
249
+ lbf: { factor: 4.44822, dimension: 'force' },
250
+ kgf: { factor: 9.80665, dimension: 'force' },
251
+ // Power (base: watt)
252
+ W: { factor: 1, dimension: 'power' },
253
+ kW: { factor: 1000, dimension: 'power' },
254
+ MW: { factor: 1e6, dimension: 'power' },
255
+ GW: { factor: 1e9, dimension: 'power' },
256
+ hp: { factor: 745.7, dimension: 'power' },
257
+ hp_metric: { factor: 735.499, dimension: 'power' },
258
+ // Frequency (base: hertz)
259
+ Hz: { factor: 1, dimension: 'frequency' },
260
+ kHz: { factor: 1000, dimension: 'frequency' },
261
+ MHz: { factor: 1e6, dimension: 'frequency' },
262
+ GHz: { factor: 1e9, dimension: 'frequency' },
263
+ THz: { factor: 1e12, dimension: 'frequency' },
264
+ rpm: { factor: 1 / 60, dimension: 'frequency' },
265
+ // Electric potential (base: volt)
266
+ V: { factor: 1, dimension: 'voltage' },
267
+ mV: { factor: 0.001, dimension: 'voltage' },
268
+ kV: { factor: 1000, dimension: 'voltage' },
269
+ // Electric current (base: ampere)
270
+ A: { factor: 1, dimension: 'current' },
271
+ mA: { factor: 0.001, dimension: 'current' },
272
+ uA: { factor: 1e-6, dimension: 'current' },
273
+ // Electric resistance (base: ohm)
274
+ ohm: { factor: 1, dimension: 'resistance' },
275
+ kohm: { factor: 1000, dimension: 'resistance' },
276
+ Mohm: { factor: 1e6, dimension: 'resistance' },
277
+ // Electric charge (base: coulomb)
278
+ C: { factor: 1, dimension: 'charge' },
279
+ mC: { factor: 0.001, dimension: 'charge' },
280
+ uC: { factor: 1e-6, dimension: 'charge' },
281
+ Ah: { factor: 3600, dimension: 'charge' },
282
+ mAh: { factor: 3.6, dimension: 'charge' },
283
+ e_charge: { factor: 1.602176634e-19, dimension: 'charge' },
284
+ // Capacitance (base: farad)
285
+ F: { factor: 1, dimension: 'capacitance' },
286
+ mF: { factor: 0.001, dimension: 'capacitance' },
287
+ uF: { factor: 1e-6, dimension: 'capacitance' },
288
+ nF: { factor: 1e-9, dimension: 'capacitance' },
289
+ pF: { factor: 1e-12, dimension: 'capacitance' },
290
+ // Magnetic field (base: tesla)
291
+ T: { factor: 1, dimension: 'magnetic_field' },
292
+ mT: { factor: 0.001, dimension: 'magnetic_field' },
293
+ uT: { factor: 1e-6, dimension: 'magnetic_field' },
294
+ gauss: { factor: 1e-4, dimension: 'magnetic_field' },
295
+ // Radiation dose (base: gray)
296
+ Gy: { factor: 1, dimension: 'radiation_dose' },
297
+ mGy: { factor: 0.001, dimension: 'radiation_dose' },
298
+ rad_dose: { factor: 0.01, dimension: 'radiation_dose' },
299
+ Sv: { factor: 1, dimension: 'dose_equivalent' },
300
+ mSv: { factor: 0.001, dimension: 'dose_equivalent' },
301
+ uSv: { factor: 1e-6, dimension: 'dose_equivalent' },
302
+ rem: { factor: 0.01, dimension: 'dose_equivalent' },
303
+ // Radioactivity (base: becquerel)
304
+ Bq: { factor: 1, dimension: 'radioactivity' },
305
+ kBq: { factor: 1000, dimension: 'radioactivity' },
306
+ MBq: { factor: 1e6, dimension: 'radioactivity' },
307
+ Ci: { factor: 3.7e10, dimension: 'radioactivity' },
308
+ mCi: { factor: 3.7e7, dimension: 'radioactivity' },
309
+ // Volume (base: cubic meter)
310
+ m3: { factor: 1, dimension: 'volume' },
311
+ L: { factor: 0.001, dimension: 'volume' },
312
+ mL: { factor: 1e-6, dimension: 'volume' },
313
+ uL: { factor: 1e-9, dimension: 'volume' },
314
+ cm3: { factor: 1e-6, dimension: 'volume' },
315
+ mm3: { factor: 1e-9, dimension: 'volume' },
316
+ gal: { factor: 0.003785411784, dimension: 'volume' },
317
+ qt: { factor: 0.000946352946, dimension: 'volume' },
318
+ pt: { factor: 0.000473176473, dimension: 'volume' },
319
+ cup: { factor: 0.000236588236, dimension: 'volume' },
320
+ fl_oz: { factor: 2.957352956e-5, dimension: 'volume' },
321
+ bbl: { factor: 0.158987295, dimension: 'volume' },
322
+ // Area (base: square meter)
323
+ m2: { factor: 1, dimension: 'area' },
324
+ cm2: { factor: 1e-4, dimension: 'area' },
325
+ mm2: { factor: 1e-6, dimension: 'area' },
326
+ km2: { factor: 1e6, dimension: 'area' },
327
+ ha: { factor: 10000, dimension: 'area' },
328
+ acre: { factor: 4046.8564224, dimension: 'area' },
329
+ ft2: { factor: 0.09290304, dimension: 'area' },
330
+ in2: { factor: 6.4516e-4, dimension: 'area' },
331
+ barn: { factor: 1e-28, dimension: 'area' },
332
+ // Speed (base: m/s)
333
+ 'm/s': { factor: 1, dimension: 'speed' },
334
+ 'km/h': { factor: 1 / 3.6, dimension: 'speed' },
335
+ 'mi/h': { factor: 0.44704, dimension: 'speed' },
336
+ mph: { factor: 0.44704, dimension: 'speed' },
337
+ kn: { factor: 0.514444, dimension: 'speed' },
338
+ 'ft/s': { factor: 0.3048, dimension: 'speed' },
339
+ mach: { factor: 343, dimension: 'speed' },
340
+ c_speed: { factor: 299792458, dimension: 'speed' },
341
+ // Density (base: kg/m3)
342
+ 'kg/m3': { factor: 1, dimension: 'density' },
343
+ 'g/cm3': { factor: 1000, dimension: 'density' },
344
+ 'g/mL': { factor: 1000, dimension: 'density' },
345
+ 'kg/L': { factor: 1000, dimension: 'density' },
346
+ 'lb/ft3': { factor: 16.01846, dimension: 'density' },
347
+ // Concentration (base: mol/L = M)
348
+ M: { factor: 1, dimension: 'concentration' },
349
+ mM: { factor: 0.001, dimension: 'concentration' },
350
+ uM: { factor: 1e-6, dimension: 'concentration' },
351
+ nM: { factor: 1e-9, dimension: 'concentration' },
352
+ pM: { factor: 1e-12, dimension: 'concentration' },
353
+ // Data (base: byte)
354
+ B: { factor: 1, dimension: 'data' },
355
+ KB: { factor: 1000, dimension: 'data' },
356
+ MB: { factor: 1e6, dimension: 'data' },
357
+ GB: { factor: 1e9, dimension: 'data' },
358
+ TB: { factor: 1e12, dimension: 'data' },
359
+ PB: { factor: 1e15, dimension: 'data' },
360
+ KiB: { factor: 1024, dimension: 'data' },
361
+ MiB: { factor: 1048576, dimension: 'data' },
362
+ GiB: { factor: 1073741824, dimension: 'data' },
363
+ TiB: { factor: 1099511627776, dimension: 'data' },
364
+ bit: { factor: 0.125, dimension: 'data' },
365
+ Kbit: { factor: 125, dimension: 'data' },
366
+ Mbit: { factor: 125000, dimension: 'data' },
367
+ Gbit: { factor: 1.25e8, dimension: 'data' },
368
+ // Angle (base: radian)
369
+ rad: { factor: 1, dimension: 'angle' },
370
+ deg: { factor: Math.PI / 180, dimension: 'angle' },
371
+ arcmin: { factor: Math.PI / 10800, dimension: 'angle' },
372
+ arcsec: { factor: Math.PI / 648000, dimension: 'angle' },
373
+ grad: { factor: Math.PI / 200, dimension: 'angle' },
374
+ rev: { factor: 2 * Math.PI, dimension: 'angle' },
375
+ // Luminous intensity / flux (base: candela / lumen)
376
+ lm: { factor: 1, dimension: 'luminous_flux' },
377
+ lx: { factor: 1, dimension: 'illuminance' },
378
+ fc: { factor: 10.7639, dimension: 'illuminance' },
379
+ // Viscosity (base: Pa*s)
380
+ 'Pa*s': { factor: 1, dimension: 'dynamic_viscosity' },
381
+ poise: { factor: 0.1, dimension: 'dynamic_viscosity' },
382
+ cP: { factor: 0.001, dimension: 'dynamic_viscosity' },
383
+ };
384
+ // Temperature conversions (special handling)
385
+ function convertTemperature(value, from, to) {
386
+ const tempUnits = ['C', 'F', 'K', 'R'];
387
+ const fromNorm = from.replace(/^deg_?/i, '').replace(/celsius/i, 'C').replace(/fahrenheit/i, 'F')
388
+ .replace(/kelvin/i, 'K').replace(/rankine/i, 'R');
389
+ const toNorm = to.replace(/^deg_?/i, '').replace(/celsius/i, 'C').replace(/fahrenheit/i, 'F')
390
+ .replace(/kelvin/i, 'K').replace(/rankine/i, 'R');
391
+ if (!tempUnits.includes(fromNorm) || !tempUnits.includes(toNorm))
392
+ return null;
393
+ // Convert to Kelvin first
394
+ let kelvin;
395
+ switch (fromNorm) {
396
+ case 'C':
397
+ kelvin = value + 273.15;
398
+ break;
399
+ case 'F':
400
+ kelvin = (value + 459.67) * 5 / 9;
401
+ break;
402
+ case 'K':
403
+ kelvin = value;
404
+ break;
405
+ case 'R':
406
+ kelvin = value * 5 / 9;
407
+ break;
408
+ default: return null;
409
+ }
410
+ // Convert from Kelvin to target
411
+ switch (toNorm) {
412
+ case 'C': return kelvin - 273.15;
413
+ case 'F': return kelvin * 9 / 5 - 459.67;
414
+ case 'K': return kelvin;
415
+ case 'R': return kelvin * 9 / 5;
416
+ default: return null;
417
+ }
418
+ }
419
+ const CONSTANTS = [
420
+ { name: 'Speed of light in vacuum', symbol: 'c', value: 299792458, uncertainty: 0, unit: 'm/s', aliases: ['speed of light', 'c', 'light speed', 'velocity of light'] },
421
+ { name: 'Planck constant', symbol: 'h', value: 6.62607015e-34, uncertainty: 0, unit: 'J*s', aliases: ['planck', 'planck constant', 'h'] },
422
+ { name: 'Reduced Planck constant', symbol: '\u0127', value: 1.054571817e-34, uncertainty: 0, unit: 'J*s', aliases: ['hbar', 'reduced planck', 'dirac constant', 'h-bar'] },
423
+ { name: 'Gravitational constant', symbol: 'G', value: 6.67430e-11, uncertainty: 1.5e-15, unit: 'm^3/(kg*s^2)', aliases: ['gravitational constant', 'newton gravitational', 'big g', 'G'] },
424
+ { name: 'Boltzmann constant', symbol: 'k_B', value: 1.380649e-23, uncertainty: 0, unit: 'J/K', aliases: ['boltzmann', 'boltzmann constant', 'kb', 'k_b'] },
425
+ { name: 'Avogadro constant', symbol: 'N_A', value: 6.02214076e23, uncertainty: 0, unit: '1/mol', aliases: ['avogadro', 'avogadro constant', 'avogadro number', 'na', 'n_a'] },
426
+ { name: 'Elementary charge', symbol: 'e', value: 1.602176634e-19, uncertainty: 0, unit: 'C', aliases: ['elementary charge', 'electron charge', 'e charge'] },
427
+ { name: 'Electron mass', symbol: 'm_e', value: 9.1093837015e-31, uncertainty: 2.8e-40, unit: 'kg', aliases: ['electron mass', 'me', 'm_e', 'mass of electron'] },
428
+ { name: 'Proton mass', symbol: 'm_p', value: 1.67262192369e-27, uncertainty: 5.1e-37, unit: 'kg', aliases: ['proton mass', 'mp', 'm_p', 'mass of proton'] },
429
+ { name: 'Neutron mass', symbol: 'm_n', value: 1.67492749804e-27, uncertainty: 9.5e-37, unit: 'kg', aliases: ['neutron mass', 'mn', 'm_n', 'mass of neutron'] },
430
+ { name: 'Fine-structure constant', symbol: '\u03B1', value: 7.2973525693e-3, uncertainty: 1.1e-12, unit: '(dimensionless)', aliases: ['fine structure', 'fine-structure constant', 'alpha', 'fine structure constant'] },
431
+ { name: 'Rydberg constant', symbol: 'R_\u221E', value: 10973731.568160, uncertainty: 2.1e-5, unit: '1/m', aliases: ['rydberg', 'rydberg constant', 'r_inf', 'r_infinity'] },
432
+ { name: 'Bohr radius', symbol: 'a_0', value: 5.29177210903e-11, uncertainty: 8.0e-21, unit: 'm', aliases: ['bohr radius', 'a0', 'a_0', 'bohr'] },
433
+ { name: 'Classical electron radius', symbol: 'r_e', value: 2.8179403262e-15, uncertainty: 1.3e-24, unit: 'm', aliases: ['electron radius', 'classical electron radius', 're'] },
434
+ { name: 'Compton wavelength', symbol: '\u03BB_C', value: 2.42631023867e-12, uncertainty: 7.3e-22, unit: 'm', aliases: ['compton wavelength', 'compton'] },
435
+ { name: 'Magnetic flux quantum', symbol: '\u03A6_0', value: 2.067833848e-15, uncertainty: 0, unit: 'Wb', aliases: ['magnetic flux quantum', 'flux quantum', 'phi_0'] },
436
+ { name: 'Conductance quantum', symbol: 'G_0', value: 7.748091729e-5, uncertainty: 0, unit: 'S', aliases: ['conductance quantum', 'g0'] },
437
+ { name: 'Josephson constant', symbol: 'K_J', value: 483597.8484e9, uncertainty: 0, unit: 'Hz/V', aliases: ['josephson constant', 'josephson', 'kj'] },
438
+ { name: 'von Klitzing constant', symbol: 'R_K', value: 25812.80745, uncertainty: 0, unit: '\u03A9', aliases: ['von klitzing', 'klitzing constant', 'rk'] },
439
+ { name: 'Bohr magneton', symbol: '\u03BC_B', value: 9.2740100783e-24, uncertainty: 2.8e-33, unit: 'J/T', aliases: ['bohr magneton', 'mu_b', 'magneton'] },
440
+ { name: 'Nuclear magneton', symbol: '\u03BC_N', value: 5.0507837461e-27, uncertainty: 1.5e-36, unit: 'J/T', aliases: ['nuclear magneton', 'mu_n'] },
441
+ { name: 'Electron g-factor', symbol: 'g_e', value: -2.00231930436256, uncertainty: 3.5e-13, unit: '(dimensionless)', aliases: ['electron g-factor', 'g_e', 'electron g factor'] },
442
+ { name: 'Muon mass', symbol: 'm_\u03BC', value: 1.883531627e-28, uncertainty: 4.2e-36, unit: 'kg', aliases: ['muon mass', 'mu mass'] },
443
+ { name: 'Tau mass', symbol: 'm_\u03C4', value: 3.16754e-27, uncertainty: 2.1e-31, unit: 'kg', aliases: ['tau mass'] },
444
+ { name: 'Stefan-Boltzmann constant', symbol: '\u03C3', value: 5.670374419e-8, uncertainty: 0, unit: 'W/(m^2*K^4)', aliases: ['stefan-boltzmann', 'stefan boltzmann', 'sigma_sb'] },
445
+ { name: 'Wien displacement law constant', symbol: 'b', value: 2.897771955e-3, uncertainty: 0, unit: 'm*K', aliases: ['wien', 'wien displacement', 'wien constant'] },
446
+ { name: 'First radiation constant', symbol: 'c_1', value: 3.741771852e-16, uncertainty: 0, unit: 'W*m^2', aliases: ['first radiation constant', 'c1'] },
447
+ { name: 'Second radiation constant', symbol: 'c_2', value: 1.438776877e-2, uncertainty: 0, unit: 'm*K', aliases: ['second radiation constant', 'c2'] },
448
+ { name: 'Molar gas constant', symbol: 'R', value: 8.314462618, uncertainty: 0, unit: 'J/(mol*K)', aliases: ['gas constant', 'molar gas constant', 'R', 'ideal gas constant'] },
449
+ { name: 'Faraday constant', symbol: 'F', value: 96485.33212, uncertainty: 0, unit: 'C/mol', aliases: ['faraday', 'faraday constant'] },
450
+ { name: 'Vacuum permittivity', symbol: '\u03B5_0', value: 8.8541878128e-12, uncertainty: 1.3e-21, unit: 'F/m', aliases: ['vacuum permittivity', 'permittivity of free space', 'epsilon_0', 'epsilon0', 'electric constant'] },
451
+ { name: 'Vacuum permeability', symbol: '\u03BC_0', value: 1.25663706212e-6, uncertainty: 1.9e-16, unit: 'N/A^2', aliases: ['vacuum permeability', 'permeability of free space', 'mu_0', 'mu0', 'magnetic constant'] },
452
+ { name: 'Impedance of free space', symbol: 'Z_0', value: 376.730313668, uncertainty: 5.7e-8, unit: '\u03A9', aliases: ['impedance of free space', 'z0', 'characteristic impedance of vacuum'] },
453
+ { name: 'Coulomb constant', symbol: 'k_e', value: 8.9875517923e9, uncertainty: 1.4, unit: 'N*m^2/C^2', aliases: ['coulomb constant', 'ke', 'k_e', 'electric force constant'] },
454
+ { name: 'Standard atmosphere', symbol: 'atm', value: 101325, uncertainty: 0, unit: 'Pa', aliases: ['standard atmosphere', 'atm pressure'] },
455
+ { name: 'Standard gravity', symbol: 'g_n', value: 9.80665, uncertainty: 0, unit: 'm/s^2', aliases: ['standard gravity', 'g', 'gravitational acceleration', 'g_n'] },
456
+ { name: 'Atomic mass constant', symbol: 'u', value: 1.66053906660e-27, uncertainty: 5.0e-37, unit: 'kg', aliases: ['atomic mass unit', 'amu', 'dalton', 'unified atomic mass unit', 'u'] },
457
+ { name: 'Electron volt', symbol: 'eV', value: 1.602176634e-19, uncertainty: 0, unit: 'J', aliases: ['electron volt', 'eV'] },
458
+ { name: 'Hartree energy', symbol: 'E_h', value: 4.3597447222071e-18, uncertainty: 8.5e-30, unit: 'J', aliases: ['hartree energy', 'hartree', 'eh'] },
459
+ { name: 'Thomson cross section', symbol: '\u03C3_T', value: 6.6524587321e-29, uncertainty: 6.0e-38, unit: 'm^2', aliases: ['thomson cross section', 'sigma_t'] },
460
+ { name: 'Proton-electron mass ratio', symbol: 'm_p/m_e', value: 1836.15267343, uncertainty: 1.1e-7, unit: '(dimensionless)', aliases: ['proton electron mass ratio', 'mp/me'] },
461
+ { name: 'Molar Planck constant', symbol: 'N_A*h', value: 3.990312712e-10, uncertainty: 0, unit: 'J*s/mol', aliases: ['molar planck constant'] },
462
+ { name: 'Loschmidt constant (273.15 K, 101.325 kPa)', symbol: 'n_0', value: 2.6867774e25, uncertainty: 0, unit: '1/m^3', aliases: ['loschmidt', 'loschmidt constant', 'n0'] },
463
+ { name: 'Molar volume of ideal gas (273.15 K, 101.325 kPa)', symbol: 'V_m', value: 22.41396954e-3, uncertainty: 0, unit: 'm^3/mol', aliases: ['molar volume', 'stp molar volume', 'vm'] },
464
+ { name: 'Sackur-Tetrode constant (1 K, 101.325 kPa)', symbol: 'S_0/R', value: -1.15170753706, uncertainty: 4.5e-10, unit: '(dimensionless)', aliases: ['sackur-tetrode', 'sackur tetrode'] },
465
+ { name: 'W boson mass', symbol: 'm_W', value: 1.43298e-25, uncertainty: 1.8e-28, unit: 'kg', aliases: ['w boson mass', 'mw'] },
466
+ { name: 'Z boson mass', symbol: 'm_Z', value: 1.62566e-25, uncertainty: 3.1e-29, unit: 'kg', aliases: ['z boson mass', 'mz'] },
467
+ { name: 'Higgs boson mass', symbol: 'm_H', value: 2.2305e-25, uncertainty: 5.3e-28, unit: 'kg', aliases: ['higgs mass', 'higgs boson mass', 'mh'] },
468
+ { name: 'Proton magnetic moment', symbol: '\u03BC_p', value: 1.41060674333e-26, uncertainty: 4.6e-36, unit: 'J/T', aliases: ['proton magnetic moment', 'mu_p'] },
469
+ { name: 'Neutron magnetic moment', symbol: '\u03BC_n', value: -9.6623651e-27, uncertainty: 2.3e-33, unit: 'J/T', aliases: ['neutron magnetic moment', 'mu_n_mag'] },
470
+ { name: 'Proton gyromagnetic ratio', symbol: '\u03B3_p', value: 2.6752218744e8, uncertainty: 1.1e-2, unit: '1/(s*T)', aliases: ['proton gyromagnetic ratio', 'gamma_p'] },
471
+ { name: 'Electron magnetic moment', symbol: '\u03BC_e', value: -9.2847647043e-24, uncertainty: 2.8e-33, unit: 'J/T', aliases: ['electron magnetic moment', 'mu_e'] },
472
+ { name: 'Proton charge radius', symbol: 'r_p', value: 8.414e-16, uncertainty: 1.9e-18, unit: 'm', aliases: ['proton radius', 'proton charge radius', 'rp'] },
473
+ { name: 'Fermi coupling constant', symbol: 'G_F/(hbar*c)^3', value: 1.1663788e-5, uncertainty: 6e-12, unit: '1/GeV^2', aliases: ['fermi coupling', 'fermi constant', 'gf'] },
474
+ { name: 'Weak mixing angle', symbol: 'sin^2(\u03B8_W)', value: 0.23121, uncertainty: 4e-5, unit: '(dimensionless)', aliases: ['weak mixing angle', 'weinberg angle', 'theta_w'] },
475
+ { name: 'Planck mass', symbol: 'm_P', value: 2.176434e-8, uncertainty: 2.4e-13, unit: 'kg', aliases: ['planck mass', 'mp_planck'] },
476
+ { name: 'Planck length', symbol: 'l_P', value: 1.616255e-35, uncertainty: 1.8e-40, unit: 'm', aliases: ['planck length', 'lp'] },
477
+ { name: 'Planck time', symbol: 't_P', value: 5.391247e-44, uncertainty: 6.0e-49, unit: 's', aliases: ['planck time', 'tp'] },
478
+ { name: 'Planck temperature', symbol: 'T_P', value: 1.416784e32, uncertainty: 1.6e27, unit: 'K', aliases: ['planck temperature', 'tp_temp'] },
479
+ { name: 'Characteristic impedance of vacuum', symbol: 'Z_0', value: 376.730313668, uncertainty: 5.7e-8, unit: '\u03A9', aliases: ['z0_vacuum'] },
480
+ { name: 'Solar mass', symbol: 'M_\u2609', value: 1.989e30, uncertainty: 2e26, unit: 'kg', aliases: ['solar mass', 'sun mass', 'msun', 'm_sun'] },
481
+ { name: 'Earth mass', symbol: 'M_\u2295', value: 5.972e24, uncertainty: 6e20, unit: 'kg', aliases: ['earth mass', 'mearth', 'm_earth'] },
482
+ { name: 'Solar radius', symbol: 'R_\u2609', value: 6.957e8, uncertainty: 1.4e5, unit: 'm', aliases: ['solar radius', 'sun radius', 'rsun'] },
483
+ { name: 'Earth radius (equatorial)', symbol: 'R_\u2295', value: 6.3781e6, uncertainty: 1, unit: 'm', aliases: ['earth radius', 'rearth'] },
484
+ { name: 'Solar luminosity', symbol: 'L_\u2609', value: 3.828e26, uncertainty: 4e22, unit: 'W', aliases: ['solar luminosity', 'sun luminosity', 'lsun'] },
485
+ { name: 'Hubble constant', symbol: 'H_0', value: 67.4, uncertainty: 0.5, unit: 'km/s/Mpc', aliases: ['hubble constant', 'hubble parameter', 'h0'] },
486
+ { name: 'Cosmological constant', symbol: '\u039B', value: 1.1056e-52, uncertainty: 1.5e-54, unit: '1/m^2', aliases: ['cosmological constant', 'lambda', 'dark energy'] },
487
+ { name: 'CMB temperature', symbol: 'T_CMB', value: 2.7255, uncertainty: 6e-4, unit: 'K', aliases: ['cmb temperature', 'cosmic microwave background temperature', 'tcmb'] },
488
+ { name: 'Age of the universe', symbol: 't_0', value: 4.35e17, uncertainty: 2e15, unit: 's', aliases: ['age of universe', 'universe age'] },
489
+ { name: 'Density parameter (matter)', symbol: '\u03A9_m', value: 0.315, uncertainty: 0.007, unit: '(dimensionless)', aliases: ['density parameter matter', 'omega_m'] },
490
+ { name: 'Density parameter (dark energy)', symbol: '\u03A9_\u039B', value: 0.685, uncertainty: 0.007, unit: '(dimensionless)', aliases: ['density parameter dark energy', 'omega_lambda'] },
491
+ { name: 'Proton rms charge radius', symbol: 'r_p', value: 8.414e-16, uncertainty: 1.9e-18, unit: 'm', aliases: ['proton rms radius'] },
492
+ { name: 'Deuteron mass', symbol: 'm_d', value: 3.3435837724e-27, uncertainty: 1.0e-36, unit: 'kg', aliases: ['deuteron mass', 'md'] },
493
+ { name: 'Alpha particle mass', symbol: 'm_\u03B1', value: 6.6446573357e-27, uncertainty: 2.0e-36, unit: 'kg', aliases: ['alpha particle mass', 'helium-4 mass', 'm_alpha'] },
494
+ ];
495
+ /** Fuzzy match a query against constant names/aliases */
496
+ function findConstant(query) {
497
+ const q = query.toLowerCase().trim();
498
+ // Exact alias match
499
+ const exact = CONSTANTS.filter(c => c.aliases.some(a => a.toLowerCase() === q) ||
500
+ c.symbol.toLowerCase() === q ||
501
+ c.name.toLowerCase() === q);
502
+ if (exact.length > 0)
503
+ return exact;
504
+ // Substring match
505
+ const sub = CONSTANTS.filter(c => c.name.toLowerCase().includes(q) ||
506
+ c.aliases.some(a => a.toLowerCase().includes(q)) ||
507
+ c.symbol.toLowerCase().includes(q));
508
+ if (sub.length > 0)
509
+ return sub;
510
+ // Word-level fuzzy: match if all query words appear somewhere
511
+ const words = q.split(/\s+/);
512
+ return CONSTANTS.filter(c => {
513
+ const haystack = `${c.name} ${c.aliases.join(' ')} ${c.symbol}`.toLowerCase();
514
+ return words.every(w => haystack.includes(w));
515
+ });
516
+ }
517
+ const FORMULAS = [
518
+ {
519
+ name: 'Ideal Gas Law', expression: 'PV = nRT',
520
+ variables: { P: 'Pressure (Pa)', V: 'Volume (m^3)', n: 'Amount (mol)', R: 'Gas constant (8.314 J/(mol*K))', T: 'Temperature (K)' },
521
+ aliases: ['ideal gas', 'pv=nrt', 'gas law'],
522
+ solve(solveFor, kv) {
523
+ const R = kv.R ?? 8.314462618;
524
+ switch (solveFor) {
525
+ case 'P': return (kv.n * R * kv.T) / kv.V;
526
+ case 'V': return (kv.n * R * kv.T) / kv.P;
527
+ case 'n': return (kv.P * kv.V) / (R * kv.T);
528
+ case 'T': return (kv.P * kv.V) / (kv.n * R);
529
+ default: return null;
530
+ }
531
+ },
532
+ },
533
+ {
534
+ name: 'Mass-Energy Equivalence', expression: 'E = mc^2',
535
+ variables: { E: 'Energy (J)', m: 'Mass (kg)', c: 'Speed of light (299792458 m/s)' },
536
+ aliases: ['mass energy', 'e=mc2', 'einstein', 'mass-energy'],
537
+ solve(solveFor, kv) {
538
+ const c = 299792458;
539
+ switch (solveFor) {
540
+ case 'E': return kv.m * c * c;
541
+ case 'm': return kv.E / (c * c);
542
+ default: return null;
543
+ }
544
+ },
545
+ },
546
+ {
547
+ name: "Newton's Second Law", expression: 'F = ma',
548
+ variables: { F: 'Force (N)', m: 'Mass (kg)', a: 'Acceleration (m/s^2)' },
549
+ aliases: ['newton second', 'f=ma', 'force'],
550
+ solve(solveFor, kv) {
551
+ switch (solveFor) {
552
+ case 'F': return kv.m * kv.a;
553
+ case 'm': return kv.F / kv.a;
554
+ case 'a': return kv.F / kv.m;
555
+ default: return null;
556
+ }
557
+ },
558
+ },
559
+ {
560
+ name: "Ohm's Law", expression: 'V = IR',
561
+ variables: { V: 'Voltage (V)', I: 'Current (A)', R: 'Resistance (\u03A9)' },
562
+ aliases: ['ohm', 'v=ir', 'ohms law'],
563
+ solve(solveFor, kv) {
564
+ switch (solveFor) {
565
+ case 'V': return kv.I * kv.R;
566
+ case 'I': return kv.V / kv.R;
567
+ case 'R': return kv.V / kv.I;
568
+ default: return null;
569
+ }
570
+ },
571
+ },
572
+ {
573
+ name: 'Coulomb\'s Law', expression: 'F = k_e * q1 * q2 / r^2',
574
+ variables: { F: 'Force (N)', q1: 'Charge 1 (C)', q2: 'Charge 2 (C)', r: 'Distance (m)', k_e: 'Coulomb constant (8.9876e9 N*m^2/C^2)' },
575
+ aliases: ['coulomb', 'coulombs law', 'electric force'],
576
+ solve(solveFor, kv) {
577
+ const ke = kv.k_e ?? 8.9875517923e9;
578
+ switch (solveFor) {
579
+ case 'F': return ke * kv.q1 * kv.q2 / (kv.r * kv.r);
580
+ case 'q1': return kv.F * kv.r * kv.r / (ke * kv.q2);
581
+ case 'q2': return kv.F * kv.r * kv.r / (ke * kv.q1);
582
+ case 'r': return Math.sqrt(ke * kv.q1 * kv.q2 / kv.F);
583
+ default: return null;
584
+ }
585
+ },
586
+ },
587
+ {
588
+ name: 'Kinetic Energy', expression: 'KE = 0.5 * m * v^2',
589
+ variables: { KE: 'Kinetic energy (J)', m: 'Mass (kg)', v: 'Velocity (m/s)' },
590
+ aliases: ['kinetic energy', 'ke'],
591
+ solve(solveFor, kv) {
592
+ switch (solveFor) {
593
+ case 'KE': return 0.5 * kv.m * kv.v * kv.v;
594
+ case 'm': return 2 * kv.KE / (kv.v * kv.v);
595
+ case 'v': return Math.sqrt(2 * kv.KE / kv.m);
596
+ default: return null;
597
+ }
598
+ },
599
+ },
600
+ {
601
+ name: 'Gravitational Potential Energy', expression: 'U = mgh',
602
+ variables: { U: 'Potential energy (J)', m: 'Mass (kg)', g: 'Gravitational accel (m/s^2)', h: 'Height (m)' },
603
+ aliases: ['gravitational potential', 'potential energy', 'u=mgh', 'mgh'],
604
+ solve(solveFor, kv) {
605
+ const g = kv.g ?? 9.80665;
606
+ switch (solveFor) {
607
+ case 'U': return kv.m * g * kv.h;
608
+ case 'm': return kv.U / (g * kv.h);
609
+ case 'h': return kv.U / (kv.m * g);
610
+ default: return null;
611
+ }
612
+ },
613
+ },
614
+ {
615
+ name: 'Electric Power', expression: 'P = IV',
616
+ variables: { P: 'Power (W)', I: 'Current (A)', V: 'Voltage (V)' },
617
+ aliases: ['electric power', 'p=iv', 'power electrical'],
618
+ solve(solveFor, kv) {
619
+ switch (solveFor) {
620
+ case 'P': return kv.I * kv.V;
621
+ case 'I': return kv.P / kv.V;
622
+ case 'V': return kv.P / kv.I;
623
+ default: return null;
624
+ }
625
+ },
626
+ },
627
+ {
628
+ name: 'Wave Equation', expression: 'v = f\u03BB',
629
+ variables: { v: 'Wave speed (m/s)', f: 'Frequency (Hz)', lambda: 'Wavelength (m)' },
630
+ aliases: ['wave equation', 'v=flambda', 'wave speed'],
631
+ solve(solveFor, kv) {
632
+ switch (solveFor) {
633
+ case 'v': return kv.f * kv.lambda;
634
+ case 'f': return kv.v / kv.lambda;
635
+ case 'lambda': return kv.v / kv.f;
636
+ default: return null;
637
+ }
638
+ },
639
+ },
640
+ {
641
+ name: 'Photon Energy', expression: 'E = hf',
642
+ variables: { E: 'Energy (J)', h: 'Planck constant (6.626e-34 J*s)', f: 'Frequency (Hz)' },
643
+ aliases: ['photon energy', 'e=hf', 'planck relation'],
644
+ solve(solveFor, kv) {
645
+ const h = kv.h ?? 6.62607015e-34;
646
+ switch (solveFor) {
647
+ case 'E': return h * kv.f;
648
+ case 'f': return kv.E / h;
649
+ default: return null;
650
+ }
651
+ },
652
+ },
653
+ {
654
+ name: 'de Broglie Wavelength', expression: '\u03BB = h / p',
655
+ variables: { lambda: 'Wavelength (m)', h: 'Planck constant (6.626e-34 J*s)', p: 'Momentum (kg*m/s)' },
656
+ aliases: ['de broglie', 'matter wave', 'lambda=h/p'],
657
+ solve(solveFor, kv) {
658
+ const h = kv.h ?? 6.62607015e-34;
659
+ switch (solveFor) {
660
+ case 'lambda': return h / kv.p;
661
+ case 'p': return h / kv.lambda;
662
+ default: return null;
663
+ }
664
+ },
665
+ },
666
+ {
667
+ name: "Snell's Law", expression: 'n1 * sin(\u03B81) = n2 * sin(\u03B82)',
668
+ variables: { n1: 'Refractive index 1', theta1: 'Angle 1 (rad)', n2: 'Refractive index 2', theta2: 'Angle 2 (rad)' },
669
+ aliases: ['snell', 'snells law', 'refraction'],
670
+ solve(solveFor, kv) {
671
+ switch (solveFor) {
672
+ case 'theta2': return Math.asin(kv.n1 * Math.sin(kv.theta1) / kv.n2);
673
+ case 'theta1': return Math.asin(kv.n2 * Math.sin(kv.theta2) / kv.n1);
674
+ case 'n1': return kv.n2 * Math.sin(kv.theta2) / Math.sin(kv.theta1);
675
+ case 'n2': return kv.n1 * Math.sin(kv.theta1) / Math.sin(kv.theta2);
676
+ default: return null;
677
+ }
678
+ },
679
+ },
680
+ {
681
+ name: 'Stefan-Boltzmann Law', expression: 'P = \u03C3 * A * T^4',
682
+ variables: { P: 'Radiated power (W)', sigma: 'Stefan-Boltzmann constant (5.670e-8 W/(m^2*K^4))', A: 'Surface area (m^2)', T: 'Temperature (K)' },
683
+ aliases: ['stefan-boltzmann', 'stefan boltzmann', 'blackbody radiation', 'thermal radiation'],
684
+ solve(solveFor, kv) {
685
+ const sigma = kv.sigma ?? 5.670374419e-8;
686
+ switch (solveFor) {
687
+ case 'P': return sigma * kv.A * Math.pow(kv.T, 4);
688
+ case 'A': return kv.P / (sigma * Math.pow(kv.T, 4));
689
+ case 'T': return Math.pow(kv.P / (sigma * kv.A), 0.25);
690
+ default: return null;
691
+ }
692
+ },
693
+ },
694
+ {
695
+ name: 'Schwarzschild Radius', expression: 'r_s = 2GM/c^2',
696
+ variables: { r_s: 'Schwarzschild radius (m)', G: 'Gravitational constant (6.674e-11)', M: 'Mass (kg)', c: 'Speed of light (299792458 m/s)' },
697
+ aliases: ['schwarzschild', 'event horizon', 'black hole radius'],
698
+ solve(solveFor, kv) {
699
+ const G = kv.G ?? 6.67430e-11;
700
+ const c = 299792458;
701
+ switch (solveFor) {
702
+ case 'r_s': return 2 * G * kv.M / (c * c);
703
+ case 'M': return kv.r_s * c * c / (2 * G);
704
+ default: return null;
705
+ }
706
+ },
707
+ },
708
+ {
709
+ name: 'Gravitational Force', expression: 'F = G * m1 * m2 / r^2',
710
+ variables: { F: 'Force (N)', G: 'Gravitational constant (6.674e-11)', m1: 'Mass 1 (kg)', m2: 'Mass 2 (kg)', r: 'Distance (m)' },
711
+ aliases: ['gravitational force', 'gravity force', 'newton gravity'],
712
+ solve(solveFor, kv) {
713
+ const G = kv.G ?? 6.67430e-11;
714
+ switch (solveFor) {
715
+ case 'F': return G * kv.m1 * kv.m2 / (kv.r * kv.r);
716
+ case 'm1': return kv.F * kv.r * kv.r / (G * kv.m2);
717
+ case 'm2': return kv.F * kv.r * kv.r / (G * kv.m1);
718
+ case 'r': return Math.sqrt(G * kv.m1 * kv.m2 / kv.F);
719
+ default: return null;
720
+ }
721
+ },
722
+ },
723
+ {
724
+ name: 'Centripetal Force', expression: 'F = mv^2/r',
725
+ variables: { F: 'Force (N)', m: 'Mass (kg)', v: 'Velocity (m/s)', r: 'Radius (m)' },
726
+ aliases: ['centripetal', 'centripetal force'],
727
+ solve(solveFor, kv) {
728
+ switch (solveFor) {
729
+ case 'F': return kv.m * kv.v * kv.v / kv.r;
730
+ case 'm': return kv.F * kv.r / (kv.v * kv.v);
731
+ case 'v': return Math.sqrt(kv.F * kv.r / kv.m);
732
+ case 'r': return kv.m * kv.v * kv.v / kv.F;
733
+ default: return null;
734
+ }
735
+ },
736
+ },
737
+ {
738
+ name: 'Simple Harmonic Motion Period', expression: 'T = 2\u03C0\u221A(m/k)',
739
+ variables: { T: 'Period (s)', m: 'Mass (kg)', k: 'Spring constant (N/m)' },
740
+ aliases: ['simple harmonic', 'shm period', 'spring period', 'harmonic oscillator'],
741
+ solve(solveFor, kv) {
742
+ switch (solveFor) {
743
+ case 'T': return 2 * Math.PI * Math.sqrt(kv.m / kv.k);
744
+ case 'm': return kv.k * (kv.T / (2 * Math.PI)) ** 2;
745
+ case 'k': return kv.m * (2 * Math.PI / kv.T) ** 2;
746
+ default: return null;
747
+ }
748
+ },
749
+ },
750
+ {
751
+ name: 'Pendulum Period', expression: 'T = 2\u03C0\u221A(L/g)',
752
+ variables: { T: 'Period (s)', L: 'Length (m)', g: 'Gravitational accel (m/s^2)' },
753
+ aliases: ['pendulum', 'pendulum period'],
754
+ solve(solveFor, kv) {
755
+ const g = kv.g ?? 9.80665;
756
+ switch (solveFor) {
757
+ case 'T': return 2 * Math.PI * Math.sqrt(kv.L / g);
758
+ case 'L': return g * (kv.T / (2 * Math.PI)) ** 2;
759
+ default: return null;
760
+ }
761
+ },
762
+ },
763
+ {
764
+ name: 'Capacitor Energy', expression: 'E = 0.5 * C * V^2',
765
+ variables: { E: 'Energy (J)', C: 'Capacitance (F)', V: 'Voltage (V)' },
766
+ aliases: ['capacitor energy', 'capacitor'],
767
+ solve(solveFor, kv) {
768
+ switch (solveFor) {
769
+ case 'E': return 0.5 * kv.C * kv.V * kv.V;
770
+ case 'C': return 2 * kv.E / (kv.V * kv.V);
771
+ case 'V': return Math.sqrt(2 * kv.E / kv.C);
772
+ default: return null;
773
+ }
774
+ },
775
+ },
776
+ {
777
+ name: 'Inductor Energy', expression: 'E = 0.5 * L * I^2',
778
+ variables: { E: 'Energy (J)', L: 'Inductance (H)', I: 'Current (A)' },
779
+ aliases: ['inductor energy', 'inductor'],
780
+ solve(solveFor, kv) {
781
+ switch (solveFor) {
782
+ case 'E': return 0.5 * kv.L * kv.I * kv.I;
783
+ case 'L': return 2 * kv.E / (kv.I * kv.I);
784
+ case 'I': return Math.sqrt(2 * kv.E / kv.L);
785
+ default: return null;
786
+ }
787
+ },
788
+ },
789
+ {
790
+ name: 'Escape Velocity', expression: 'v_e = \u221A(2GM/r)',
791
+ variables: { v_e: 'Escape velocity (m/s)', G: 'Gravitational constant (6.674e-11)', M: 'Mass (kg)', r: 'Radius (m)' },
792
+ aliases: ['escape velocity', 'escape speed'],
793
+ solve(solveFor, kv) {
794
+ const G = kv.G ?? 6.67430e-11;
795
+ switch (solveFor) {
796
+ case 'v_e': return Math.sqrt(2 * G * kv.M / kv.r);
797
+ case 'M': return kv.v_e * kv.v_e * kv.r / (2 * G);
798
+ case 'r': return 2 * G * kv.M / (kv.v_e * kv.v_e);
799
+ default: return null;
800
+ }
801
+ },
802
+ },
803
+ {
804
+ name: 'Orbital Velocity', expression: 'v = \u221A(GM/r)',
805
+ variables: { v: 'Orbital velocity (m/s)', G: 'Gravitational constant (6.674e-11)', M: 'Central mass (kg)', r: 'Orbital radius (m)' },
806
+ aliases: ['orbital velocity', 'circular orbit'],
807
+ solve(solveFor, kv) {
808
+ const G = kv.G ?? 6.67430e-11;
809
+ switch (solveFor) {
810
+ case 'v': return Math.sqrt(G * kv.M / kv.r);
811
+ case 'M': return kv.v * kv.v * kv.r / G;
812
+ case 'r': return G * kv.M / (kv.v * kv.v);
813
+ default: return null;
814
+ }
815
+ },
816
+ },
817
+ {
818
+ name: "Kepler's Third Law", expression: 'T^2 = (4\u03C0^2 / GM) * a^3',
819
+ variables: { T: 'Orbital period (s)', G: 'Gravitational constant (6.674e-11)', M: 'Central mass (kg)', a: 'Semi-major axis (m)' },
820
+ aliases: ['kepler third', 'keplers third law', 'kepler 3'],
821
+ solve(solveFor, kv) {
822
+ const G = kv.G ?? 6.67430e-11;
823
+ const coeff = 4 * Math.PI * Math.PI / (G * kv.M);
824
+ switch (solveFor) {
825
+ case 'T': return Math.sqrt(coeff * Math.pow(kv.a, 3));
826
+ case 'a': return Math.pow(kv.T * kv.T / coeff, 1 / 3);
827
+ case 'M': return 4 * Math.PI * Math.PI * Math.pow(kv.a, 3) / (G * kv.T * kv.T);
828
+ default: return null;
829
+ }
830
+ },
831
+ },
832
+ {
833
+ name: 'Doppler Effect (light)', expression: 'f_obs = f_src * \u221A((1-\u03B2)/(1+\u03B2))',
834
+ variables: { f_obs: 'Observed frequency (Hz)', f_src: 'Source frequency (Hz)', beta: 'v/c (ratio)' },
835
+ aliases: ['doppler', 'relativistic doppler', 'redshift'],
836
+ solve(solveFor, kv) {
837
+ switch (solveFor) {
838
+ case 'f_obs': return kv.f_src * Math.sqrt((1 - kv.beta) / (1 + kv.beta));
839
+ case 'f_src': return kv.f_obs / Math.sqrt((1 - kv.beta) / (1 + kv.beta));
840
+ case 'beta': {
841
+ const r = kv.f_obs / kv.f_src;
842
+ return (1 - r * r) / (1 + r * r);
843
+ }
844
+ default: return null;
845
+ }
846
+ },
847
+ },
848
+ {
849
+ name: 'Lorentz Factor', expression: '\u03B3 = 1 / \u221A(1 - v^2/c^2)',
850
+ variables: { gamma: 'Lorentz factor', v: 'Velocity (m/s)', c: 'Speed of light (299792458 m/s)' },
851
+ aliases: ['lorentz factor', 'gamma factor', 'time dilation', 'lorentz'],
852
+ solve(solveFor, kv) {
853
+ const c = 299792458;
854
+ switch (solveFor) {
855
+ case 'gamma': return 1 / Math.sqrt(1 - (kv.v * kv.v) / (c * c));
856
+ case 'v': return c * Math.sqrt(1 - 1 / (kv.gamma * kv.gamma));
857
+ default: return null;
858
+ }
859
+ },
860
+ },
861
+ {
862
+ name: 'Diffraction Grating', expression: 'd * sin(\u03B8) = m\u03BB',
863
+ variables: { d: 'Slit spacing (m)', theta: 'Angle (rad)', m_order: 'Order (integer)', lambda: 'Wavelength (m)' },
864
+ aliases: ['diffraction', 'grating equation', 'diffraction grating'],
865
+ solve(solveFor, kv) {
866
+ switch (solveFor) {
867
+ case 'lambda': return kv.d * Math.sin(kv.theta) / kv.m_order;
868
+ case 'theta': return Math.asin(kv.m_order * kv.lambda / kv.d);
869
+ case 'd': return kv.m_order * kv.lambda / Math.sin(kv.theta);
870
+ case 'm_order': return kv.d * Math.sin(kv.theta) / kv.lambda;
871
+ default: return null;
872
+ }
873
+ },
874
+ },
875
+ {
876
+ name: 'Beer-Lambert Law', expression: 'A = \u03B5 * l * c',
877
+ variables: { A: 'Absorbance', epsilon: 'Molar absorptivity (L/(mol*cm))', l: 'Path length (cm)', c_conc: 'Concentration (mol/L)' },
878
+ aliases: ['beer lambert', 'beer-lambert', 'absorbance', 'beer law'],
879
+ solve(solveFor, kv) {
880
+ switch (solveFor) {
881
+ case 'A': return kv.epsilon * kv.l * kv.c_conc;
882
+ case 'epsilon': return kv.A / (kv.l * kv.c_conc);
883
+ case 'l': return kv.A / (kv.epsilon * kv.c_conc);
884
+ case 'c_conc': return kv.A / (kv.epsilon * kv.l);
885
+ default: return null;
886
+ }
887
+ },
888
+ },
889
+ {
890
+ name: 'Nernst Equation', expression: 'E = E\u00B0 - (RT/nF) * ln(Q)',
891
+ variables: { E: 'Cell potential (V)', E0: 'Standard potential (V)', R: 'Gas constant (8.314)', T: 'Temperature (K)', n: 'Electrons transferred', F_const: 'Faraday constant (96485)', Q: 'Reaction quotient' },
892
+ aliases: ['nernst', 'nernst equation', 'electrochemistry'],
893
+ solve(solveFor, kv) {
894
+ const R = kv.R ?? 8.314462618;
895
+ const Fc = kv.F_const ?? 96485.33212;
896
+ switch (solveFor) {
897
+ case 'E': return kv.E0 - (R * kv.T / (kv.n * Fc)) * Math.log(kv.Q);
898
+ case 'Q': return Math.exp((kv.E0 - kv.E) * kv.n * Fc / (R * kv.T));
899
+ case 'E0': return kv.E + (R * kv.T / (kv.n * Fc)) * Math.log(kv.Q);
900
+ default: return null;
901
+ }
902
+ },
903
+ },
904
+ {
905
+ name: 'Arrhenius Equation', expression: 'k = A * exp(-Ea/(RT))',
906
+ variables: { k: 'Rate constant', A_pre: 'Pre-exponential factor', Ea: 'Activation energy (J/mol)', R: 'Gas constant (8.314)', T: 'Temperature (K)' },
907
+ aliases: ['arrhenius', 'reaction rate', 'activation energy'],
908
+ solve(solveFor, kv) {
909
+ const R = kv.R ?? 8.314462618;
910
+ switch (solveFor) {
911
+ case 'k': return kv.A_pre * Math.exp(-kv.Ea / (R * kv.T));
912
+ case 'Ea': return -R * kv.T * Math.log(kv.k / kv.A_pre);
913
+ case 'T': return -kv.Ea / (R * Math.log(kv.k / kv.A_pre));
914
+ case 'A_pre': return kv.k / Math.exp(-kv.Ea / (R * kv.T));
915
+ default: return null;
916
+ }
917
+ },
918
+ },
919
+ {
920
+ name: 'Clausius-Clapeyron', expression: 'ln(P2/P1) = -\u0394Hvap/R * (1/T2 - 1/T1)',
921
+ variables: { P1: 'Pressure 1 (Pa)', P2: 'Pressure 2 (Pa)', T1: 'Temperature 1 (K)', T2: 'Temperature 2 (K)', dHvap: 'Enthalpy of vaporization (J/mol)', R: 'Gas constant (8.314)' },
922
+ aliases: ['clausius-clapeyron', 'clausius clapeyron', 'vapor pressure'],
923
+ solve(solveFor, kv) {
924
+ const R = kv.R ?? 8.314462618;
925
+ switch (solveFor) {
926
+ case 'P2': return kv.P1 * Math.exp(-kv.dHvap / R * (1 / kv.T2 - 1 / kv.T1));
927
+ case 'T2': return 1 / (1 / kv.T1 - R * Math.log(kv.P2 / kv.P1) / kv.dHvap);
928
+ case 'dHvap': return -R * Math.log(kv.P2 / kv.P1) / (1 / kv.T2 - 1 / kv.T1);
929
+ default: return null;
930
+ }
931
+ },
932
+ },
933
+ {
934
+ name: 'Hubble Law', expression: 'v = H_0 * d',
935
+ variables: { v: 'Recession velocity (km/s)', H0: 'Hubble constant (~67.4 km/s/Mpc)', d: 'Distance (Mpc)' },
936
+ aliases: ['hubble law', 'hubble', 'recession velocity'],
937
+ solve(solveFor, kv) {
938
+ const H = kv.H0 ?? 67.4;
939
+ switch (solveFor) {
940
+ case 'v': return H * kv.d;
941
+ case 'd': return kv.v / H;
942
+ case 'H0': return kv.v / kv.d;
943
+ default: return null;
944
+ }
945
+ },
946
+ },
947
+ {
948
+ name: 'Lens Equation', expression: '1/f = 1/do + 1/di',
949
+ variables: { f: 'Focal length (m)', do_dist: 'Object distance (m)', di: 'Image distance (m)' },
950
+ aliases: ['lens equation', 'thin lens', 'mirror equation'],
951
+ solve(solveFor, kv) {
952
+ switch (solveFor) {
953
+ case 'f': return 1 / (1 / kv.do_dist + 1 / kv.di);
954
+ case 'do_dist': return 1 / (1 / kv.f - 1 / kv.di);
955
+ case 'di': return 1 / (1 / kv.f - 1 / kv.do_dist);
956
+ default: return null;
957
+ }
958
+ },
959
+ },
960
+ {
961
+ name: 'Resistors in Parallel', expression: '1/R_total = 1/R1 + 1/R2',
962
+ variables: { R_total: 'Total resistance (\u03A9)', R1: 'Resistance 1 (\u03A9)', R2: 'Resistance 2 (\u03A9)' },
963
+ aliases: ['parallel resistance', 'resistors parallel'],
964
+ solve(solveFor, kv) {
965
+ switch (solveFor) {
966
+ case 'R_total': return 1 / (1 / kv.R1 + 1 / kv.R2);
967
+ case 'R1': return 1 / (1 / kv.R_total - 1 / kv.R2);
968
+ case 'R2': return 1 / (1 / kv.R_total - 1 / kv.R1);
969
+ default: return null;
970
+ }
971
+ },
972
+ },
973
+ {
974
+ name: 'RC Time Constant', expression: '\u03C4 = RC',
975
+ variables: { tau: 'Time constant (s)', R: 'Resistance (\u03A9)', C: 'Capacitance (F)' },
976
+ aliases: ['rc circuit', 'time constant', 'rc time constant'],
977
+ solve(solveFor, kv) {
978
+ switch (solveFor) {
979
+ case 'tau': return kv.R * kv.C;
980
+ case 'R': return kv.tau / kv.C;
981
+ case 'C': return kv.tau / kv.R;
982
+ default: return null;
983
+ }
984
+ },
985
+ },
986
+ {
987
+ name: 'Heisenberg Uncertainty (position-momentum)', expression: '\u0394x * \u0394p >= \u0127/2',
988
+ variables: { dx: 'Position uncertainty (m)', dp: 'Momentum uncertainty (kg*m/s)' },
989
+ aliases: ['heisenberg', 'uncertainty principle', 'heisenberg uncertainty'],
990
+ solve(solveFor, kv) {
991
+ const hbar = 1.054571817e-34;
992
+ switch (solveFor) {
993
+ case 'dx': return hbar / (2 * kv.dp);
994
+ case 'dp': return hbar / (2 * kv.dx);
995
+ default: return null;
996
+ }
997
+ },
998
+ },
999
+ {
1000
+ name: 'Boltzmann Distribution', expression: 'N2/N1 = exp(-\u0394E/(k_B*T))',
1001
+ variables: { ratio: 'N2/N1 population ratio', dE: 'Energy difference (J)', k_B: 'Boltzmann constant (1.381e-23 J/K)', T: 'Temperature (K)' },
1002
+ aliases: ['boltzmann distribution', 'population ratio', 'thermal distribution'],
1003
+ solve(solveFor, kv) {
1004
+ const kB = kv.k_B ?? 1.380649e-23;
1005
+ switch (solveFor) {
1006
+ case 'ratio': return Math.exp(-kv.dE / (kB * kv.T));
1007
+ case 'dE': return -kB * kv.T * Math.log(kv.ratio);
1008
+ case 'T': return -kv.dE / (kB * Math.log(kv.ratio));
1009
+ default: return null;
1010
+ }
1011
+ },
1012
+ },
1013
+ {
1014
+ name: 'Bernoulli Equation', expression: 'P + 0.5\u03C1v^2 + \u03C1gh = const',
1015
+ variables: { P1: 'Pressure 1 (Pa)', rho: 'Fluid density (kg/m^3)', v1: 'Velocity 1 (m/s)', h1: 'Height 1 (m)', P2: 'Pressure 2 (Pa)', v2: 'Velocity 2 (m/s)', h2: 'Height 2 (m)' },
1016
+ aliases: ['bernoulli', 'fluid dynamics', 'bernoulli equation'],
1017
+ solve(solveFor, kv) {
1018
+ const g = 9.80665;
1019
+ switch (solveFor) {
1020
+ case 'P2': return kv.P1 + 0.5 * kv.rho * (kv.v1 ** 2 - kv.v2 ** 2) + kv.rho * g * (kv.h1 - kv.h2);
1021
+ case 'v2': return Math.sqrt(kv.v1 ** 2 + 2 * (kv.P1 - kv.P2) / kv.rho + 2 * g * (kv.h1 - kv.h2));
1022
+ case 'P1': return kv.P2 - 0.5 * kv.rho * (kv.v1 ** 2 - kv.v2 ** 2) - kv.rho * g * (kv.h1 - kv.h2);
1023
+ default: return null;
1024
+ }
1025
+ },
1026
+ },
1027
+ {
1028
+ name: 'Drag Force', expression: 'F_d = 0.5 * C_d * \u03C1 * A * v^2',
1029
+ variables: { F_d: 'Drag force (N)', C_d: 'Drag coefficient', rho: 'Fluid density (kg/m^3)', A: 'Reference area (m^2)', v: 'Velocity (m/s)' },
1030
+ aliases: ['drag force', 'air resistance', 'drag equation'],
1031
+ solve(solveFor, kv) {
1032
+ switch (solveFor) {
1033
+ case 'F_d': return 0.5 * kv.C_d * kv.rho * kv.A * kv.v * kv.v;
1034
+ case 'v': return Math.sqrt(2 * kv.F_d / (kv.C_d * kv.rho * kv.A));
1035
+ case 'C_d': return 2 * kv.F_d / (kv.rho * kv.A * kv.v * kv.v);
1036
+ default: return null;
1037
+ }
1038
+ },
1039
+ },
1040
+ {
1041
+ name: 'Torque', expression: '\u03C4 = r * F * sin(\u03B8)',
1042
+ variables: { tau: 'Torque (N*m)', r: 'Lever arm (m)', F: 'Force (N)', theta: 'Angle (rad)' },
1043
+ aliases: ['torque', 'moment of force'],
1044
+ solve(solveFor, kv) {
1045
+ switch (solveFor) {
1046
+ case 'tau': return kv.r * kv.F * Math.sin(kv.theta);
1047
+ case 'F': return kv.tau / (kv.r * Math.sin(kv.theta));
1048
+ case 'r': return kv.tau / (kv.F * Math.sin(kv.theta));
1049
+ case 'theta': return Math.asin(kv.tau / (kv.r * kv.F));
1050
+ default: return null;
1051
+ }
1052
+ },
1053
+ },
1054
+ {
1055
+ name: 'Entropy Change', expression: '\u0394S = Q/T',
1056
+ variables: { dS: 'Entropy change (J/K)', Q: 'Heat transfer (J)', T: 'Temperature (K)' },
1057
+ aliases: ['entropy', 'entropy change'],
1058
+ solve(solveFor, kv) {
1059
+ switch (solveFor) {
1060
+ case 'dS': return kv.Q / kv.T;
1061
+ case 'Q': return kv.dS * kv.T;
1062
+ case 'T': return kv.Q / kv.dS;
1063
+ default: return null;
1064
+ }
1065
+ },
1066
+ },
1067
+ {
1068
+ name: 'Carnot Efficiency', expression: '\u03B7 = 1 - T_cold/T_hot',
1069
+ variables: { eta: 'Efficiency (0-1)', T_cold: 'Cold reservoir temp (K)', T_hot: 'Hot reservoir temp (K)' },
1070
+ aliases: ['carnot', 'carnot efficiency', 'heat engine efficiency'],
1071
+ solve(solveFor, kv) {
1072
+ switch (solveFor) {
1073
+ case 'eta': return 1 - kv.T_cold / kv.T_hot;
1074
+ case 'T_cold': return kv.T_hot * (1 - kv.eta);
1075
+ case 'T_hot': return kv.T_cold / (1 - kv.eta);
1076
+ default: return null;
1077
+ }
1078
+ },
1079
+ },
1080
+ {
1081
+ name: 'Magnetic Force on Moving Charge', expression: 'F = qvB*sin(\u03B8)',
1082
+ variables: { F: 'Force (N)', q: 'Charge (C)', v: 'Velocity (m/s)', B: 'Magnetic field (T)', theta: 'Angle (rad)' },
1083
+ aliases: ['lorentz force', 'magnetic force', 'qvb'],
1084
+ solve(solveFor, kv) {
1085
+ switch (solveFor) {
1086
+ case 'F': return kv.q * kv.v * kv.B * Math.sin(kv.theta);
1087
+ case 'v': return kv.F / (kv.q * kv.B * Math.sin(kv.theta));
1088
+ case 'B': return kv.F / (kv.q * kv.v * Math.sin(kv.theta));
1089
+ default: return null;
1090
+ }
1091
+ },
1092
+ },
1093
+ {
1094
+ name: 'Planck Radiation Law (peak)', expression: '\u03BB_max = b / T',
1095
+ variables: { lambda_max: 'Peak wavelength (m)', b: 'Wien constant (2.898e-3 m*K)', T: 'Temperature (K)' },
1096
+ aliases: ['wien law', 'planck peak', 'peak wavelength'],
1097
+ solve(solveFor, kv) {
1098
+ const b = kv.b ?? 2.897771955e-3;
1099
+ switch (solveFor) {
1100
+ case 'lambda_max': return b / kv.T;
1101
+ case 'T': return b / kv.lambda_max;
1102
+ default: return null;
1103
+ }
1104
+ },
1105
+ },
1106
+ {
1107
+ name: 'Rocket Equation (Tsiolkovsky)', expression: '\u0394v = v_e * ln(m_0/m_f)',
1108
+ variables: { dv: 'Delta-v (m/s)', v_e: 'Exhaust velocity (m/s)', m_0: 'Initial mass (kg)', m_f: 'Final mass (kg)' },
1109
+ aliases: ['tsiolkovsky', 'rocket equation', 'delta-v'],
1110
+ solve(solveFor, kv) {
1111
+ switch (solveFor) {
1112
+ case 'dv': return kv.v_e * Math.log(kv.m_0 / kv.m_f);
1113
+ case 'm_0': return kv.m_f * Math.exp(kv.dv / kv.v_e);
1114
+ case 'm_f': return kv.m_0 / Math.exp(kv.dv / kv.v_e);
1115
+ case 'v_e': return kv.dv / Math.log(kv.m_0 / kv.m_f);
1116
+ default: return null;
1117
+ }
1118
+ },
1119
+ },
1120
+ ];
1121
+ function findFormula(query) {
1122
+ const q = query.toLowerCase().trim();
1123
+ for (const f of FORMULAS) {
1124
+ if (f.expression.toLowerCase() === q || f.name.toLowerCase() === q)
1125
+ return f;
1126
+ if (f.aliases.some(a => a.toLowerCase() === q))
1127
+ return f;
1128
+ }
1129
+ // Partial match
1130
+ for (const f of FORMULAS) {
1131
+ if (f.name.toLowerCase().includes(q) || f.expression.toLowerCase().includes(q))
1132
+ return f;
1133
+ if (f.aliases.some(a => a.toLowerCase().includes(q)))
1134
+ return f;
1135
+ }
1136
+ return null;
1137
+ }
1138
+ // ─── Fetch Helper ───────────────────────────────────────────────────────────
1139
+ const UA = 'KBot/3.0 (Lab Tools)';
1140
+ async function labFetch(url) {
1141
+ return fetch(url, {
1142
+ headers: { 'User-Agent': UA },
1143
+ signal: AbortSignal.timeout(10000),
1144
+ });
1145
+ }
1146
+ async function labFetchJson(url) {
1147
+ const res = await labFetch(url);
1148
+ if (!res.ok)
1149
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
1150
+ return res.json();
1151
+ }
1152
+ // ─── Tool Implementations ───────────────────────────────────────────────────
1153
+ export function registerLabCoreTools() {
1154
+ // ── 1. Experiment Design ────────────────────────────────────────────────
1155
+ registerTool({
1156
+ name: 'experiment_design',
1157
+ description: 'Generate a structured experimental design from a research question. Covers RCT, factorial, observational, crossover, and longitudinal designs. Outputs variables, controls, sample size formula, randomization scheme, and statistical test recommendation.',
1158
+ parameters: {
1159
+ research_question: { type: 'string', description: 'The research question to design an experiment for', required: true },
1160
+ design_type: { type: 'string', description: 'Design type: rct, factorial, observational, crossover, longitudinal', required: true },
1161
+ variables: { type: 'string', description: 'Comma-separated list of variables to consider (optional)' },
1162
+ },
1163
+ tier: 'free',
1164
+ async execute(args) {
1165
+ const question = String(args.research_question);
1166
+ const designType = String(args.design_type).toLowerCase().trim();
1167
+ const variables = args.variables ? String(args.variables).split(',').map(v => v.trim()) : [];
1168
+ const designs = {
1169
+ rct: {
1170
+ fullName: 'Randomized Controlled Trial (RCT)',
1171
+ description: 'Gold standard for establishing causal relationships. Participants randomly assigned to treatment or control groups.',
1172
+ structure: '1. Define target population and eligibility criteria\n2. Randomize participants to treatment (T) and control (C) groups\n3. Administer intervention to T, placebo/standard care to C\n4. Measure primary and secondary outcomes\n5. Analyze with intention-to-treat (ITT) principle',
1173
+ randomization: 'Simple randomization (coin flip), block randomization (blocks of 4-6), stratified randomization (by key confounders), or adaptive randomization (minimization)',
1174
+ sampleSizeFormula: 'n = (Z_{alpha/2} + Z_{beta})^2 * 2 * sigma^2 / delta^2\nWhere: Z_{alpha/2} = 1.96 (for alpha=0.05), Z_{beta} = 0.84 (for power=0.80), sigma = population SD, delta = minimum detectable effect',
1175
+ statisticalTests: ['Independent t-test (continuous outcome)', 'Chi-square test (categorical outcome)', 'ANCOVA (adjusted for baseline)', 'Cox regression (time-to-event)', 'Mixed-effects model (repeated measures)'],
1176
+ strengths: ['Strongest evidence for causation', 'Controls for known and unknown confounders', 'Minimizes selection bias'],
1177
+ limitations: ['Expensive and time-consuming', 'May not be ethical for all questions', 'External validity concerns (strict inclusion criteria)'],
1178
+ ethicalConsiderations: ['Equipoise must exist', 'Informed consent required', 'IRB/Ethics committee approval', 'Data Safety Monitoring Board (DSMB) for interim analyses'],
1179
+ },
1180
+ factorial: {
1181
+ fullName: 'Factorial Design',
1182
+ description: 'Tests multiple factors simultaneously, revealing main effects and interactions. Efficient for studying combinations.',
1183
+ structure: '1. Identify factors (A, B, ...) and levels for each\n2. Create all factor-level combinations (full factorial) or select subset (fractional)\n3. Randomly assign participants to combinations\n4. Measure outcomes for each combination\n5. Analyze main effects and interactions via ANOVA',
1184
+ randomization: 'Complete randomization to factorial cells. For 2x2: participants randomly assigned to one of 4 cells (A+B+, A+B-, A-B+, A-B-)',
1185
+ sampleSizeFormula: 'n_per_cell = (Z_{alpha/2} + Z_{beta})^2 * sigma^2 / delta^2\nTotal N = n_per_cell * number_of_cells\nFor 2^k design: number_of_cells = 2^k\nFractional designs: 2^(k-p) cells (Resolution III+ recommended)',
1186
+ statisticalTests: ['Two-way/multi-way ANOVA', 'Interaction F-tests', 'Simple effects analysis (if interaction significant)', 'Tukey HSD or Bonferroni post-hoc'],
1187
+ strengths: ['Tests interactions between factors', 'More efficient than one-factor-at-a-time', 'Can estimate all main effects and interactions'],
1188
+ limitations: ['Number of cells grows exponentially (2^k)', 'Hard to interpret high-order interactions', 'Requires larger total sample size'],
1189
+ ethicalConsiderations: ['Participants exposed to multiple manipulations', 'Combination effects may be unpredictable', 'Power analysis must account for interaction terms'],
1190
+ },
1191
+ observational: {
1192
+ fullName: 'Observational Study',
1193
+ description: 'Researcher observes without intervening. Includes cohort, case-control, and cross-sectional designs.',
1194
+ structure: '1. Define exposure and outcome variables\n2. Select study population and comparison group\n3. Collect data through surveys, records, or measurement\n4. Control for confounders via matching, stratification, or regression\n5. Analyze association strength (OR, RR, HR)',
1195
+ randomization: 'No randomization (observational). Instead use: propensity score matching, inverse probability weighting, or instrumental variables to reduce confounding.',
1196
+ sampleSizeFormula: 'Cohort: n = (Z_{alpha/2} + Z_{beta})^2 * [p1*(1-p1) + p2*(1-p2)] / (p1 - p2)^2\nCase-control: n_cases = (Z_{alpha/2}*sqrt(2*p_bar*(1-p_bar)) + Z_{beta}*sqrt(p1*(1-p1)+p2*(1-p2)))^2 / (p1-p2)^2\nWhere p1, p2 are expected proportions in exposed/unexposed',
1197
+ statisticalTests: ['Logistic regression (binary outcome)', 'Cox proportional hazards (time-to-event)', 'Propensity score matching', 'Mantel-Haenszel test (stratified)', 'Poisson regression (count data)'],
1198
+ strengths: ['Ethical when randomization impossible', 'Can study rare exposures or outcomes', 'Often cheaper and faster than RCTs', 'Better external validity'],
1199
+ limitations: ['Cannot establish causation', 'Susceptible to confounding', 'Selection bias and recall bias possible'],
1200
+ ethicalConsiderations: ['Privacy and data protection', 'Informed consent for surveys', 'Responsible reporting (correlation != causation)'],
1201
+ },
1202
+ crossover: {
1203
+ fullName: 'Crossover Design',
1204
+ description: 'Each participant receives all treatments in sequence, acting as their own control. Powerful for within-subject comparisons.',
1205
+ structure: '1. Randomize participants to treatment sequences (e.g., AB vs BA)\n2. Administer first treatment, measure outcome\n3. Washout period (sufficient to eliminate carryover)\n4. Administer second treatment, measure outcome\n5. Analyze within-subject differences',
1206
+ randomization: 'Randomize treatment ORDER (not assignment). For 2 treatments: equal allocation to AB and BA sequences. For k treatments: Latin square or Williams design.',
1207
+ sampleSizeFormula: 'n = (Z_{alpha/2} + Z_{beta})^2 * sigma_d^2 / delta^2\nWhere sigma_d = within-subject SD of paired differences\nNote: typically requires 50-75% fewer subjects than parallel design because within-subject variability < between-subject',
1208
+ statisticalTests: ['Paired t-test', 'Repeated measures ANOVA', 'Carryover effect test (sequence x period interaction)', 'Mixed-effects model with period and sequence terms'],
1209
+ strengths: ['Each participant is own control', 'Eliminates between-subject variability', 'Requires fewer participants', 'Cost-effective'],
1210
+ limitations: ['Requires adequate washout period', 'Carryover effects can bias results', 'Not suitable for irreversible outcomes', 'Dropouts lose all data'],
1211
+ ethicalConsiderations: ['Extended study duration for participants', 'Washout period risks (no treatment)', 'Must test for carryover before interpreting results'],
1212
+ },
1213
+ longitudinal: {
1214
+ fullName: 'Longitudinal Study',
1215
+ description: 'Follows the same subjects over time, measuring changes and trajectories. Ideal for studying development, progression, or long-term effects.',
1216
+ structure: '1. Define cohort and measurement schedule (waves)\n2. Baseline assessment of all variables\n3. Follow-up assessments at predetermined intervals\n4. Track attrition and handle missing data\n5. Model change over time (growth curves, trajectories)',
1217
+ randomization: 'No randomization (observational longitudinal). Sampling strategies: random sampling from population, stratified sampling, or purposive sampling for subgroups of interest.',
1218
+ sampleSizeFormula: 'n = (Z_{alpha/2} + Z_{beta})^2 * sigma^2 * [1 + (m-1)*rho] / (m * delta^2)\nWhere m = number of measurements, rho = intraclass correlation\nAdjust for expected attrition: n_adjusted = n / (1 - attrition_rate)^waves',
1219
+ statisticalTests: ['Mixed-effects / multilevel models', 'Growth curve modeling (latent growth)', 'Generalized estimating equations (GEE)', 'Survival analysis (time-to-event)', 'Autoregressive cross-lagged models'],
1220
+ strengths: ['Tracks individual change over time', 'Establishes temporal ordering', 'Can identify developmental trajectories', 'Powerful for mediation analysis'],
1221
+ limitations: ['Attrition can introduce bias', 'Expensive and time-consuming', 'Practice effects on repeated measures', 'Cohort effects may limit generalizability'],
1222
+ ethicalConsiderations: ['Long-term participant burden', 'Data security over extended periods', 'Right to withdraw at any point', 'Incidental findings protocols'],
1223
+ },
1224
+ };
1225
+ const design = designs[designType];
1226
+ if (!design) {
1227
+ return `**Error**: Unknown design type "${designType}". Supported types: ${Object.keys(designs).join(', ')}`;
1228
+ }
1229
+ const iv = variables.length > 0 ? variables[0] : '[Independent Variable]';
1230
+ const dv = variables.length > 1 ? variables[1] : '[Dependent Variable]';
1231
+ const covariates = variables.length > 2 ? variables.slice(2) : ['[Covariate 1]', '[Covariate 2]'];
1232
+ return [
1233
+ `# Experimental Design: ${design.fullName}`,
1234
+ '',
1235
+ `## Research Question`,
1236
+ `> ${question}`,
1237
+ '',
1238
+ `## Design Overview`,
1239
+ design.description,
1240
+ '',
1241
+ `## Variables`,
1242
+ `- **Independent Variable (IV)**: ${iv}`,
1243
+ `- **Dependent Variable (DV)**: ${dv}`,
1244
+ `- **Covariates/Controls**: ${covariates.join(', ')}`,
1245
+ `- **Confounders to Address**: Age, sex, baseline status, socioeconomic factors`,
1246
+ '',
1247
+ `## Study Structure`,
1248
+ design.structure,
1249
+ '',
1250
+ `## Randomization Scheme`,
1251
+ design.randomization,
1252
+ '',
1253
+ `## Sample Size Estimation`,
1254
+ '```',
1255
+ design.sampleSizeFormula,
1256
+ '```',
1257
+ '',
1258
+ `## Recommended Statistical Tests`,
1259
+ ...design.statisticalTests.map(t => `- ${t}`),
1260
+ '',
1261
+ `## Strengths`,
1262
+ ...design.strengths.map(s => `- ${s}`),
1263
+ '',
1264
+ `## Limitations`,
1265
+ ...design.limitations.map(l => `- ${l}`),
1266
+ '',
1267
+ `## Ethical Considerations`,
1268
+ ...design.ethicalConsiderations.map(e => `- ${e}`),
1269
+ '',
1270
+ `## Reporting Guidelines`,
1271
+ designType === 'rct' ? '- Follow CONSORT Statement (Consolidated Standards of Reporting Trials)' :
1272
+ designType === 'observational' ? '- Follow STROBE Statement (Strengthening the Reporting of Observational Studies)' :
1273
+ '- Follow EQUATOR Network guidelines appropriate to your design',
1274
+ `- Pre-register study protocol (ClinicalTrials.gov, OSF, AsPredicted)`,
1275
+ `- Report effect sizes with confidence intervals, not just p-values`,
1276
+ ].join('\n');
1277
+ },
1278
+ });
1279
+ // ── 2. Hypothesis Test ──────────────────────────────────────────────────
1280
+ registerTool({
1281
+ name: 'hypothesis_test',
1282
+ description: 'Run statistical hypothesis tests with real calculations. Supports: t-test (one-sample, two-sample, paired), chi-square, mann-whitney, wilcoxon, anova, kruskal-wallis. Returns test statistic, p-value, effect size, and interpretation.',
1283
+ parameters: {
1284
+ test_type: { type: 'string', description: 'Test type: t-test-one, t-test-two, t-test-paired, chi-square, mann-whitney, wilcoxon, anova, kruskal-wallis', required: true },
1285
+ data_a: { type: 'string', description: 'Comma-separated numbers for sample A (or observed counts for chi-square)', required: true },
1286
+ data_b: { type: 'string', description: 'Comma-separated numbers for sample B (or expected counts for chi-square)' },
1287
+ alpha: { type: 'number', description: 'Significance level (default: 0.05)' },
1288
+ },
1289
+ tier: 'free',
1290
+ async execute(args) {
1291
+ const testType = String(args.test_type).toLowerCase().trim();
1292
+ const a = parseNumbers(String(args.data_a));
1293
+ const b = args.data_b ? parseNumbers(String(args.data_b)) : [];
1294
+ const alpha = typeof args.alpha === 'number' ? args.alpha : 0.05;
1295
+ if (a.length < 2)
1296
+ return '**Error**: Sample A must have at least 2 values.';
1297
+ const lines = [];
1298
+ switch (testType) {
1299
+ case 't-test-one':
1300
+ case 'one-sample-t':
1301
+ case 't-one': {
1302
+ const mu0 = b.length > 0 ? b[0] : 0;
1303
+ const n = a.length;
1304
+ const m = mean(a);
1305
+ const se = stddev(a) / Math.sqrt(n);
1306
+ const t = (m - mu0) / se;
1307
+ const df = n - 1;
1308
+ const p = tTestPValue(t, df);
1309
+ const d = (m - mu0) / stddev(a); // Cohen's d
1310
+ lines.push(`# One-Sample t-Test`, '', `## Hypotheses`, `- H0: \u03BC = ${mu0}`, `- H1: \u03BC \u2260 ${mu0}`, '', `## Sample Statistics`, `- n = ${n}`, `- Mean = ${m.toFixed(6)}`, `- SD = ${stddev(a).toFixed(6)}`, `- SE = ${se.toFixed(6)}`, '', `## Test Results`, `- t-statistic = ${t.toFixed(6)}`, `- Degrees of freedom = ${df}`, `- p-value (two-tailed) = ${p.toFixed(8)}`, '', `## Effect Size`, `- Cohen's d = ${d.toFixed(4)} (${Math.abs(d) < 0.2 ? 'negligible' : Math.abs(d) < 0.5 ? 'small' : Math.abs(d) < 0.8 ? 'medium' : 'large'})`, '', `## Decision`, `- At \u03B1 = ${alpha}: **${p < alpha ? 'Reject H0' : 'Fail to reject H0'}**`, p < alpha
1311
+ ? `- There is statistically significant evidence that the population mean differs from ${mu0}.`
1312
+ : `- There is insufficient evidence to conclude the population mean differs from ${mu0}.`);
1313
+ break;
1314
+ }
1315
+ case 't-test-two':
1316
+ case 'two-sample-t':
1317
+ case 't-two':
1318
+ case 'independent-t': {
1319
+ if (b.length < 2)
1320
+ return '**Error**: Sample B must have at least 2 values for two-sample t-test.';
1321
+ const n1 = a.length, n2 = b.length;
1322
+ const m1 = mean(a), m2 = mean(b);
1323
+ const v1 = variance(a), v2 = variance(b);
1324
+ // Welch's t-test (does not assume equal variances)
1325
+ const se = Math.sqrt(v1 / n1 + v2 / n2);
1326
+ const t = (m1 - m2) / se;
1327
+ // Welch-Satterthwaite degrees of freedom
1328
+ const dfNum = (v1 / n1 + v2 / n2) ** 2;
1329
+ const dfDen = (v1 / n1) ** 2 / (n1 - 1) + (v2 / n2) ** 2 / (n2 - 1);
1330
+ const df = dfNum / dfDen;
1331
+ const p = tTestPValue(t, df);
1332
+ // Pooled SD for Cohen's d
1333
+ const sp = Math.sqrt(((n1 - 1) * v1 + (n2 - 1) * v2) / (n1 + n2 - 2));
1334
+ const d = (m1 - m2) / sp;
1335
+ lines.push(`# Two-Sample t-Test (Welch's)`, '', `## Hypotheses`, `- H0: \u03BC1 = \u03BC2`, `- H1: \u03BC1 \u2260 \u03BC2`, '', `## Sample Statistics`, `| | Sample A | Sample B |`, `|---|---|---|`, `| n | ${n1} | ${n2} |`, `| Mean | ${m1.toFixed(6)} | ${m2.toFixed(6)} |`, `| SD | ${Math.sqrt(v1).toFixed(6)} | ${Math.sqrt(v2).toFixed(6)} |`, `| Variance | ${v1.toFixed(6)} | ${v2.toFixed(6)} |`, '', `## Test Results`, `- t-statistic = ${t.toFixed(6)}`, `- Degrees of freedom (Welch-Satterthwaite) = ${df.toFixed(2)}`, `- p-value (two-tailed) = ${p.toFixed(8)}`, '', `## Effect Size`, `- Cohen's d = ${d.toFixed(4)} (${Math.abs(d) < 0.2 ? 'negligible' : Math.abs(d) < 0.5 ? 'small' : Math.abs(d) < 0.8 ? 'medium' : 'large'})`, `- Mean difference = ${(m1 - m2).toFixed(6)}`, '', `## Decision`, `- At \u03B1 = ${alpha}: **${p < alpha ? 'Reject H0' : 'Fail to reject H0'}**`);
1336
+ break;
1337
+ }
1338
+ case 't-test-paired':
1339
+ case 'paired-t':
1340
+ case 't-paired': {
1341
+ if (b.length !== a.length)
1342
+ return `**Error**: Paired t-test requires equal sample sizes. A has ${a.length}, B has ${b.length}.`;
1343
+ const diffs = a.map((v, i) => v - b[i]);
1344
+ const n = diffs.length;
1345
+ const md = mean(diffs);
1346
+ const sd = stddev(diffs);
1347
+ const se = sd / Math.sqrt(n);
1348
+ const t = md / se;
1349
+ const df = n - 1;
1350
+ const p = tTestPValue(t, df);
1351
+ const d = md / sd; // Cohen's d for paired
1352
+ lines.push(`# Paired t-Test`, '', `## Hypotheses`, `- H0: \u03BC_d = 0 (no difference)`, `- H1: \u03BC_d \u2260 0`, '', `## Difference Statistics`, `- n pairs = ${n}`, `- Mean difference = ${md.toFixed(6)}`, `- SD of differences = ${sd.toFixed(6)}`, `- SE = ${se.toFixed(6)}`, '', `## Test Results`, `- t-statistic = ${t.toFixed(6)}`, `- Degrees of freedom = ${df}`, `- p-value (two-tailed) = ${p.toFixed(8)}`, '', `## Effect Size`, `- Cohen's d (paired) = ${d.toFixed(4)} (${Math.abs(d) < 0.2 ? 'negligible' : Math.abs(d) < 0.5 ? 'small' : Math.abs(d) < 0.8 ? 'medium' : 'large'})`, '', `## Decision`, `- At \u03B1 = ${alpha}: **${p < alpha ? 'Reject H0' : 'Fail to reject H0'}**`);
1353
+ break;
1354
+ }
1355
+ case 'chi-square':
1356
+ case 'chi2':
1357
+ case 'chisquare': {
1358
+ // observed = a, expected = b (or uniform if not provided)
1359
+ const observed = a;
1360
+ const expected = b.length === a.length ? b : a.map(() => mean(a));
1361
+ const k = observed.length;
1362
+ let chiSq = 0;
1363
+ for (let i = 0; i < k; i++) {
1364
+ chiSq += (observed[i] - expected[i]) ** 2 / expected[i];
1365
+ }
1366
+ const df = k - 1;
1367
+ const p = 1 - chiSquareCdf(chiSq, df);
1368
+ // Cramer's V (for goodness of fit, use phi)
1369
+ const n = observed.reduce((s, v) => s + v, 0);
1370
+ const phi = Math.sqrt(chiSq / n);
1371
+ lines.push(`# Chi-Square Goodness-of-Fit Test`, '', `## Hypotheses`, `- H0: Observed frequencies match expected frequencies`, `- H1: Observed frequencies differ from expected frequencies`, '', `## Data`, `| Category | Observed | Expected |`, `|---|---|---|`, ...observed.map((o, i) => `| ${i + 1} | ${o} | ${expected[i].toFixed(2)} |`), '', `## Test Results`, `- \u03C7\u00B2 statistic = ${chiSq.toFixed(6)}`, `- Degrees of freedom = ${df}`, `- p-value = ${p.toFixed(8)}`, '', `## Effect Size`, `- Phi (\u03C6) = ${phi.toFixed(4)}`, '', `## Decision`, `- At \u03B1 = ${alpha}: **${p < alpha ? 'Reject H0' : 'Fail to reject H0'}**`);
1372
+ break;
1373
+ }
1374
+ case 'mann-whitney':
1375
+ case 'mann-whitney-u':
1376
+ case 'mannwhitney': {
1377
+ if (b.length < 2)
1378
+ return '**Error**: Sample B must have at least 2 values for Mann-Whitney U test.';
1379
+ const n1 = a.length, n2 = b.length;
1380
+ // Combine and rank
1381
+ const combined = [
1382
+ ...a.map(v => ({ v, group: 'a' })),
1383
+ ...b.map(v => ({ v, group: 'b' })),
1384
+ ].sort((x, y) => x.v - y.v);
1385
+ // Assign ranks with tie handling
1386
+ const ranks = new Array(combined.length);
1387
+ let i = 0;
1388
+ while (i < combined.length) {
1389
+ let j = i;
1390
+ while (j < combined.length && combined[j].v === combined[i].v)
1391
+ j++;
1392
+ const avgRank = (i + 1 + j) / 2;
1393
+ for (let k = i; k < j; k++)
1394
+ ranks[k] = avgRank;
1395
+ i = j;
1396
+ }
1397
+ let R1 = 0;
1398
+ for (let k = 0; k < combined.length; k++) {
1399
+ if (combined[k].group === 'a')
1400
+ R1 += ranks[k];
1401
+ }
1402
+ const U1 = R1 - n1 * (n1 + 1) / 2;
1403
+ const U2 = n1 * n2 - U1;
1404
+ const U = Math.min(U1, U2);
1405
+ // Normal approximation for p-value
1406
+ const muU = n1 * n2 / 2;
1407
+ const sigmaU = Math.sqrt(n1 * n2 * (n1 + n2 + 1) / 12);
1408
+ const z = (U - muU) / sigmaU;
1409
+ const p = 2 * normalCdf(z); // two-tailed (z is typically negative for min U)
1410
+ // Rank-biserial correlation
1411
+ const rbc = 1 - (2 * U) / (n1 * n2);
1412
+ lines.push(`# Mann-Whitney U Test`, '', `## Hypotheses`, `- H0: The two populations have the same distribution`, `- H1: The two populations differ in location`, '', `## Sample Statistics`, `- n1 = ${n1}, n2 = ${n2}`, `- Median A = ${[...a].sort((x, y) => x - y)[Math.floor(n1 / 2)].toFixed(4)}`, `- Median B = ${[...b].sort((x, y) => x - y)[Math.floor(n2 / 2)].toFixed(4)}`, `- Rank sum (A) = ${R1.toFixed(1)}`, '', `## Test Results`, `- U1 = ${U1.toFixed(1)}, U2 = ${U2.toFixed(1)}`, `- U (min) = ${U.toFixed(1)}`, `- z-approximation = ${z.toFixed(6)}`, `- p-value (two-tailed) = ${p.toFixed(8)}`, n1 <= 20 || n2 <= 20 ? `- *Note: Normal approximation used. For small samples (n <= 20), consider exact tables.*` : '', '', `## Effect Size`, `- Rank-biserial correlation = ${rbc.toFixed(4)} (${Math.abs(rbc) < 0.1 ? 'negligible' : Math.abs(rbc) < 0.3 ? 'small' : Math.abs(rbc) < 0.5 ? 'medium' : 'large'})`, '', `## Decision`, `- At \u03B1 = ${alpha}: **${p < alpha ? 'Reject H0' : 'Fail to reject H0'}**`);
1413
+ break;
1414
+ }
1415
+ case 'wilcoxon':
1416
+ case 'wilcoxon-signed-rank':
1417
+ case 'signed-rank': {
1418
+ if (b.length > 0 && b.length !== a.length)
1419
+ return `**Error**: Wilcoxon signed-rank test requires equal sample sizes or single-sample vs zero. A=${a.length}, B=${b.length}.`;
1420
+ const diffs = b.length === a.length ? a.map((v, i) => v - b[i]) : a;
1421
+ const nonZero = diffs.filter(d => d !== 0);
1422
+ const n = nonZero.length;
1423
+ if (n < 2)
1424
+ return '**Error**: Need at least 2 non-zero differences.';
1425
+ // Rank absolute differences
1426
+ const ranked = nonZero.map(d => ({ d, abs: Math.abs(d) })).sort((x, y) => x.abs - y.abs);
1427
+ const ranksArr = new Array(n);
1428
+ let ri = 0;
1429
+ while (ri < n) {
1430
+ let rj = ri;
1431
+ while (rj < n && ranked[rj].abs === ranked[ri].abs)
1432
+ rj++;
1433
+ const avg = (ri + 1 + rj) / 2;
1434
+ for (let rk = ri; rk < rj; rk++)
1435
+ ranksArr[rk] = avg;
1436
+ ri = rj;
1437
+ }
1438
+ let Wplus = 0, Wminus = 0;
1439
+ for (let k = 0; k < n; k++) {
1440
+ if (ranked[k].d > 0)
1441
+ Wplus += ranksArr[k];
1442
+ else
1443
+ Wminus += ranksArr[k];
1444
+ }
1445
+ const W = Math.min(Wplus, Wminus);
1446
+ // Normal approximation
1447
+ const muW = n * (n + 1) / 4;
1448
+ const sigmaW = Math.sqrt(n * (n + 1) * (2 * n + 1) / 24);
1449
+ const z = (W - muW) / sigmaW;
1450
+ const p = 2 * normalCdf(z);
1451
+ // Effect size: r = z / sqrt(n)
1452
+ const r = z / Math.sqrt(n);
1453
+ lines.push(`# Wilcoxon Signed-Rank Test`, '', `## Hypotheses`, `- H0: Median difference = 0`, `- H1: Median difference \u2260 0`, '', `## Statistics`, `- n (non-zero differences) = ${n}`, `- W+ = ${Wplus.toFixed(1)}, W- = ${Wminus.toFixed(1)}`, `- W (test statistic) = ${W.toFixed(1)}`, '', `## Test Results`, `- z-approximation = ${z.toFixed(6)}`, `- p-value (two-tailed) = ${p.toFixed(8)}`, '', `## Effect Size`, `- r = ${r.toFixed(4)} (${Math.abs(r) < 0.1 ? 'negligible' : Math.abs(r) < 0.3 ? 'small' : Math.abs(r) < 0.5 ? 'medium' : 'large'})`, '', `## Decision`, `- At \u03B1 = ${alpha}: **${p < alpha ? 'Reject H0' : 'Fail to reject H0'}**`);
1454
+ break;
1455
+ }
1456
+ case 'anova':
1457
+ case 'one-way-anova': {
1458
+ // data_a is first group, data_b is remaining groups separated by semicolons
1459
+ // Or: treat data_a as semicolon-separated groups if data_b is empty
1460
+ let groups;
1461
+ if (b.length > 0) {
1462
+ groups = [a, b];
1463
+ }
1464
+ else {
1465
+ // Try semicolons in original string
1466
+ const rawA = String(args.data_a);
1467
+ if (rawA.includes(';')) {
1468
+ groups = rawA.split(';').map(g => parseNumbers(g));
1469
+ }
1470
+ else {
1471
+ return '**Error**: ANOVA requires 2+ groups. Separate groups with semicolons in data_a, or provide data_b for a second group.';
1472
+ }
1473
+ }
1474
+ groups = groups.filter(g => g.length > 0);
1475
+ if (groups.length < 2)
1476
+ return '**Error**: ANOVA requires at least 2 groups.';
1477
+ const k = groups.length;
1478
+ const N = groups.reduce((s, g) => s + g.length, 0);
1479
+ const grandMean = groups.reduce((s, g) => s + g.reduce((a, b) => a + b, 0), 0) / N;
1480
+ // Between-group sum of squares
1481
+ let SSB = 0;
1482
+ for (const g of groups) {
1483
+ const gm = mean(g);
1484
+ SSB += g.length * (gm - grandMean) ** 2;
1485
+ }
1486
+ // Within-group sum of squares
1487
+ let SSW = 0;
1488
+ for (const g of groups) {
1489
+ const gm = mean(g);
1490
+ for (const v of g)
1491
+ SSW += (v - gm) ** 2;
1492
+ }
1493
+ const dfB = k - 1;
1494
+ const dfW = N - k;
1495
+ const MSB = SSB / dfB;
1496
+ const MSW = SSW / dfW;
1497
+ const F = MSB / MSW;
1498
+ const p = 1 - gammaPLower(dfB / 2, (dfB * F / (dfB * F + dfW)) * dfB / 2);
1499
+ // Better: use beta distribution for F-test p-value
1500
+ const x = dfW / (dfW + dfB * F);
1501
+ const pBeta = betaIncomplete(x, dfW / 2, dfB / 2);
1502
+ // Eta-squared
1503
+ const etaSq = SSB / (SSB + SSW);
1504
+ lines.push(`# One-Way ANOVA`, '', `## Hypotheses`, `- H0: All group means are equal (\u03BC1 = \u03BC2 = ... = \u03BCk)`, `- H1: At least one group mean differs`, '', `## Group Statistics`, `| Group | n | Mean | SD |`, `|---|---|---|---|`, ...groups.map((g, i) => `| ${i + 1} | ${g.length} | ${mean(g).toFixed(4)} | ${stddev(g).toFixed(4)} |`), '', `## ANOVA Table`, `| Source | SS | df | MS | F |`, `|---|---|---|---|---|`, `| Between | ${SSB.toFixed(4)} | ${dfB} | ${MSB.toFixed(4)} | ${F.toFixed(4)} |`, `| Within | ${SSW.toFixed(4)} | ${dfW} | ${MSW.toFixed(4)} | |`, `| Total | ${(SSB + SSW).toFixed(4)} | ${N - 1} | | |`, '', `## Test Results`, `- F(${dfB}, ${dfW}) = ${F.toFixed(6)}`, `- p-value = ${pBeta.toFixed(8)}`, '', `## Effect Size`, `- Eta-squared (\u03B7\u00B2) = ${etaSq.toFixed(4)} (${etaSq < 0.01 ? 'negligible' : etaSq < 0.06 ? 'small' : etaSq < 0.14 ? 'medium' : 'large'})`, '', `## Decision`, `- At \u03B1 = ${alpha}: **${pBeta < alpha ? 'Reject H0' : 'Fail to reject H0'}**`, pBeta < alpha ? '- *Consider post-hoc pairwise comparisons (Tukey HSD, Bonferroni)*' : '');
1505
+ break;
1506
+ }
1507
+ case 'kruskal-wallis':
1508
+ case 'kruskal': {
1509
+ let groups;
1510
+ if (b.length > 0) {
1511
+ groups = [a, b];
1512
+ }
1513
+ else {
1514
+ const rawA = String(args.data_a);
1515
+ if (rawA.includes(';')) {
1516
+ groups = rawA.split(';').map(g => parseNumbers(g));
1517
+ }
1518
+ else {
1519
+ return '**Error**: Kruskal-Wallis requires 2+ groups. Separate groups with semicolons in data_a.';
1520
+ }
1521
+ }
1522
+ groups = groups.filter(g => g.length > 0);
1523
+ if (groups.length < 2)
1524
+ return '**Error**: Kruskal-Wallis requires at least 2 groups.';
1525
+ const k = groups.length;
1526
+ const N = groups.reduce((s, g) => s + g.length, 0);
1527
+ // Combine and rank
1528
+ const combined = [];
1529
+ for (let gi = 0; gi < groups.length; gi++) {
1530
+ for (const v of groups[gi])
1531
+ combined.push({ v, group: gi });
1532
+ }
1533
+ combined.sort((x, y) => x.v - y.v);
1534
+ const ranksArr = new Array(combined.length);
1535
+ let ci = 0;
1536
+ while (ci < combined.length) {
1537
+ let cj = ci;
1538
+ while (cj < combined.length && combined[cj].v === combined[ci].v)
1539
+ cj++;
1540
+ const avg = (ci + 1 + cj) / 2;
1541
+ for (let ck = ci; ck < cj; ck++)
1542
+ ranksArr[ck] = avg;
1543
+ ci = cj;
1544
+ }
1545
+ // Sum of ranks per group
1546
+ const R = new Array(k).fill(0);
1547
+ for (let idx = 0; idx < combined.length; idx++) {
1548
+ R[combined[idx].group] += ranksArr[idx];
1549
+ }
1550
+ // H statistic
1551
+ let H = 0;
1552
+ for (let gi = 0; gi < k; gi++) {
1553
+ const ni = groups[gi].length;
1554
+ H += (R[gi] ** 2) / ni;
1555
+ }
1556
+ H = (12 / (N * (N + 1))) * H - 3 * (N + 1);
1557
+ const df = k - 1;
1558
+ const p = 1 - chiSquareCdf(H, df);
1559
+ // Epsilon-squared
1560
+ const epsSq = H / ((N * N - 1) / (N + 1));
1561
+ lines.push(`# Kruskal-Wallis H Test`, '', `## Hypotheses`, `- H0: All groups come from the same distribution`, `- H1: At least one group differs`, '', `## Group Statistics`, `| Group | n | Median | Mean Rank |`, `|---|---|---|---|`, ...groups.map((g, i) => {
1562
+ const sorted = [...g].sort((x, y) => x - y);
1563
+ const median = sorted.length % 2 === 0
1564
+ ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
1565
+ : sorted[Math.floor(sorted.length / 2)];
1566
+ return `| ${i + 1} | ${g.length} | ${median.toFixed(4)} | ${(R[i] / g.length).toFixed(2)} |`;
1567
+ }), '', `## Test Results`, `- H statistic = ${H.toFixed(6)}`, `- Degrees of freedom = ${df}`, `- p-value = ${p.toFixed(8)}`, '', `## Effect Size`, `- Epsilon-squared (\u03B5\u00B2) = ${epsSq.toFixed(4)} (${epsSq < 0.01 ? 'negligible' : epsSq < 0.06 ? 'small' : epsSq < 0.14 ? 'medium' : 'large'})`, '', `## Decision`, `- At \u03B1 = ${alpha}: **${p < alpha ? 'Reject H0' : 'Fail to reject H0'}**`, p < alpha ? '- *Consider post-hoc pairwise comparisons (Dunn test)*' : '');
1568
+ break;
1569
+ }
1570
+ default:
1571
+ return `**Error**: Unknown test type "${testType}". Supported: t-test-one, t-test-two, t-test-paired, chi-square, mann-whitney, wilcoxon, anova, kruskal-wallis`;
1572
+ }
1573
+ return lines.filter(l => l !== undefined).join('\n');
1574
+ },
1575
+ });
1576
+ // ── 3. Literature Search ────────────────────────────────────────────────
1577
+ registerTool({
1578
+ name: 'literature_search',
1579
+ description: 'Cross-database academic search via OpenAlex + CrossRef APIs simultaneously. Returns title, authors, year, journal, DOI, and citation count. Results are deduplicated by DOI.',
1580
+ parameters: {
1581
+ query: { type: 'string', description: 'Search query for academic literature', required: true },
1582
+ year_from: { type: 'number', description: 'Filter papers from this year onward (optional)' },
1583
+ year_to: { type: 'number', description: 'Filter papers up to this year (optional)' },
1584
+ limit: { type: 'number', description: 'Maximum number of results (default: 10)' },
1585
+ },
1586
+ tier: 'free',
1587
+ async execute(args) {
1588
+ const query = String(args.query);
1589
+ const yearFrom = typeof args.year_from === 'number' ? args.year_from : undefined;
1590
+ const yearTo = typeof args.year_to === 'number' ? args.year_to : undefined;
1591
+ const limit = typeof args.limit === 'number' ? Math.min(args.limit, 50) : 10;
1592
+ const papers = [];
1593
+ const seenDois = new Set();
1594
+ // Fetch from OpenAlex and CrossRef in parallel
1595
+ const [openAlexResult, crossRefResult] = await Promise.allSettled([
1596
+ // OpenAlex
1597
+ (async () => {
1598
+ const params = new URLSearchParams({ search: query, per_page: String(limit) });
1599
+ if (yearFrom || yearTo) {
1600
+ const filter = [];
1601
+ if (yearFrom)
1602
+ filter.push(`from_publication_date:${yearFrom}-01-01`);
1603
+ if (yearTo)
1604
+ filter.push(`to_publication_date:${yearTo}-12-31`);
1605
+ params.set('filter', filter.join(','));
1606
+ }
1607
+ const url = `https://api.openalex.org/works?${params.toString()}`;
1608
+ const data = await labFetchJson(url);
1609
+ if (data.results) {
1610
+ for (const r of data.results) {
1611
+ const doi = r.doi ? r.doi.replace('https://doi.org/', '') : '';
1612
+ if (doi && seenDois.has(doi.toLowerCase()))
1613
+ continue;
1614
+ if (doi)
1615
+ seenDois.add(doi.toLowerCase());
1616
+ papers.push({
1617
+ title: r.title || 'Untitled',
1618
+ authors: r.authorships?.map(a => a.author?.display_name || 'Unknown').slice(0, 5) || [],
1619
+ year: r.publication_year || null,
1620
+ journal: r.primary_location?.source?.display_name || 'Unknown',
1621
+ doi,
1622
+ citations: r.cited_by_count || 0,
1623
+ source: 'OpenAlex',
1624
+ });
1625
+ }
1626
+ }
1627
+ })(),
1628
+ // CrossRef
1629
+ (async () => {
1630
+ const params = new URLSearchParams({ query, rows: String(limit) });
1631
+ if (yearFrom)
1632
+ params.set('filter', `from-pub-date:${yearFrom}`);
1633
+ const url = `https://api.crossref.org/works?${params.toString()}`;
1634
+ const data = await labFetchJson(url);
1635
+ if (data.message?.items) {
1636
+ for (const r of data.message.items) {
1637
+ const doi = r.DOI || '';
1638
+ if (doi && seenDois.has(doi.toLowerCase()))
1639
+ continue;
1640
+ if (doi)
1641
+ seenDois.add(doi.toLowerCase());
1642
+ const year = r.published?.['date-parts']?.[0]?.[0] || null;
1643
+ if (yearTo && year && year > yearTo)
1644
+ continue;
1645
+ papers.push({
1646
+ title: r.title?.[0] || 'Untitled',
1647
+ authors: r.author?.map(a => `${a.given || ''} ${a.family || ''}`.trim()).slice(0, 5) || [],
1648
+ year,
1649
+ journal: r['container-title']?.[0] || 'Unknown',
1650
+ doi,
1651
+ citations: r['is-referenced-by-count'] || 0,
1652
+ source: 'CrossRef',
1653
+ });
1654
+ }
1655
+ }
1656
+ })(),
1657
+ ]);
1658
+ // Sort by citation count descending
1659
+ papers.sort((a, b) => b.citations - a.citations);
1660
+ const results = papers.slice(0, limit);
1661
+ if (results.length === 0) {
1662
+ const errors = [];
1663
+ if (openAlexResult.status === 'rejected')
1664
+ errors.push(`OpenAlex: ${openAlexResult.reason}`);
1665
+ if (crossRefResult.status === 'rejected')
1666
+ errors.push(`CrossRef: ${crossRefResult.reason}`);
1667
+ return errors.length > 0
1668
+ ? `**No results found.** API errors:\n${errors.join('\n')}`
1669
+ : `**No results found** for "${query}". Try broader search terms.`;
1670
+ }
1671
+ const lines = [
1672
+ `# Literature Search Results`,
1673
+ `**Query**: "${query}"${yearFrom ? ` | From: ${yearFrom}` : ''}${yearTo ? ` | To: ${yearTo}` : ''}`,
1674
+ `**Results**: ${results.length} papers (deduplicated across OpenAlex + CrossRef)`,
1675
+ '',
1676
+ ];
1677
+ for (let i = 0; i < results.length; i++) {
1678
+ const p = results[i];
1679
+ lines.push(`### ${i + 1}. ${p.title}`, `- **Authors**: ${p.authors.length > 0 ? p.authors.join(', ') : 'Unknown'}${p.authors.length >= 5 ? ' et al.' : ''}`, `- **Year**: ${p.year || 'N/A'} | **Journal**: ${p.journal}`, `- **DOI**: ${p.doi ? `[${p.doi}](https://doi.org/${p.doi})` : 'N/A'}`, `- **Citations**: ${p.citations} | **Source**: ${p.source}`, '');
1680
+ }
1681
+ return lines.join('\n');
1682
+ },
1683
+ });
1684
+ // ── 4. Citation Graph ───────────────────────────────────────────────────
1685
+ registerTool({
1686
+ name: 'citation_graph',
1687
+ description: 'Map the citation network for a paper. Shows who cites it, what it cites, and identifies bridge papers. Uses Semantic Scholar API.',
1688
+ parameters: {
1689
+ paper_id: { type: 'string', description: 'DOI (e.g. 10.1234/...) or Semantic Scholar paper ID', required: true },
1690
+ depth: { type: 'number', description: 'Citation depth to explore (default: 1, max: 2)' },
1691
+ },
1692
+ tier: 'free',
1693
+ async execute(args) {
1694
+ const paperId = String(args.paper_id);
1695
+ const depth = Math.min(typeof args.depth === 'number' ? args.depth : 1, 2);
1696
+ const id = paperId.startsWith('10.') ? `DOI:${paperId}` : paperId;
1697
+ const baseUrl = 'https://api.semanticscholar.org/graph/v1';
1698
+ try {
1699
+ // Fetch paper details, citations, and references in parallel
1700
+ const [paperRes, citationsRes, referencesRes] = await Promise.allSettled([
1701
+ labFetchJson(`${baseUrl}/paper/${encodeURIComponent(id)}?fields=title,year,authors,citationCount,referenceCount,venue,externalIds`),
1702
+ labFetchJson(`${baseUrl}/paper/${encodeURIComponent(id)}/citations?fields=title,year,citationCount,authors,venue&limit=50`),
1703
+ labFetchJson(`${baseUrl}/paper/${encodeURIComponent(id)}/references?fields=title,year,citationCount,authors,venue&limit=50`),
1704
+ ]);
1705
+ const lines = [];
1706
+ // Paper details
1707
+ if (paperRes.status === 'fulfilled') {
1708
+ const p = paperRes.value;
1709
+ lines.push(`# Citation Graph`, '', `## Source Paper`, `- **Title**: ${p.title || 'Unknown'}`, `- **Year**: ${p.year || 'N/A'}`, `- **Authors**: ${p.authors?.map(a => a.name).slice(0, 5).join(', ') || 'Unknown'}`, `- **Venue**: ${p.venue || 'N/A'}`, `- **DOI**: ${p.externalIds?.DOI || 'N/A'}`, `- **Total citations**: ${p.citationCount || 0}`, `- **Total references**: ${p.referenceCount || 0}`, '');
1710
+ }
1711
+ else {
1712
+ return `**Error**: Could not find paper "${paperId}". Check the DOI or Semantic Scholar ID. Error: ${paperRes.reason}`;
1713
+ }
1714
+ // Citations (papers that cite this one)
1715
+ if (citationsRes.status === 'fulfilled') {
1716
+ const data = citationsRes.value;
1717
+ const citing = data.data || [];
1718
+ lines.push(`## Citing Papers (${citing.length} shown)`);
1719
+ if (citing.length === 0) {
1720
+ lines.push('*No citing papers found.*', '');
1721
+ }
1722
+ else {
1723
+ // Sort by citation count
1724
+ const sorted = [...citing].sort((a, b) => (b.citingPaper.citationCount || 0) - (a.citingPaper.citationCount || 0));
1725
+ for (const c of sorted.slice(0, 20)) {
1726
+ const cp = c.citingPaper;
1727
+ lines.push(`- **${cp.title || 'Untitled'}** (${cp.year || 'N/A'}) — ${cp.citationCount || 0} citations — ${cp.authors?.slice(0, 3).map(a => a.name).join(', ') || 'Unknown'}`);
1728
+ }
1729
+ if (sorted.length > 20)
1730
+ lines.push(`- *...and ${sorted.length - 20} more*`);
1731
+ lines.push('');
1732
+ // Identify bridge papers (highly cited papers that cite this one — influential connectors)
1733
+ const bridges = sorted.filter(c => (c.citingPaper.citationCount || 0) > 100).slice(0, 5);
1734
+ if (bridges.length > 0) {
1735
+ lines.push(`## Bridge Papers (highly cited papers that cite this one)`);
1736
+ for (const b of bridges) {
1737
+ lines.push(`- **${b.citingPaper.title}** (${b.citingPaper.year || 'N/A'}) — ${b.citingPaper.citationCount} citations`);
1738
+ }
1739
+ lines.push('');
1740
+ }
1741
+ }
1742
+ }
1743
+ // References (papers this one cites)
1744
+ if (referencesRes.status === 'fulfilled') {
1745
+ const data = referencesRes.value;
1746
+ const refs = data.data || [];
1747
+ lines.push(`## References (${refs.length} shown)`);
1748
+ if (refs.length === 0) {
1749
+ lines.push('*No references found.*', '');
1750
+ }
1751
+ else {
1752
+ const sorted = [...refs].sort((a, b) => (b.citedPaper.citationCount || 0) - (a.citedPaper.citationCount || 0));
1753
+ for (const r of sorted.slice(0, 20)) {
1754
+ const rp = r.citedPaper;
1755
+ if (!rp.title)
1756
+ continue;
1757
+ lines.push(`- **${rp.title}** (${rp.year || 'N/A'}) — ${rp.citationCount || 0} citations — ${rp.authors?.slice(0, 3).map(a => a.name).join(', ') || 'Unknown'}`);
1758
+ }
1759
+ if (sorted.length > 20)
1760
+ lines.push(`- *...and ${sorted.length - 20} more*`);
1761
+ lines.push('');
1762
+ // Key foundational papers (most-cited references)
1763
+ const foundational = sorted.filter(r => r.citedPaper.title && (r.citedPaper.citationCount || 0) > 500).slice(0, 5);
1764
+ if (foundational.length > 0) {
1765
+ lines.push(`## Foundational Papers (most-cited references)`);
1766
+ for (const f of foundational) {
1767
+ lines.push(`- **${f.citedPaper.title}** (${f.citedPaper.year || 'N/A'}) — ${f.citedPaper.citationCount} citations`);
1768
+ }
1769
+ lines.push('');
1770
+ }
1771
+ }
1772
+ }
1773
+ // Depth 2: fetch citations-of-citations for the top 3 citing papers
1774
+ if (depth >= 2 && citationsRes.status === 'fulfilled') {
1775
+ const data = citationsRes.value;
1776
+ const topCiting = (data.data || [])
1777
+ .filter(c => c.citingPaper.paperId)
1778
+ .sort((a, b) => (b.citingPaper.citationCount || 0) - (a.citingPaper.citationCount || 0))
1779
+ .slice(0, 3);
1780
+ if (topCiting.length > 0) {
1781
+ lines.push(`## Depth-2: Citations of Top Citing Papers`);
1782
+ const d2Results = await Promise.allSettled(topCiting.map(c => labFetchJson(`${baseUrl}/paper/${c.citingPaper.paperId}/citations?fields=title,year,citationCount&limit=5`)));
1783
+ for (let i = 0; i < topCiting.length; i++) {
1784
+ lines.push(`\n### Citations of "${topCiting[i].citingPaper.title}"`);
1785
+ const d2Res = d2Results[i];
1786
+ if (d2Res.status === 'fulfilled') {
1787
+ const d2 = d2Res.value;
1788
+ for (const c of (d2.data || []).slice(0, 5)) {
1789
+ if (c.citingPaper.title) {
1790
+ lines.push(`- ${c.citingPaper.title} (${c.citingPaper.year || 'N/A'}) — ${c.citingPaper.citationCount || 0} citations`);
1791
+ }
1792
+ }
1793
+ }
1794
+ else {
1795
+ lines.push('- *Could not fetch depth-2 citations*');
1796
+ }
1797
+ }
1798
+ lines.push('');
1799
+ }
1800
+ }
1801
+ return lines.join('\n');
1802
+ }
1803
+ catch (err) {
1804
+ return `**Error**: Failed to fetch citation data. ${err instanceof Error ? err.message : String(err)}`;
1805
+ }
1806
+ },
1807
+ });
1808
+ // ── 5. Unit Convert ─────────────────────────────────────────────────────
1809
+ registerTool({
1810
+ name: 'unit_convert',
1811
+ description: 'Convert between scientific units across all domains: length, mass, time, energy, pressure, temperature, force, power, frequency, electric, magnetic, radiation, concentration, data, speed, density, volume, area, angle, and more. ~200+ conversion pairs.',
1812
+ parameters: {
1813
+ value: { type: 'number', description: 'Numeric value to convert', required: true },
1814
+ from_unit: { type: 'string', description: 'Source unit (e.g. km, eV, atm, C, m/s, kg/m3)', required: true },
1815
+ to_unit: { type: 'string', description: 'Target unit (e.g. mi, J, Pa, F, km/h, g/cm3)', required: true },
1816
+ },
1817
+ tier: 'free',
1818
+ async execute(args) {
1819
+ const value = Number(args.value);
1820
+ const fromUnit = String(args.from_unit).trim();
1821
+ const toUnit = String(args.to_unit).trim();
1822
+ if (isNaN(value))
1823
+ return '**Error**: Value must be a number.';
1824
+ // Check temperature first (special handling)
1825
+ const tempResult = convertTemperature(value, fromUnit, toUnit);
1826
+ if (tempResult !== null) {
1827
+ return [
1828
+ `# Unit Conversion`,
1829
+ '',
1830
+ `**${value} ${fromUnit}** = **${tempResult.toPrecision(10)} ${toUnit}**`,
1831
+ '',
1832
+ `*Temperature conversion (non-linear)*`,
1833
+ ].join('\n');
1834
+ }
1835
+ // Look up units
1836
+ const from = UNITS[fromUnit];
1837
+ const to = UNITS[toUnit];
1838
+ if (!from)
1839
+ return `**Error**: Unknown unit "${fromUnit}". Use \`unit_convert\` with common abbreviations (m, kg, J, Pa, eV, atm, etc.).`;
1840
+ if (!to)
1841
+ return `**Error**: Unknown unit "${toUnit}". Use \`unit_convert\` with common abbreviations.`;
1842
+ if (from.dimension !== to.dimension)
1843
+ return `**Error**: Incompatible dimensions: "${fromUnit}" is ${from.dimension}, "${toUnit}" is ${to.dimension}.`;
1844
+ // Convert: value * from_factor / to_factor
1845
+ const result = value * from.factor / to.factor;
1846
+ // Format the result nicely
1847
+ const formatted = Math.abs(result) < 0.001 || Math.abs(result) > 1e9
1848
+ ? result.toExponential(10)
1849
+ : result.toPrecision(12);
1850
+ return [
1851
+ `# Unit Conversion`,
1852
+ '',
1853
+ `**${value} ${fromUnit}** = **${formatted} ${toUnit}**`,
1854
+ '',
1855
+ `| | Value | Unit | Dimension |`,
1856
+ `|---|---|---|---|`,
1857
+ `| From | ${value} | ${fromUnit} | ${from.dimension} |`,
1858
+ `| To | ${formatted} | ${toUnit} | ${to.dimension} |`,
1859
+ '',
1860
+ `*Conversion factor: 1 ${fromUnit} = ${(from.factor / to.factor).toExponential(6)} ${toUnit}*`,
1861
+ ].join('\n');
1862
+ },
1863
+ });
1864
+ // ── 6. Physical Constants ───────────────────────────────────────────────
1865
+ registerTool({
1866
+ name: 'physical_constants',
1867
+ description: 'Look up NIST CODATA physical constants with full precision, uncertainty, and units. Covers ~80 constants: speed of light, Planck, Boltzmann, Avogadro, electron mass, proton mass, gravitational, fine-structure, Rydberg, and more. Supports fuzzy name matching.',
1868
+ parameters: {
1869
+ name: { type: 'string', description: 'Name of the constant (e.g. "speed of light", "planck", "boltzmann", "avogadro", "electron mass")', required: true },
1870
+ },
1871
+ tier: 'free',
1872
+ async execute(args) {
1873
+ const query = String(args.name);
1874
+ const results = findConstant(query);
1875
+ if (results.length === 0) {
1876
+ // Show all available constants grouped
1877
+ const categories = {};
1878
+ for (const c of CONSTANTS) {
1879
+ const cat = c.unit === '(dimensionless)' ? 'Dimensionless' :
1880
+ /kg/.test(c.unit) ? 'Mass' :
1881
+ /m\/s|m\^/.test(c.unit) ? 'Mechanical' :
1882
+ /J|eV|W/.test(c.unit) ? 'Energy' :
1883
+ /C|A|V|F|S|H/.test(c.unit) ? 'Electromagnetic' :
1884
+ /K/.test(c.unit) ? 'Thermal' :
1885
+ /mol/.test(c.unit) ? 'Molar' :
1886
+ 'Other';
1887
+ if (!categories[cat])
1888
+ categories[cat] = [];
1889
+ categories[cat].push(c.name);
1890
+ }
1891
+ const lines = [`**No constant found matching "${query}".** Available constants:\n`];
1892
+ for (const [cat, names] of Object.entries(categories)) {
1893
+ lines.push(`**${cat}**: ${names.join(', ')}`);
1894
+ }
1895
+ return lines.join('\n');
1896
+ }
1897
+ const lines = [`# Physical Constants\n`];
1898
+ for (const c of results) {
1899
+ const valueStr = c.value.toExponential(12);
1900
+ const uncStr = c.uncertainty === 0 ? 'exact (by definition)' : `\u00B1 ${c.uncertainty.toExponential(4)}`;
1901
+ lines.push(`## ${c.name}`, `- **Symbol**: ${c.symbol}`, `- **Value**: ${valueStr}`, `- **Uncertainty**: ${uncStr}`, `- **Unit**: ${c.unit}`, `- **Relative uncertainty**: ${c.uncertainty === 0 ? 'exact' : (c.uncertainty / Math.abs(c.value)).toExponential(4)}`, '');
1902
+ }
1903
+ return lines.join('\n');
1904
+ },
1905
+ });
1906
+ // ── 7. Formula Solve ────────────────────────────────────────────────────
1907
+ registerTool({
1908
+ name: 'formula_solve',
1909
+ description: 'Solve or rearrange common scientific formulas. Covers ~50 formulas: PV=nRT, E=mc2, F=ma, V=IR, Coulomb, kinetic energy, Schwarzschild radius, Arrhenius, Nernst, Bernoulli, and many more. Given known values, solves for the unknown variable.',
1910
+ parameters: {
1911
+ formula: { type: 'string', description: 'Formula name or expression (e.g. "ideal gas", "E=mc2", "ohm", "coulomb", "arrhenius")', required: true },
1912
+ solve_for: { type: 'string', description: 'Variable to solve for (e.g. "P", "m", "V", "T")', required: true },
1913
+ known_values: { type: 'string', description: 'JSON object of known values, e.g. {"n": 1, "T": 300, "V": 0.0224}', required: true },
1914
+ },
1915
+ tier: 'free',
1916
+ async execute(args) {
1917
+ const formulaQuery = String(args.formula);
1918
+ const solveFor = String(args.solve_for);
1919
+ let knownValues;
1920
+ try {
1921
+ knownValues = JSON.parse(String(args.known_values));
1922
+ }
1923
+ catch {
1924
+ return '**Error**: known_values must be valid JSON. Example: `{"n": 1, "T": 300, "V": 0.0224}`';
1925
+ }
1926
+ const formula = findFormula(formulaQuery);
1927
+ if (!formula) {
1928
+ const available = FORMULAS.map(f => `- **${f.name}**: ${f.expression} (aliases: ${f.aliases.join(', ')})`).join('\n');
1929
+ return `**Formula not found**: "${formulaQuery}"\n\n## Available Formulas\n${available}`;
1930
+ }
1931
+ if (!(solveFor in formula.variables)) {
1932
+ return `**Error**: Variable "${solveFor}" not found in ${formula.name}.\nAvailable variables: ${Object.entries(formula.variables).map(([k, v]) => `\`${k}\` (${v})`).join(', ')}`;
1933
+ }
1934
+ try {
1935
+ const result = formula.solve(solveFor, knownValues);
1936
+ if (result === null || isNaN(result) || !isFinite(result)) {
1937
+ return `**Error**: Could not solve for "${solveFor}" with the given values. Check that all required variables are provided and values are valid.\n\nRequired variables: ${Object.entries(formula.variables).filter(([k]) => k !== solveFor).map(([k, v]) => `\`${k}\` (${v})`).join(', ')}`;
1938
+ }
1939
+ const formatted = Math.abs(result) < 0.001 || Math.abs(result) > 1e6
1940
+ ? result.toExponential(8)
1941
+ : result.toPrecision(10);
1942
+ return [
1943
+ `# Formula Solution`,
1944
+ '',
1945
+ `## ${formula.name}`,
1946
+ `**Expression**: ${formula.expression}`,
1947
+ '',
1948
+ `## Known Values`,
1949
+ ...Object.entries(knownValues).map(([k, v]) => `- **${k}** = ${v}${formula.variables[k] ? ` (${formula.variables[k]})` : ''}`),
1950
+ '',
1951
+ `## Result`,
1952
+ `**${solveFor}** = **${formatted}**`,
1953
+ formula.variables[solveFor] ? `*(${formula.variables[solveFor]})*` : '',
1954
+ '',
1955
+ `## Verification`,
1956
+ `Substituting back: ${Object.entries(knownValues).map(([k, v]) => `${k}=${v}`).join(', ')}, ${solveFor}=${formatted}`,
1957
+ ].join('\n');
1958
+ }
1959
+ catch (err) {
1960
+ return `**Error**: Calculation failed. ${err instanceof Error ? err.message : String(err)}`;
1961
+ }
1962
+ },
1963
+ });
1964
+ // ── 8. Research Methodology ─────────────────────────────────────────────
1965
+ registerTool({
1966
+ name: 'research_methodology',
1967
+ description: 'Generate detailed methodology sections for academic papers. Supports study types: experimental, survey, case-study, meta-analysis, cohort, ethnographic. Includes sampling strategy, data collection, analysis pipeline, and ethical considerations.',
1968
+ parameters: {
1969
+ study_type: { type: 'string', description: 'Study type: experimental, survey, case-study, meta-analysis, cohort, ethnographic', required: true },
1970
+ field: { type: 'string', description: 'Research field (e.g. psychology, medicine, computer science, education)', required: true },
1971
+ sample_description: { type: 'string', description: 'Description of target population/sample (e.g. "adults aged 18-65 with type 2 diabetes")', required: true },
1972
+ },
1973
+ tier: 'free',
1974
+ async execute(args) {
1975
+ const studyType = String(args.study_type).toLowerCase().trim();
1976
+ const field = String(args.field);
1977
+ const sample = String(args.sample_description);
1978
+ const methodologies = {
1979
+ experimental: {
1980
+ title: 'Experimental Study Methodology',
1981
+ design: `A randomized experimental design will be employed to establish causal relationships. Participants (${sample}) will be randomly assigned to treatment and control conditions. The study will use a pre-test/post-test control group design with blinding where feasible.`,
1982
+ sampling: `**Probability sampling** will be used to recruit from the target population (${sample}). Inclusion and exclusion criteria will be defined a priori. Sample size will be determined via power analysis (\u03B1=0.05, power=0.80, estimated effect size from pilot data or prior literature). Stratified randomization will balance key confounders across conditions.`,
1983
+ dataCollection: [
1984
+ 'Pre-registration of hypotheses, methods, and analysis plan (OSF/ClinicalTrials.gov)',
1985
+ 'Baseline assessment of all outcome and covariate measures',
1986
+ 'Random assignment to conditions using computer-generated sequence',
1987
+ 'Standardized intervention protocol with fidelity monitoring',
1988
+ 'Post-intervention outcome measurement (blinded assessors where possible)',
1989
+ 'Follow-up assessment at predetermined intervals',
1990
+ 'Adverse event monitoring and documentation',
1991
+ ],
1992
+ analysisSteps: [
1993
+ 'Descriptive statistics and assumption checking (normality, homogeneity of variance)',
1994
+ 'Intention-to-treat (ITT) analysis as primary; per-protocol as sensitivity analysis',
1995
+ 'Primary analysis: ANCOVA with baseline as covariate, or mixed-effects model for repeated measures',
1996
+ 'Effect sizes with 95% confidence intervals (Cohen\'s d, partial \u03B7\u00B2)',
1997
+ 'Multiple comparison correction (Bonferroni/Holm) if >1 primary outcome',
1998
+ 'Sensitivity analyses: missing data handling (multiple imputation), subgroup analyses (pre-specified)',
1999
+ 'Mediation/moderation analyses if theoretically justified',
2000
+ ],
2001
+ validityThreats: ['Selection bias (mitigated by randomization)', 'Attrition (monitor and analyze dropouts)', 'Hawthorne effect', 'Demand characteristics', 'Experimenter bias (mitigated by blinding)'],
2002
+ ethicalConsiderations: ['IRB/Ethics board approval required', 'Written informed consent', 'Right to withdraw without penalty', 'Data anonymization and secure storage', 'Equipoise requirement', 'DSMB for interim safety monitoring'],
2003
+ reportingGuideline: 'CONSORT (Consolidated Standards of Reporting Trials)',
2004
+ },
2005
+ survey: {
2006
+ title: 'Survey Study Methodology',
2007
+ design: `A cross-sectional survey design will be used to assess attitudes, behaviors, and characteristics of the target population (${sample}). The survey instrument will be developed through iterative pilot testing and validated using established psychometric methods.`,
2008
+ sampling: `**Stratified random sampling** from accessible population (${sample}). Sampling frame will be defined from institutional records, registries, or databases. Target response rate: 60%+ (with non-response bias analysis). Over-sampling of underrepresented subgroups if needed.`,
2009
+ dataCollection: [
2010
+ 'Literature review to identify existing validated instruments',
2011
+ 'Item generation and expert review (content validity)',
2012
+ 'Cognitive interviewing/think-aloud protocols (face validity)',
2013
+ 'Pilot study (n=30-50) for item analysis and reliability estimation',
2014
+ 'Final instrument: demographics, validated scales, open-ended items',
2015
+ 'Distribution via online platform (Qualtrics/REDCap) with unique links',
2016
+ 'Reminders at 1 week and 2 weeks post-distribution',
2017
+ 'Data quality checks: attention checks, completion time, straightlining detection',
2018
+ ],
2019
+ analysisSteps: [
2020
+ 'Response rate calculation and non-response bias analysis',
2021
+ 'Data cleaning: remove incomplete (<50%) and low-quality responses',
2022
+ 'Confirmatory factor analysis (CFA) to validate scale structure',
2023
+ 'Internal consistency: Cronbach\'s alpha (\u03B1 > 0.70 acceptable)',
2024
+ 'Descriptive statistics stratified by key demographics',
2025
+ 'Inferential statistics: regression, SEM, or multilevel modeling as appropriate',
2026
+ 'Open-ended responses: thematic analysis (Braun & Clarke framework)',
2027
+ ],
2028
+ validityThreats: ['Non-response bias', 'Social desirability bias', 'Common method variance', 'Self-selection bias', 'Recall bias'],
2029
+ ethicalConsiderations: ['IRB/Ethics approval', 'Informed consent on first page', 'Anonymity/confidentiality guarantees', 'No deceptive items', 'Data stored on encrypted servers', 'Compliance with GDPR/local data protection laws'],
2030
+ reportingGuideline: 'CHERRIES (Checklist for Reporting Results of Internet E-Surveys)',
2031
+ },
2032
+ 'case-study': {
2033
+ title: 'Case Study Methodology',
2034
+ design: `An instrumental case study design (Stake, 1995) will be employed to provide in-depth understanding of the phenomenon within its real-world context. The case (${sample}) was selected purposively for its potential to illuminate theoretical constructs.`,
2035
+ sampling: `**Purposive sampling** — case selected for theoretical relevance, not statistical representativeness. Selection criteria: (1) information-rich case, (2) accessible for extended observation, (3) representative of the phenomenon. Multiple cases may be included for cross-case analysis (Yin, 2018).`,
2036
+ dataCollection: [
2037
+ 'Document analysis: archival records, reports, meeting minutes, correspondence',
2038
+ 'Semi-structured interviews with key stakeholders (audio-recorded, transcribed)',
2039
+ 'Direct observation with field notes (structured observation protocol)',
2040
+ 'Participant observation where appropriate (reflexive journaling)',
2041
+ 'Physical artifacts and digital data sources',
2042
+ 'Triangulation across multiple data sources for each finding',
2043
+ ],
2044
+ analysisSteps: [
2045
+ 'Within-case analysis: chronological narrative construction',
2046
+ 'Open coding of interview transcripts and field notes',
2047
+ 'Axial coding: identifying relationships between categories',
2048
+ 'Pattern matching: comparing empirical patterns to theoretical predictions',
2049
+ 'Explanation building: iterative refinement of causal mechanisms',
2050
+ 'Cross-case synthesis (if multiple cases)',
2051
+ 'Member checking: participants review interpretations for accuracy',
2052
+ ],
2053
+ validityThreats: ['Researcher bias', 'Limited generalizability', 'Selective reporting', 'Reactivity/observer effect'],
2054
+ ethicalConsiderations: ['IRB/Ethics approval', 'Informed consent for all participants', 'Anonymization of identifying details', 'Secure storage of recordings/transcripts', 'Right to review and retract statements', 'Power dynamics awareness (researcher-participant relationship)'],
2055
+ reportingGuideline: 'CARE (Case Reports) or Yin\'s case study reporting standards',
2056
+ },
2057
+ 'meta-analysis': {
2058
+ title: 'Meta-Analysis Methodology',
2059
+ design: `A systematic review and meta-analysis will be conducted following PRISMA 2020 guidelines to synthesize quantitative evidence on the research question. The protocol will be pre-registered on PROSPERO.`,
2060
+ sampling: `**Systematic literature search** across multiple databases: PubMed/MEDLINE, PsycINFO, Scopus, Web of Science, and Cochrane Library. Grey literature: ProQuest Dissertations, conference proceedings, pre-print servers. Inclusion criteria defined using PICOS framework. No language restrictions. Search strategy developed with medical librarian.`,
2061
+ dataCollection: [
2062
+ 'Develop and pilot search strategy with Boolean operators and MeSH terms',
2063
+ 'Run searches across all databases; deduplicate using Covidence/Rayyan',
2064
+ 'Title/abstract screening by 2 independent reviewers (kappa > 0.80)',
2065
+ 'Full-text screening with documented exclusion reasons',
2066
+ 'Data extraction using standardized form (piloted on 5 studies)',
2067
+ 'Risk of bias assessment: Cochrane RoB 2 (RCTs) or Newcastle-Ottawa Scale (observational)',
2068
+ 'GRADE assessment of certainty of evidence',
2069
+ 'Contact original authors for missing data where needed',
2070
+ ],
2071
+ analysisSteps: [
2072
+ 'Calculate standardized effect sizes (SMD, OR, RR, HR) from each study',
2073
+ 'Random-effects meta-analysis (DerSimonian-Laird or REML estimator)',
2074
+ 'Heterogeneity assessment: Q-statistic, I\u00B2 (>75% = substantial), \u03C4\u00B2',
2075
+ 'Forest plot visualization',
2076
+ 'Subgroup analyses (pre-specified): by study design, population, intervention characteristics',
2077
+ 'Meta-regression for continuous moderators',
2078
+ 'Sensitivity analyses: leave-one-out, trim-and-fill, influence diagnostics',
2079
+ 'Publication bias: funnel plot, Egger\'s test, p-curve analysis',
2080
+ ],
2081
+ validityThreats: ['Publication bias', 'Heterogeneity', 'Garbage in/garbage out (low-quality studies)', 'Language bias', 'Time-lag bias', 'Ecological fallacy'],
2082
+ ethicalConsiderations: ['Protocol pre-registration (PROSPERO)', 'Transparent reporting of all search decisions', 'No selective outcome reporting', 'Declare conflicts of interest', 'Open data/code for reproducibility'],
2083
+ reportingGuideline: 'PRISMA 2020 (Preferred Reporting Items for Systematic Reviews and Meta-Analyses)',
2084
+ },
2085
+ cohort: {
2086
+ title: 'Cohort Study Methodology',
2087
+ design: `A prospective cohort study will follow a defined group (${sample}) over time to assess the relationship between exposure and outcome. Participants will be classified by exposure status at baseline and followed for the development of outcomes.`,
2088
+ sampling: `**Consecutive or population-based sampling** of ${sample}. Cohort defined by shared characteristic or exposure. Comparison group: unexposed individuals from the same source population. Sample size calculated based on expected incidence rates, desired HR precision, and anticipated attrition. Over-recruit by 20-30% for dropout.`,
2089
+ dataCollection: [
2090
+ 'Baseline assessment: exposure measurement, covariates, outcome-free confirmation',
2091
+ 'Standardized data collection instruments (validated questionnaires, clinical measures)',
2092
+ 'Regular follow-up at predetermined intervals (annual, biannual)',
2093
+ 'Ascertainment of outcomes through clinical records, registries, or biomarkers',
2094
+ 'Loss-to-follow-up tracking with documented reasons',
2095
+ 'Biospecimen banking (if applicable) with informed consent',
2096
+ ],
2097
+ analysisSteps: [
2098
+ 'Describe cohort characteristics: exposed vs. unexposed comparison',
2099
+ 'Attrition analysis: compare completers vs. dropouts',
2100
+ 'Incidence rates and cumulative incidence curves',
2101
+ 'Cox proportional hazards regression (check PH assumption via Schoenfeld residuals)',
2102
+ 'Confounding control: multivariable adjustment, propensity scores, or IPW',
2103
+ 'Time-varying exposure analysis if relevant',
2104
+ 'Competing risks analysis (Fine-Gray model) if applicable',
2105
+ 'Sensitivity analyses for unmeasured confounding (E-value)',
2106
+ ],
2107
+ validityThreats: ['Confounding (known and unknown)', 'Loss to follow-up (selection bias)', 'Information bias (exposure misclassification)', 'Healthy worker/volunteer effect', 'Reverse causation (if exposure timing unclear)'],
2108
+ ethicalConsiderations: ['Long-term participant commitment', 'Ongoing consent for continued participation', 'Incidental findings protocol', 'Data security across extended study period', 'Community engagement and results dissemination'],
2109
+ reportingGuideline: 'STROBE (Strengthening the Reporting of Observational Studies in Epidemiology)',
2110
+ },
2111
+ ethnographic: {
2112
+ title: 'Ethnographic Study Methodology',
2113
+ design: `An ethnographic approach will be used to understand the cultural practices, social dynamics, and lived experiences of ${sample}. Extended immersion in the field setting will enable thick description (Geertz, 1973) and emic understanding.`,
2114
+ sampling: `**Purposive and snowball sampling** within the cultural setting. Key informants identified through initial contacts and theoretical sampling. Sampling continues until theoretical saturation is reached. Setting selected for its potential to reveal cultural patterns relevant to the research question.`,
2115
+ dataCollection: [
2116
+ 'Extended participant observation (minimum 3-6 months in field)',
2117
+ 'Detailed field notes: descriptive, reflective, and analytic memos',
2118
+ 'In-depth interviews: unstructured and semi-structured (key informants)',
2119
+ 'Focus groups with community members',
2120
+ 'Document and artifact collection (photographs, social media, texts)',
2121
+ 'Reflexive journal: researcher positionality and evolving interpretations',
2122
+ 'Life histories or narrative interviews for depth',
2123
+ ],
2124
+ analysisSteps: [
2125
+ 'Concurrent data collection and analysis (iterative cycle)',
2126
+ 'Open coding of field notes and transcripts',
2127
+ 'Domain analysis: identifying cultural categories',
2128
+ 'Taxonomic analysis: internal structure of domains',
2129
+ 'Componential analysis: attributes that distinguish members of domains',
2130
+ 'Theme identification: cross-cutting cultural themes',
2131
+ 'Narrative construction: thick description with analytic commentary',
2132
+ 'Member checking and peer debriefing for credibility',
2133
+ ],
2134
+ validityThreats: ['Going native (over-identification)', 'Reactivity/observer effect', 'Researcher bias', 'Cultural misinterpretation', 'Power dynamics', 'Selective attention and recall'],
2135
+ ethicalConsiderations: ['Community-level and individual informed consent', 'Anonymization and pseudonyms', 'Power dynamics awareness', 'Cultural sensitivity and reciprocity', 'Protection of vulnerable populations', 'Community review of findings before publication', 'IRB/Ethics board with qualitative expertise'],
2136
+ reportingGuideline: 'COREQ (Consolidated Criteria for Reporting Qualitative Research)',
2137
+ },
2138
+ };
2139
+ const meth = methodologies[studyType];
2140
+ if (!meth) {
2141
+ return `**Error**: Unknown study type "${studyType}". Supported: ${Object.keys(methodologies).join(', ')}`;
2142
+ }
2143
+ return [
2144
+ `# ${meth.title}`,
2145
+ `**Field**: ${field} | **Sample**: ${sample}`,
2146
+ '',
2147
+ `## Research Design`,
2148
+ meth.design,
2149
+ '',
2150
+ `## Sampling Strategy`,
2151
+ meth.sampling,
2152
+ '',
2153
+ `## Data Collection Procedures`,
2154
+ ...meth.dataCollection.map((s, i) => `${i + 1}. ${s}`),
2155
+ '',
2156
+ `## Data Analysis Pipeline`,
2157
+ ...meth.analysisSteps.map((s, i) => `${i + 1}. ${s}`),
2158
+ '',
2159
+ `## Threats to Validity`,
2160
+ ...meth.validityThreats.map(t => `- ${t}`),
2161
+ '',
2162
+ `## Ethical Considerations`,
2163
+ ...meth.ethicalConsiderations.map(e => `- ${e}`),
2164
+ '',
2165
+ `## Reporting Guideline`,
2166
+ `Follow **${meth.reportingGuideline}** for transparent reporting.`,
2167
+ '',
2168
+ `## Quality Criteria`,
2169
+ studyType === 'ethnographic' || studyType === 'case-study'
2170
+ ? '- Credibility (prolonged engagement, triangulation, member checking)\n- Transferability (thick description)\n- Dependability (audit trail)\n- Confirmability (reflexivity)'
2171
+ : '- Internal validity (causal inference strength)\n- External validity (generalizability)\n- Construct validity (measurement quality)\n- Reliability (reproducibility)',
2172
+ ].join('\n');
2173
+ },
2174
+ });
2175
+ // ── 9. Preprint Tracker ─────────────────────────────────────────────────
2176
+ registerTool({
2177
+ name: 'preprint_tracker',
2178
+ description: 'Track recent preprints from arXiv, bioRxiv, and medRxiv in a specific field. Returns title, authors, date, abstract snippet, and link.',
2179
+ parameters: {
2180
+ field: { type: 'string', description: 'Research field or topic (e.g. "machine learning", "CRISPR", "COVID-19 vaccines", "quantum computing")', required: true },
2181
+ days_back: { type: 'number', description: 'How many days back to search (default: 7)' },
2182
+ limit: { type: 'number', description: 'Maximum number of results (default: 10)' },
2183
+ },
2184
+ tier: 'free',
2185
+ async execute(args) {
2186
+ const field = String(args.field);
2187
+ const daysBack = typeof args.days_back === 'number' ? args.days_back : 7;
2188
+ const limit = typeof args.limit === 'number' ? Math.min(args.limit, 30) : 10;
2189
+ const preprints = [];
2190
+ // Calculate date range
2191
+ const now = new Date();
2192
+ const fromDate = new Date(now.getTime() - daysBack * 24 * 60 * 60 * 1000);
2193
+ const fromStr = fromDate.toISOString().split('T')[0];
2194
+ const toStr = now.toISOString().split('T')[0];
2195
+ // Fetch from all three sources in parallel
2196
+ const [arxivResult, biorxivResult, medrxivResult] = await Promise.allSettled([
2197
+ // arXiv API (returns Atom XML)
2198
+ (async () => {
2199
+ const encoded = encodeURIComponent(field);
2200
+ const url = `http://export.arxiv.org/api/query?search_query=all:${encoded}&sortBy=submittedDate&sortOrder=descending&max_results=${limit}`;
2201
+ const res = await labFetch(url);
2202
+ const text = await res.text();
2203
+ // Parse XML manually (no DOM parser in Node without dependencies)
2204
+ const entries = text.split('<entry>').slice(1);
2205
+ for (const entry of entries) {
2206
+ const extract = (tag) => {
2207
+ const match = entry.match(new RegExp(`<${tag}[^>]*>(.*?)</${tag}>`, 's'));
2208
+ return match ? match[1].trim() : '';
2209
+ };
2210
+ const title = extract('title').replace(/\s+/g, ' ');
2211
+ const summary = extract('summary').replace(/\s+/g, ' ');
2212
+ const published = extract('published');
2213
+ const category = entry.match(/term="([^"]+)"/)?.[1] || '';
2214
+ // Extract authors
2215
+ const authorMatches = entry.matchAll(/<name>([^<]+)<\/name>/g);
2216
+ const authors = Array.from(authorMatches).map(m => m[1]);
2217
+ // Extract link
2218
+ const linkMatch = entry.match(/href="(https:\/\/arxiv\.org\/abs\/[^"]+)"/);
2219
+ const url = linkMatch ? linkMatch[1] : '';
2220
+ if (title && published) {
2221
+ const pubDate = published.split('T')[0];
2222
+ if (pubDate >= fromStr) {
2223
+ preprints.push({
2224
+ title, authors: authors.slice(0, 5), date: pubDate,
2225
+ abstract: summary.slice(0, 200) + (summary.length > 200 ? '...' : ''),
2226
+ url, source: 'arXiv', category,
2227
+ });
2228
+ }
2229
+ }
2230
+ }
2231
+ })(),
2232
+ // bioRxiv API
2233
+ (async () => {
2234
+ const url = `https://api.biorxiv.org/details/biorxiv/${fromStr}/${toStr}/0/25`;
2235
+ const data = await labFetchJson(url);
2236
+ if (data.collection) {
2237
+ for (const p of data.collection) {
2238
+ const title = p.title || '';
2239
+ if (title.toLowerCase().includes(field.toLowerCase()) ||
2240
+ (p.abstract || '').toLowerCase().includes(field.toLowerCase()) ||
2241
+ (p.category || '').toLowerCase().includes(field.toLowerCase())) {
2242
+ preprints.push({
2243
+ title,
2244
+ authors: (p.authors || '').split(';').map(a => a.trim()).filter(Boolean).slice(0, 5),
2245
+ date: p.date || '',
2246
+ abstract: (p.abstract || '').slice(0, 200) + ((p.abstract || '').length > 200 ? '...' : ''),
2247
+ url: p.doi ? `https://doi.org/${p.doi}` : '',
2248
+ source: 'bioRxiv',
2249
+ category: p.category,
2250
+ });
2251
+ }
2252
+ }
2253
+ }
2254
+ })(),
2255
+ // medRxiv API
2256
+ (async () => {
2257
+ const url = `https://api.biorxiv.org/details/medrxiv/${fromStr}/${toStr}/0/25`;
2258
+ const data = await labFetchJson(url);
2259
+ if (data.collection) {
2260
+ for (const p of data.collection) {
2261
+ const title = p.title || '';
2262
+ if (title.toLowerCase().includes(field.toLowerCase()) ||
2263
+ (p.abstract || '').toLowerCase().includes(field.toLowerCase()) ||
2264
+ (p.category || '').toLowerCase().includes(field.toLowerCase())) {
2265
+ preprints.push({
2266
+ title,
2267
+ authors: (p.authors || '').split(';').map(a => a.trim()).filter(Boolean).slice(0, 5),
2268
+ date: p.date || '',
2269
+ abstract: (p.abstract || '').slice(0, 200) + ((p.abstract || '').length > 200 ? '...' : ''),
2270
+ url: p.doi ? `https://doi.org/${p.doi}` : '',
2271
+ source: 'medRxiv',
2272
+ category: p.category,
2273
+ });
2274
+ }
2275
+ }
2276
+ }
2277
+ })(),
2278
+ ]);
2279
+ // Sort by date descending
2280
+ preprints.sort((a, b) => b.date.localeCompare(a.date));
2281
+ const results = preprints.slice(0, limit);
2282
+ if (results.length === 0) {
2283
+ const errors = [];
2284
+ if (arxivResult.status === 'rejected')
2285
+ errors.push(`arXiv: ${arxivResult.reason}`);
2286
+ if (biorxivResult.status === 'rejected')
2287
+ errors.push(`bioRxiv: ${biorxivResult.reason}`);
2288
+ if (medrxivResult.status === 'rejected')
2289
+ errors.push(`medRxiv: ${medrxivResult.reason}`);
2290
+ return errors.length > 0
2291
+ ? `**No preprints found** for "${field}" in the last ${daysBack} days.\n\nAPI errors:\n${errors.join('\n')}`
2292
+ : `**No preprints found** for "${field}" in the last ${daysBack} days. Try broader terms or a longer time window.`;
2293
+ }
2294
+ // Count by source
2295
+ const sourceCounts = {};
2296
+ for (const p of results)
2297
+ sourceCounts[p.source] = (sourceCounts[p.source] || 0) + 1;
2298
+ const lines = [
2299
+ `# Recent Preprints: "${field}"`,
2300
+ `**Period**: ${fromStr} to ${toStr} (${daysBack} days)`,
2301
+ `**Results**: ${results.length} preprints (${Object.entries(sourceCounts).map(([k, v]) => `${k}: ${v}`).join(', ')})`,
2302
+ '',
2303
+ ];
2304
+ for (let i = 0; i < results.length; i++) {
2305
+ const p = results[i];
2306
+ lines.push(`### ${i + 1}. ${p.title}`, `- **Authors**: ${p.authors.join(', ')}${p.authors.length >= 5 ? ' et al.' : ''}`, `- **Date**: ${p.date} | **Source**: ${p.source}${p.category ? ` | **Category**: ${p.category}` : ''}`, p.url ? `- **Link**: ${p.url}` : '', p.abstract ? `- **Abstract**: ${p.abstract}` : '', '');
2307
+ }
2308
+ return lines.filter(l => l !== undefined).join('\n');
2309
+ },
2310
+ });
2311
+ // ── 10. Open Access Find ────────────────────────────────────────────────
2312
+ registerTool({
2313
+ name: 'open_access_find',
2314
+ description: 'Find free full-text versions of academic papers via Unpaywall and CORE APIs. Provide a DOI or title to discover open access copies, green/gold OA status, and download links.',
2315
+ parameters: {
2316
+ identifier: { type: 'string', description: 'DOI (e.g. "10.1038/nature12373") or paper title', required: true },
2317
+ type: { type: 'string', description: 'Identifier type: "doi" or "title"', required: true },
2318
+ },
2319
+ tier: 'free',
2320
+ async execute(args) {
2321
+ const identifier = String(args.identifier).trim();
2322
+ const type = String(args.type).toLowerCase().trim();
2323
+ if (type !== 'doi' && type !== 'title') {
2324
+ return '**Error**: Type must be "doi" or "title".';
2325
+ }
2326
+ const results = [];
2327
+ if (type === 'doi') {
2328
+ // Fetch from Unpaywall and CORE in parallel
2329
+ const [unpaywall, core] = await Promise.allSettled([
2330
+ // Unpaywall
2331
+ (async () => {
2332
+ const url = `https://api.unpaywall.org/v2/${encodeURIComponent(identifier)}?email=kbot@kernel.chat`;
2333
+ const data = await labFetchJson(url);
2334
+ const otherLocs = [];
2335
+ if (data.oa_locations) {
2336
+ for (const loc of data.oa_locations.slice(0, 5)) {
2337
+ if (loc.url) {
2338
+ otherLocs.push({
2339
+ url: loc.url,
2340
+ type: `${loc.host_type || 'unknown'} (${loc.version || 'unknown'})`,
2341
+ source: 'Unpaywall',
2342
+ });
2343
+ }
2344
+ }
2345
+ }
2346
+ results.push({
2347
+ title: data.title || 'Unknown',
2348
+ doi: data.doi || identifier,
2349
+ isOA: data.is_oa || false,
2350
+ oaStatus: data.oa_status || 'closed',
2351
+ bestUrl: data.best_oa_location?.url || '',
2352
+ pdfUrl: data.best_oa_location?.url_for_pdf || '',
2353
+ license: data.best_oa_location?.license || 'Unknown',
2354
+ source: 'Unpaywall',
2355
+ otherLocations: otherLocs,
2356
+ });
2357
+ })(),
2358
+ // CORE
2359
+ (async () => {
2360
+ const url = `https://api.core.ac.uk/v3/search/works?q=doi:"${encodeURIComponent(identifier)}"&limit=3`;
2361
+ const data = await labFetchJson(url);
2362
+ if (data.results) {
2363
+ for (const r of data.results) {
2364
+ results.push({
2365
+ title: r.title || 'Unknown',
2366
+ doi: r.doi || identifier,
2367
+ isOA: !!(r.downloadUrl || r.sourceFulltextUrls?.length),
2368
+ oaStatus: r.downloadUrl ? 'available' : 'unknown',
2369
+ bestUrl: r.downloadUrl || r.sourceFulltextUrls?.[0] || '',
2370
+ pdfUrl: r.downloadUrl || '',
2371
+ license: 'Check source',
2372
+ source: 'CORE',
2373
+ otherLocations: (r.sourceFulltextUrls || []).slice(0, 3).map(u => ({ url: u, type: 'repository', source: 'CORE' })),
2374
+ });
2375
+ }
2376
+ }
2377
+ })(),
2378
+ ]);
2379
+ if (results.length === 0) {
2380
+ const errors = [];
2381
+ if (unpaywall.status === 'rejected')
2382
+ errors.push(`Unpaywall: ${unpaywall.reason}`);
2383
+ if (core.status === 'rejected')
2384
+ errors.push(`CORE: ${core.reason}`);
2385
+ return `**No open access version found** for DOI: ${identifier}\n\n${errors.length ? 'API errors:\n' + errors.join('\n') : 'The paper may not have an open access version available.'}`;
2386
+ }
2387
+ }
2388
+ else {
2389
+ // Title search via CORE
2390
+ try {
2391
+ const url = `https://api.core.ac.uk/v3/search/works?q=${encodeURIComponent(identifier)}&limit=5`;
2392
+ const data = await labFetchJson(url);
2393
+ if (data.results) {
2394
+ for (const r of data.results) {
2395
+ results.push({
2396
+ title: r.title || 'Unknown',
2397
+ doi: r.doi || '',
2398
+ isOA: !!(r.downloadUrl || r.sourceFulltextUrls?.length),
2399
+ oaStatus: r.downloadUrl ? 'available' : 'unknown',
2400
+ bestUrl: r.downloadUrl || r.sourceFulltextUrls?.[0] || '',
2401
+ pdfUrl: r.downloadUrl || '',
2402
+ license: 'Check source',
2403
+ source: 'CORE',
2404
+ otherLocations: (r.sourceFulltextUrls || []).slice(0, 3).map(u => ({ url: u, type: 'repository', source: 'CORE' })),
2405
+ });
2406
+ }
2407
+ }
2408
+ }
2409
+ catch (err) {
2410
+ return `**Error searching by title**: ${err instanceof Error ? err.message : String(err)}`;
2411
+ }
2412
+ if (results.length === 0) {
2413
+ return `**No open access versions found** for title: "${identifier}". Try searching by DOI for more precise results.`;
2414
+ }
2415
+ }
2416
+ const lines = [
2417
+ `# Open Access Finder`,
2418
+ `**Query**: ${type === 'doi' ? `DOI: ${identifier}` : `Title: "${identifier}"`}`,
2419
+ '',
2420
+ ];
2421
+ // Deduplicate by DOI
2422
+ const seen = new Set();
2423
+ const unique = results.filter(r => {
2424
+ const key = r.doi || r.title;
2425
+ if (seen.has(key))
2426
+ return false;
2427
+ seen.add(key);
2428
+ return true;
2429
+ });
2430
+ for (const r of unique) {
2431
+ const statusEmoji = r.isOA ? 'OPEN' : 'CLOSED';
2432
+ lines.push(`## ${r.title}`, `- **DOI**: ${r.doi ? `[${r.doi}](https://doi.org/${r.doi})` : 'N/A'}`, `- **OA Status**: **${statusEmoji}** (${r.oaStatus})`, `- **License**: ${r.license}`, `- **Source**: ${r.source}`);
2433
+ if (r.bestUrl)
2434
+ lines.push(`- **Best OA Link**: ${r.bestUrl}`);
2435
+ if (r.pdfUrl && r.pdfUrl !== r.bestUrl)
2436
+ lines.push(`- **PDF**: ${r.pdfUrl}`);
2437
+ if (r.otherLocations.length > 0) {
2438
+ lines.push(`- **Other locations**:`);
2439
+ for (const loc of r.otherLocations) {
2440
+ lines.push(` - ${loc.url} (${loc.type})`);
2441
+ }
2442
+ }
2443
+ lines.push('');
2444
+ }
2445
+ if (!unique.some(r => r.isOA)) {
2446
+ lines.push(`---`, `**No open access version found.** Alternatives:`, `- Request from the author directly (ResearchGate, email)`, `- Check your institutional library access`, `- Search Google Scholar for cached/preprint versions`, `- Try interlibrary loan (ILL) through your library`);
2447
+ }
2448
+ return lines.join('\n');
2449
+ },
2450
+ });
2451
+ }
2452
+ //# sourceMappingURL=lab-core.js.map