@svrnsec/pulse 0.7.0 → 0.8.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 (48) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +883 -782
  3. package/SECURITY.md +86 -86
  4. package/bin/svrnsec-pulse.js +7 -7
  5. package/dist/{pulse.cjs.js → pulse.cjs} +6378 -6419
  6. package/dist/pulse.cjs.map +1 -0
  7. package/dist/pulse.esm.js +6379 -6420
  8. package/dist/pulse.esm.js.map +1 -1
  9. package/index.d.ts +895 -846
  10. package/package.json +185 -184
  11. package/pkg/pulse_core.js +174 -173
  12. package/src/analysis/audio.js +213 -213
  13. package/src/analysis/authenticityAudit.js +408 -393
  14. package/src/analysis/coherence.js +502 -502
  15. package/src/analysis/coordinatedBehavior.js +825 -804
  16. package/src/analysis/heuristic.js +428 -428
  17. package/src/analysis/jitter.js +446 -446
  18. package/src/analysis/llm.js +473 -472
  19. package/src/analysis/populationEntropy.js +404 -403
  20. package/src/analysis/provider.js +248 -248
  21. package/src/analysis/refraction.js +392 -391
  22. package/src/analysis/trustScore.js +356 -356
  23. package/src/cli/args.js +36 -36
  24. package/src/cli/commands/scan.js +192 -192
  25. package/src/cli/runner.js +157 -157
  26. package/src/collector/adaptive.js +200 -200
  27. package/src/collector/bio.js +297 -287
  28. package/src/collector/canvas.js +247 -239
  29. package/src/collector/dram.js +203 -203
  30. package/src/collector/enf.js +311 -311
  31. package/src/collector/entropy.js +195 -195
  32. package/src/collector/gpu.js +248 -245
  33. package/src/collector/idleAttestation.js +480 -480
  34. package/src/collector/sabTimer.js +189 -191
  35. package/src/fingerprint.js +475 -475
  36. package/src/index.js +342 -342
  37. package/src/integrations/react-native.js +462 -459
  38. package/src/integrations/react.js +184 -185
  39. package/src/middleware/express.js +155 -155
  40. package/src/middleware/next.js +174 -175
  41. package/src/proof/challenge.js +249 -249
  42. package/src/proof/engagementToken.js +426 -394
  43. package/src/proof/fingerprint.js +268 -268
  44. package/src/proof/validator.js +82 -142
  45. package/src/registry/serializer.js +349 -349
  46. package/src/terminal.js +263 -263
  47. package/src/update-notifier.js +259 -264
  48. package/dist/pulse.cjs.js.map +0 -1
@@ -1,287 +1,297 @@
1
- /**
2
- * @svrnsec/pulse — Bio-Binding Layer
3
- *
4
- * Captures mouse-movement micro-stutters and keystroke-cadence dynamics
5
- * WHILE the hardware entropy probe is running. Computes the
6
- * "Interference Coefficient": how much human input jitters hardware timing.
7
- *
8
- * PRIVACY NOTE: Only timing deltas are retained. No key labels, no raw
9
- * (x, y) coordinates, no content of any kind is stored or transmitted.
10
- */
11
-
12
- // ---------------------------------------------------------------------------
13
- // Internal state
14
- // ---------------------------------------------------------------------------
15
- const MAX_EVENTS = 500; // rolling buffer cap
16
-
17
- // ---------------------------------------------------------------------------
18
- // BioCollector class
19
- // ---------------------------------------------------------------------------
20
- export class BioCollector {
21
- constructor() {
22
- this._mouseEvents = []; // { t: DOMHighResTimeStamp, dx, dy }
23
- this._keyEvents = []; // { t, type: 'down'|'up', dwell: ms|null }
24
- this._lastKey = {}; // keyCode { downAt: t }
25
- this._lastMouse = null; // { t, x, y }
26
- this._startTime = null;
27
- this._active = false;
28
-
29
- // Bound handlers (needed for removeEventListener)
30
- this._onMouseMove = this._onMouseMove.bind(this);
31
- this._onKeyDown = this._onKeyDown.bind(this);
32
- this._onKeyUp = this._onKeyUp.bind(this);
33
- }
34
-
35
- // ── Lifecycle ────────────────────────────────────────────────────────────
36
-
37
- start() {
38
- if (this._active) return;
39
- this._active = true;
40
- this._startTime = performance.now();
41
-
42
- if (typeof window !== 'undefined') {
43
- window.addEventListener('pointermove', this._onMouseMove, { passive: true });
44
- window.addEventListener('keydown', this._onKeyDown, { passive: true });
45
- window.addEventListener('keyup', this._onKeyUp, { passive: true });
46
- }
47
- }
48
-
49
- stop() {
50
- if (!this._active) return;
51
- this._active = false;
52
-
53
- if (typeof window !== 'undefined') {
54
- window.removeEventListener('pointermove', this._onMouseMove);
55
- window.removeEventListener('keydown', this._onKeyDown);
56
- window.removeEventListener('keyup', this._onKeyUp);
57
- }
58
- }
59
-
60
- // ── Event handlers ────────────────────────────────────────────────────────
61
-
62
- _onMouseMove(e) {
63
- if (!this._active) return;
64
- const t = e.timeStamp ?? performance.now();
65
- const cur = { t, x: e.clientX, y: e.clientY };
66
-
67
- if (this._lastMouse) {
68
- const dt = t - this._lastMouse.t;
69
- const dx = cur.x - this._lastMouse.x;
70
- const dy = cur.y - this._lastMouse.y;
71
- // Only store the delta, not absolute position (privacy)
72
- if (this._mouseEvents.length < MAX_EVENTS) {
73
- this._mouseEvents.push({ t, dt, dx, dy,
74
- pressure: e.pressure ?? 0,
75
- pointerType: e.pointerType ?? 'mouse' });
76
- }
77
- }
78
- this._lastMouse = cur;
79
- }
80
-
81
- _onKeyDown(e) {
82
- if (!this._active) return;
83
- const t = e.timeStamp ?? performance.now();
84
- // Store timestamp keyed by code (NOT key label)
85
- this._lastKey[e.code] = { downAt: t };
86
- }
87
-
88
- _onKeyUp(e) {
89
- if (!this._active) return;
90
- const t = e.timeStamp ?? performance.now();
91
- const rec = this._lastKey[e.code];
92
- const dwell = rec ? (t - rec.downAt) : null;
93
- delete this._lastKey[e.code];
94
-
95
- if (this._keyEvents.length < MAX_EVENTS) {
96
- // Only dwell time; key identity NOT stored.
97
- this._keyEvents.push({ t, dwell });
98
- }
99
- }
100
-
101
- // ── snapshot ─────────────────────────────────────────────────────────────
102
-
103
- /**
104
- * Returns a privacy-preserving statistical snapshot of collected bio signals.
105
- * Raw events are summarised; nothing identifiable is included in the output.
106
- *
107
- * @param {number[]} computationTimings - entropy probe timing array
108
- * @returns {BioSnapshot}
109
- */
110
- snapshot(computationTimings = []) {
111
- const now = performance.now();
112
- const durationMs = this._startTime != null ? (now - this._startTime) : 0;
113
-
114
- // ── Mouse statistics ────────────────────────────────────────────────
115
- const iei = this._mouseEvents.map(e => e.dt);
116
- const velocities = this._mouseEvents.map(e =>
117
- e.dt > 0 ? Math.hypot(e.dx, e.dy) / e.dt : 0
118
- );
119
- const pressure = this._mouseEvents.map(e => e.pressure);
120
- const angJerk = _computeAngularJerk(this._mouseEvents);
121
-
122
- const mouseStats = {
123
- sampleCount: iei.length,
124
- ieiMean: _mean(iei),
125
- ieiCV: _cv(iei),
126
- velocityP50: _percentile(velocities, 50),
127
- velocityP95: _percentile(velocities, 95),
128
- angularJerkMean: _mean(angJerk),
129
- pressureVariance: _variance(pressure),
130
- };
131
-
132
- // ── Keyboard statistics ───────────────────────────────────────────────
133
- const dwellTimes = this._keyEvents.filter(e => e.dwell != null).map(e => e.dwell);
134
- const iki = [];
135
- for (let i = 1; i < this._keyEvents.length; i++) {
136
- iki.push(this._keyEvents[i].t - this._keyEvents[i - 1].t);
137
- }
138
-
139
- const keyStats = {
140
- sampleCount: dwellTimes.length,
141
- dwellMean: _mean(dwellTimes),
142
- dwellCV: _cv(dwellTimes),
143
- ikiMean: _mean(iki),
144
- ikiCV: _cv(iki),
145
- };
146
-
147
- // ── Interference Coefficient ──────────────────────────────────────────
148
- // Cross-correlate input event density with computation timing deviations.
149
- // A real human on real hardware creates measurable CPU-scheduling pressure
150
- // that perturbs the entropy probe's timing.
151
- const interferenceCoefficient = _computeInterference(
152
- this._mouseEvents,
153
- this._keyEvents,
154
- computationTimings,
155
- );
156
-
157
- return {
158
- mouse: mouseStats,
159
- keyboard: keyStats,
160
- interferenceCoefficient,
161
- durationMs,
162
- hasActivity: iei.length > 5 || dwellTimes.length > 2,
163
- };
164
- }
165
- }
166
-
167
- /**
168
- * @typedef {object} BioSnapshot
169
- * @property {object} mouse
170
- * @property {object} keyboard
171
- * @property {number} interferenceCoefficient – [−1, 1]; higher = more human
172
- * @property {number} durationMs
173
- * @property {boolean} hasActivity
174
- */
175
-
176
- // ---------------------------------------------------------------------------
177
- // Statistical helpers (private)
178
- // ---------------------------------------------------------------------------
179
-
180
- function _mean(arr) {
181
- if (!arr.length) return 0;
182
- return arr.reduce((a, b) => a + b, 0) / arr.length;
183
- }
184
-
185
- function _variance(arr) {
186
- if (arr.length < 2) return 0;
187
- const m = _mean(arr);
188
- return arr.reduce((s, v) => s + (v - m) ** 2, 0) / (arr.length - 1);
189
- }
190
-
191
- function _cv(arr) {
192
- if (!arr.length) return 0;
193
- const m = _mean(arr);
194
- if (m === 0) return 0;
195
- return Math.sqrt(_variance(arr)) / Math.abs(m);
196
- }
197
-
198
- function _percentile(sorted, p) {
199
- const arr = [...sorted].sort((a, b) => a - b);
200
- if (!arr.length) return 0;
201
- const idx = (p / 100) * (arr.length - 1);
202
- const lo = Math.floor(idx);
203
- const hi = Math.ceil(idx);
204
- return arr[lo] + (arr[hi] - arr[lo]) * (idx - lo);
205
- }
206
-
207
- /** Angular jerk: second derivative of movement direction (radians / s²) */
208
- function _computeAngularJerk(events) {
209
- if (events.length < 3) return [];
210
- const angles = [];
211
- for (let i = 0; i < events.length; i++) {
212
- const { dx, dy } = events[i];
213
- angles.push(Math.atan2(dy, dx));
214
- }
215
- const d1 = [];
216
- for (let i = 1; i < angles.length; i++) {
217
- const dt = events[i].dt || 1;
218
- d1.push((angles[i] - angles[i - 1]) / dt);
219
- }
220
- const d2 = [];
221
- for (let i = 1; i < d1.length; i++) {
222
- const dt = events[i].dt || 1;
223
- d2.push(Math.abs((d1[i] - d1[i - 1]) / dt));
224
- }
225
- return d2;
226
- }
227
-
228
- /**
229
- * Interference Coefficient
230
- *
231
- * For each computation sample, check whether an input event occurred within
232
- * ±16 ms (one animation frame). Build two parallel series:
233
- * X[i] = 1 if input near sample i, else 0
234
- * Y[i] = deviation of timing[i] from mean timing
235
- * Return the Pearson correlation between X and Y.
236
- * A real human on real hardware produces positive correlation (input events
237
- * cause measurable CPU scheduling perturbations).
238
- */
239
- function _computeInterference(mouseEvents, keyEvents, timings) {
240
- if (!timings.length) return 0;
241
-
242
- const allInputTimes = [
243
- ...mouseEvents.map(e => e.t),
244
- ...keyEvents.map(e => e.t),
245
- ].sort((a, b) => a - b);
246
-
247
- if (!allInputTimes.length) return 0;
248
-
249
- const WINDOW_MS = 16;
250
- const meanTiming = _mean(timings);
251
-
252
- // We need absolute timestamps for the probe samples.
253
- // We don't have them directly – use relative index spacing as a proxy.
254
- // The entropy probe runs for ~(mean * n) ms starting at collectedAt.
255
- // This is a statistical approximation; the exact alignment improves
256
- // when callers pass `collectedAt` from the entropy result.
257
- // For now we distribute samples evenly across the collection window.
258
- const first = allInputTimes[0];
259
- const last = allInputTimes[allInputTimes.length - 1];
260
- const span = Math.max(last - first, 1);
261
-
262
- const X = timings.map((_, i) => {
263
- const tSample = first + (i / timings.length) * span;
264
- return allInputTimes.some(t => Math.abs(t - tSample) < WINDOW_MS) ? 1 : 0;
265
- });
266
-
267
- const Y = timings.map(t => t - meanTiming);
268
-
269
- return _pearson(X, Y);
270
- }
271
-
272
- function _pearson(X, Y) {
273
- const n = X.length;
274
- if (n < 2) return 0;
275
- const mx = _mean(X);
276
- const my = _mean(Y);
277
- let num = 0, da = 0, db = 0;
278
- for (let i = 0; i < n; i++) {
279
- const a = X[i] - mx;
280
- const b = Y[i] - my;
281
- num += a * b;
282
- da += a * a;
283
- db += b * b;
284
- }
285
- const denom = Math.sqrt(da * db);
286
- return denom < 1e-14 ? 0 : num / denom;
287
- }
1
+ /**
2
+ * @svrnsec/pulse — Bio-Binding Layer
3
+ *
4
+ * Captures mouse-movement micro-stutters and keystroke-cadence dynamics
5
+ * WHILE the hardware entropy probe is running. Computes the
6
+ * "Interference Coefficient": how much human input jitters hardware timing.
7
+ *
8
+ * PRIVACY NOTE: Only timing deltas are retained. No key labels, no raw
9
+ * (x, y) coordinates, no content of any kind is stored or transmitted.
10
+ */
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Internal state
14
+ // ---------------------------------------------------------------------------
15
+ const MAX_EVENTS = 500; // rolling buffer cap
16
+ const MAX_KEY_STATES = 50; // cap on simultaneous tracked key states
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // BioCollector class
20
+ // ---------------------------------------------------------------------------
21
+ export class BioCollector {
22
+ constructor() {
23
+ this._mouseEvents = []; // { t: DOMHighResTimeStamp, dx, dy }
24
+ this._keyEvents = []; // { t, type: 'down'|'up', dwell: ms|null }
25
+ this._lastKey = {}; // keyCode → { downAt: t }
26
+ this._lastMouse = null; // { t, x, y }
27
+ this._startTime = null;
28
+ this._active = false;
29
+
30
+ // Bound handlers (needed for removeEventListener)
31
+ this._onMouseMove = this._onMouseMove.bind(this);
32
+ this._onKeyDown = this._onKeyDown.bind(this);
33
+ this._onKeyUp = this._onKeyUp.bind(this);
34
+ }
35
+
36
+ // ── Lifecycle ────────────────────────────────────────────────────────────
37
+
38
+ start() {
39
+ if (this._active) return;
40
+ this._active = true;
41
+ this._startTime = performance.now();
42
+
43
+ if (typeof window !== 'undefined') {
44
+ window.addEventListener('pointermove', this._onMouseMove, { passive: true });
45
+ window.addEventListener('keydown', this._onKeyDown, { passive: true });
46
+ window.addEventListener('keyup', this._onKeyUp, { passive: true });
47
+ }
48
+ }
49
+
50
+ stop() {
51
+ if (!this._active) return;
52
+ this._active = false;
53
+
54
+ if (typeof window !== 'undefined') {
55
+ window.removeEventListener('pointermove', this._onMouseMove);
56
+ window.removeEventListener('keydown', this._onKeyDown);
57
+ window.removeEventListener('keyup', this._onKeyUp);
58
+ }
59
+
60
+ this._lastMouse = null;
61
+ }
62
+
63
+ // ── Event handlers ────────────────────────────────────────────────────────
64
+
65
+ _onMouseMove(e) {
66
+ if (!this._active) return;
67
+ const t = e.timeStamp ?? performance.now();
68
+ const cur = { t, x: e.clientX, y: e.clientY };
69
+
70
+ if (this._lastMouse) {
71
+ const dt = t - this._lastMouse.t;
72
+ const dx = cur.x - this._lastMouse.x;
73
+ const dy = cur.y - this._lastMouse.y;
74
+ // Only store the delta, not absolute position (privacy)
75
+ if (this._mouseEvents.length < MAX_EVENTS) {
76
+ this._mouseEvents.push({ t, dt, dx, dy,
77
+ pressure: e.pressure ?? 0,
78
+ pointerType: e.pointerType ?? 'mouse' });
79
+ }
80
+ }
81
+ this._lastMouse = cur;
82
+ }
83
+
84
+ _onKeyDown(e) {
85
+ if (!this._active) return;
86
+ const t = e.timeStamp ?? performance.now();
87
+ // Store timestamp keyed by code (NOT key label)
88
+ this._lastKey[e.code] = { downAt: t };
89
+ // Prevent unbounded growth if keys are held without keyup
90
+ if (Object.keys(this._lastKey).length > MAX_KEY_STATES) {
91
+ const oldest = Object.entries(this._lastKey)
92
+ .sort((a, b) => a[1].downAt - b[1].downAt)[0];
93
+ if (oldest) delete this._lastKey[oldest[0]];
94
+ }
95
+ }
96
+
97
+ _onKeyUp(e) {
98
+ if (!this._active) return;
99
+ const t = e.timeStamp ?? performance.now();
100
+ const rec = this._lastKey[e.code];
101
+ const dwell = rec ? (t - rec.downAt) : null;
102
+ delete this._lastKey[e.code];
103
+
104
+ if (this._keyEvents.length < MAX_EVENTS) {
105
+ // Only dwell time; key identity NOT stored.
106
+ this._keyEvents.push({ t, dwell });
107
+ }
108
+ }
109
+
110
+ // ── snapshot ─────────────────────────────────────────────────────────────
111
+
112
+ /**
113
+ * Returns a privacy-preserving statistical snapshot of collected bio signals.
114
+ * Raw events are summarised; nothing identifiable is included in the output.
115
+ *
116
+ * @param {number[]} computationTimings - entropy probe timing array
117
+ * @returns {BioSnapshot}
118
+ */
119
+ snapshot(computationTimings = []) {
120
+ const now = performance.now();
121
+ const durationMs = this._startTime != null ? (now - this._startTime) : 0;
122
+
123
+ // ── Mouse statistics ────────────────────────────────────────────────
124
+ const iei = this._mouseEvents.map(e => e.dt);
125
+ const velocities = this._mouseEvents.map(e =>
126
+ e.dt > 0 ? Math.hypot(e.dx, e.dy) / e.dt : 0
127
+ );
128
+ const pressure = this._mouseEvents.map(e => e.pressure);
129
+ const angJerk = _computeAngularJerk(this._mouseEvents);
130
+
131
+ const mouseStats = {
132
+ sampleCount: iei.length,
133
+ ieiMean: _mean(iei),
134
+ ieiCV: _cv(iei),
135
+ velocityP50: _percentile(velocities, 50),
136
+ velocityP95: _percentile(velocities, 95),
137
+ angularJerkMean: _mean(angJerk),
138
+ pressureVariance: _variance(pressure),
139
+ };
140
+
141
+ // ── Keyboard statistics ───────────────────────────────────────────────
142
+ const dwellTimes = this._keyEvents.filter(e => e.dwell != null).map(e => e.dwell);
143
+ const iki = [];
144
+ for (let i = 1; i < this._keyEvents.length; i++) {
145
+ iki.push(this._keyEvents[i].t - this._keyEvents[i - 1].t);
146
+ }
147
+
148
+ const keyStats = {
149
+ sampleCount: dwellTimes.length,
150
+ dwellMean: _mean(dwellTimes),
151
+ dwellCV: _cv(dwellTimes),
152
+ ikiMean: _mean(iki),
153
+ ikiCV: _cv(iki),
154
+ };
155
+
156
+ // ── Interference Coefficient ──────────────────────────────────────────
157
+ // Cross-correlate input event density with computation timing deviations.
158
+ // A real human on real hardware creates measurable CPU-scheduling pressure
159
+ // that perturbs the entropy probe's timing.
160
+ const interferenceCoefficient = _computeInterference(
161
+ this._mouseEvents,
162
+ this._keyEvents,
163
+ computationTimings,
164
+ );
165
+
166
+ return {
167
+ mouse: mouseStats,
168
+ keyboard: keyStats,
169
+ interferenceCoefficient,
170
+ durationMs,
171
+ hasActivity: iei.length > 5 || dwellTimes.length > 2,
172
+ };
173
+ }
174
+ }
175
+
176
+ /**
177
+ * @typedef {object} BioSnapshot
178
+ * @property {object} mouse
179
+ * @property {object} keyboard
180
+ * @property {number} interferenceCoefficient – [−1, 1]; higher = more human
181
+ * @property {number} durationMs
182
+ * @property {boolean} hasActivity
183
+ */
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Statistical helpers (private)
187
+ // ---------------------------------------------------------------------------
188
+
189
+ function _mean(arr) {
190
+ if (!arr.length) return 0;
191
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
192
+ }
193
+
194
+ function _variance(arr) {
195
+ if (arr.length < 2) return 0;
196
+ const m = _mean(arr);
197
+ return arr.reduce((s, v) => s + (v - m) ** 2, 0) / (arr.length - 1);
198
+ }
199
+
200
+ function _cv(arr) {
201
+ if (!arr.length) return 0;
202
+ const m = _mean(arr);
203
+ if (m === 0) return 0;
204
+ return Math.sqrt(_variance(arr)) / Math.abs(m);
205
+ }
206
+
207
+ function _percentile(sorted, p) {
208
+ const arr = [...sorted].sort((a, b) => a - b);
209
+ if (!arr.length) return 0;
210
+ const idx = (p / 100) * (arr.length - 1);
211
+ const lo = Math.floor(idx);
212
+ const hi = Math.ceil(idx);
213
+ return arr[lo] + (arr[hi] - arr[lo]) * (idx - lo);
214
+ }
215
+
216
+ /** Angular jerk: second derivative of movement direction (radians / s²) */
217
+ function _computeAngularJerk(events) {
218
+ if (events.length < 3) return [];
219
+ const angles = [];
220
+ for (let i = 0; i < events.length; i++) {
221
+ const { dx, dy } = events[i];
222
+ angles.push(Math.atan2(dy, dx));
223
+ }
224
+ const d1 = [];
225
+ for (let i = 1; i < angles.length; i++) {
226
+ const dt = events[i].dt || 1;
227
+ d1.push((angles[i] - angles[i - 1]) / dt);
228
+ }
229
+ const d2 = [];
230
+ for (let i = 1; i < d1.length; i++) {
231
+ const dt = events[i].dt || 1;
232
+ d2.push(Math.abs((d1[i] - d1[i - 1]) / dt));
233
+ }
234
+ return d2;
235
+ }
236
+
237
+ /**
238
+ * Interference Coefficient
239
+ *
240
+ * For each computation sample, check whether an input event occurred within
241
+ * ±16 ms (one animation frame). Build two parallel series:
242
+ * X[i] = 1 if input near sample i, else 0
243
+ * Y[i] = deviation of timing[i] from mean timing
244
+ * Return the Pearson correlation between X and Y.
245
+ * A real human on real hardware produces positive correlation (input events
246
+ * cause measurable CPU scheduling perturbations).
247
+ */
248
+ function _computeInterference(mouseEvents, keyEvents, timings) {
249
+ if (!timings.length) return 0;
250
+
251
+ const allInputTimes = [
252
+ ...mouseEvents.map(e => e.t),
253
+ ...keyEvents.map(e => e.t),
254
+ ].sort((a, b) => a - b);
255
+
256
+ if (!allInputTimes.length) return 0;
257
+
258
+ const WINDOW_MS = 16;
259
+ const meanTiming = _mean(timings);
260
+
261
+ // Note: timing alignment is approximate; probe start timestamp would improve accuracy
262
+ // We need absolute timestamps for the probe samples.
263
+ // We don't have them directly use relative index spacing as a proxy.
264
+ // The entropy probe runs for ~(mean * n) ms starting at collectedAt.
265
+ // This is a statistical approximation; the exact alignment improves
266
+ // when callers pass `collectedAt` from the entropy result.
267
+ // For now we distribute samples evenly across the collection window.
268
+ const first = allInputTimes[0];
269
+ const last = allInputTimes[allInputTimes.length - 1];
270
+ const span = Math.max(last - first, 1);
271
+
272
+ const X = timings.map((_, i) => {
273
+ const tSample = first + (i / timings.length) * span;
274
+ return allInputTimes.some(t => Math.abs(t - tSample) < WINDOW_MS) ? 1 : 0;
275
+ });
276
+
277
+ const Y = timings.map(t => t - meanTiming);
278
+
279
+ return _pearson(X, Y);
280
+ }
281
+
282
+ function _pearson(X, Y) {
283
+ const n = X.length;
284
+ if (n < 2) return 0;
285
+ const mx = _mean(X);
286
+ const my = _mean(Y);
287
+ let num = 0, da = 0, db = 0;
288
+ for (let i = 0; i < n; i++) {
289
+ const a = X[i] - mx;
290
+ const b = Y[i] - my;
291
+ num += a * b;
292
+ da += a * a;
293
+ db += b * b;
294
+ }
295
+ const denom = Math.sqrt(da * db);
296
+ return denom < 1e-14 ? 0 : num / denom;
297
+ }